类加载过程
案例分析:
class Super{
static{
System.out.println("Super");
}
public static int value = 123;
}
class Sub extends Super{
static{
System.out.println("Sub");
}
}
public class{
public static void main(String[] args) {
System.out.println(Sub.value);
}
//不会输出Sub,因为对于静态字段,只有直接定义这个字段的类才会被初始化,所以子类不会被初始化
public class Main{
public static void main(String[] args) {
Super[] sup=new Super[10];
}
//依然不会打印Sup,Super类没有被初始化,但这段代码触发了另一类的初始化阶段,该类有虚拟机自动生成,直接继承Object,它代表了一个元素类型为Super的一维数组。
class A1{
static final int b=2;
static{
System.out.println("1111");
}
}
public class Main{
public static void main(String[] args) throws InterruptedException {
System.out.println(A1.b);
}
//只会输出b,并不会输出1111。首先该程序会加载Main类(因为他有main方法),A1的b在编译阶段通过常量传播优化,已经转到了引用它的的类的常量池中,所以不会再去加载A1了
1、加载:
1、通过全类名获取定义此类的二进制字节流(从zip,jar,war,ear,网络,其他文件,,数据库等地方获取)
2、将字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存(并没有明确规定在堆中,就HotSpot而言,Class对象放在方法区中)中生成一个代表该类的Class对象,作为方法区这些数据的访问入口
2、连接:
1、验证:
文件格式验证,元数据验证,字节码验证,符号引用验证
2、准备:
正式为类变量分配内存并设置类变量初始值的阶段:
- 这里进行内存分配的仅包括static,不包括实例变量,实例变量会在对象实例化时随对象一块分配在堆中
- 若该static变量时final,则从常量池中将初始者赋给它。
- 否则是该数据类型默认的零值(如0、0L、null、false等)
3、解析:
将常量池内的符号引用替换为直接引用的过程。
(符号引用:以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时输入无歧义的定位到目标即可。直接引用:直接指向目标的指针、相对偏移量或能间接定位到的句柄)
3、初始化:
5种必须立即对类初始化的情况(加载验证准备自然需要在此之前开始)
- 遇到new,getstatic,putstatic,invokestatic时,触发初始化。代码场景是读取或设置一个类的静态字段(被final修饰,在准备就放入常量池的静态字段除外),以及调用一个类的静态方法时。
- 使用java.lang.reflect包的方法对类进行反射调用。
- 初始化一个类的时候,如果其父类还没有进行过初始化,则需要先触发父类的初始化。
- 虚拟机会先初始化主类(含main方法的类)
- 当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化。
初始化是执行类加载器的()方法的过程:
-
()方法是由编译器自动收集类中的所有类变量(static)的赋值动作和静态语句块(static{})中的语句合并产生的。收集顺序是由出现顺序而定的,(不论是static{a=3},还是static int a=4;,都是按出现顺序,比如该顺序的话a==4)静态语句块只能访问之前的变量,之后的只能赋值,不能访问。
-
()方法与构造函数不同,不需要显示的调用父类构造器,因为在执行()方法之前父类的()方法就已经执行完毕。
-
()方法不是必须的,如果类中没有类变量和静态语句块,就不会生成。
-
接口中不能使用静态语句块,但仍有变量初始化的赋值操作,因此接口也会生成()方法,不同的是,执行接口的()方法并不需要先执行父接口的()方法,只有父接口中定义的变量使用时,父接口才会初始化,此外接口的实现类在初始化是也不会执行接口的()方法。
-
虚拟机会保证一个类的()方法在多线程中被正确的加锁、同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的()方法,其他线程都会阻塞等待,如果一个类的()方法耗时很长,就可能造成多个进程阻塞,这种阻塞通常是隐蔽的。
类加载器
设计者将类加载阶段的“通过全类名获取定义此类的二进制字节流”这个动作放到jvm外部实现,以便让应用程序自己决定如何获取所需的类。实现该动作的代码称为“类加载器”。
双亲委派模型:
要求:除了顶层的启动类加载器外,其余的类加载器都应当有自己的父亲加载器,这里类加载器之间的父子关系一般不会是继承,而是组合的关系来复用父加载器的代码。
工作过程:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都如此,因此所有的加载请求最终都传到顶层,只有顶层自己无法完成这个加载请求(搜索范围内没找到),子加载器才会尝试自己去加载。
好处:让Java类随着它的加载器一起具备了优先级的层级关系,比如java.lang.Object类,无论哪个类加载器要加载这个类最终都会委派到顶层的启动类加载器,因此Object类在程序的各种类加载器环境中都是同一个类。如果不使用该模型,由各个类加载器自行加载,如果用户自己编写了一个java.lang.Object的类,并放在ClassPath下,那系统就会出现多个不同的Object类,应用程序会变得混乱。
参考资料:深入理解Java虚拟机