java类加载机制
使用某个类是发生的事情
加载->链接(验证、准备、解析)->初始化->可以使用
加载:将class文件字节码内容加载到内存当中,并将这些静态数据转换成方法区中的运行时数据结构
在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。
链接:将java类的二进制代码合并到jvm的运行状态之中的过程
验证:确保加载的类信息符合jvm的规范,没有安全方面的问题。(确保加载的二进制代码不会损坏jvm)。
准备:正式为类变量(static变量,此时初始化为0,在后续的初始化时会将用户的初始化值赋给类变量)分配内存并设置变量初始值的阶段,这些内存都将在方法区中进行分配。
解析:虚拟机常量池内的符号引用替换为直接引用的过程。(注:常量池内的内容包括类名、类内部的变量名、字符串常量等等)
初始化:
初始化阶段是执行类构造器<clinit>()方法的过程,类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的(简言之,就是合并类的静态变量和静态代码块,形成类构造器进行执行)。
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
当访问一个java类的静态域时,只有真正声明这个域的类才会被初始化。
如下代码说明了初始化阶段为执行类构造器<clinit>()的过程,即类构造器的执行是先于类的构造方法的。
class A { public static int width = 100; static { System.out.println("before 静态初始化类A"+A.width); width = 300; System.out.println("after 静态初始化类A"+A.width); } public A(){ System.out.println("创建A类的对象"); } } public class Demo { public static void main(String[] args) { A a = new A(); System.out.println(A.width); } }
可以看到类构造方法<clinit>()的执行在构造器之前。
下面代码则说明了当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
package com.test; class A_Father{ static { System.out.println("静态初始化A_Father"); } } class A extends A_Father{ public static int width = 100; static { System.out.println("before 静态初始化类A"+A.width); width = 300; System.out.println("after 静态初始化类A"+A.width); } public A(){ System.out.println("创建A类的对象"); } } public class Demo { public static void main(String[] args) { A a = new A(); System.out.println(A.width); } }
看如下的代码分析输出
package com.test; class A_Father{ static { System.out.println("静态初始化A_Father"); } } class A extends A_Father{ public static int width = 100; static { System.out.println("before 静态初始化类A"+A.width); width = 300; System.out.println("after 静态初始化类A"+A.width); } public A(){ System.out.println("创建A类的对象"); } } public class Demo { static{ System.out.println("静态初始化Demo"); } public static void main(String[] args) { System.out.println("Demo的main方法"); A a = new A(); System.out.println(A.width); A a2 = new A(); //java.lang.Class<A> aclass = A.class; //Class aclass2 = A.class; } }
输出如下:
静态初始化Demo
Demo的main方法
静态初始化A_Father
before 静态初始化类A100
after 静态初始化类A300
创建A类的对象
300
创建A类的对象
首先程序执行时要加载public的Demo类,初始化类(执行类构造方法)于是打印了“静态初始化Demo”,类初始化完成后进入方法调用,即main方法中,输出“Demo的main方法”,此时执行下一句 A a = new A(); 因为A类此时尚未加载,所以首先要加载类A,然后链接初始化,初始化时发现A还有个父类没有加载呢,于是又去加载父类,链接初始化父类,打印了消息“静态初始化A_Father”,父类初始化完成了,此时类A的初始化开始,于是打印了“before 静态初始化类A100” 和“after 静态初始化类A300”,到这个时候,类已经全部加载到内存当中了,于是可以创建A的对象了,于是打印“创建A类的对象”,执行打印语句输出“300”,最后又创建了A的一个对象,但是由于类已经加载过了,所以只会调用类的构造器输出“创建A类的对象”,说明了类的加载只有一次,而类的实例化可以有多次。
关于类的加载有一个细节:
类的加载分为类的被动引用和类的主动引用,类的被动引用不会发生类的初始化,类的主动引用一定会发生类的初始化。
类的被动引用有:
1.当访问一个静态域时,只有真正声明这个域的类才会被初始化(通过子类引用父类的静态变量不会导致子类初始化,只有父类被初始化)
2.通过数组定义类引用,不会触发此类的初始化。
3.引用常量不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)
类的主动引用有:
1.new一个类的对象
2.调用类额静态成员(除了final常量)和静态方法。
3.使用java.lang.reflect包的方法对类进行反射调用
4.当虚拟机启动,java Hello,则一定会初始化Hello类(直白的说就是先加载main方法所在的类)
5.当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类。
测试代码:
package com.test; class A_Father{ static { System.out.println("静态初始化A_Father"); } } class A extends A_Father{ public static int width = 100; public static final int MAX = 100; static { System.out.println("before 静态初始化类A"+A.width); width = 300; System.out.println("after 静态初始化类A"+A.width); } public static void test(){ System.out.println("静态方法"); } public A(){ System.out.println("创建A类的对象"); } } class B extends A{ static { System.out.println("静态初始化B"); } } public class Demo { static{ System.out.println("静态初始化Demo"); } public static void main(String[] args) throws ClassNotFoundException { System.out.println("Demo的main方法"); //主动加载 //new A(); //System.out.println(A.width); //A.test(); //Class.forName("com.test.A"); //被动加载 //System.out.println(A.MAX); //A[] as = new A[10]; System.out.println(B.width); } }