类加载基本介绍
JAVA类加载分为三步: 加载、连接 、初始化。
类加载的最终产物是位于heap(堆)中的类对象,Class对象封装了在方法区内的类的数据结构,提供了访问方法区类数据结构的接口(即反射接口)。
下面看一段代码:
class Singleton { private static Singleton singleton=new Singleton(); public static int counter1; public static int counter2=0; private Singleton(){ counter1++; counter2++; } public static Singleton getInstance(){ return singleton; } } public class Test { public static void main(String[] args) { Singleton singleton=Singleton.getInstance(); System.out.println(singleton.counter1); System.out.println(singleton.counter2); } }
输出是1和0,看完下面你就知道为什么了
类加载过程
加载:将位于硬盘中的class文件加载到内存中。
连接:将已经加载到内存中的二进制数据合并到虚拟机的运行时环境中去。
验证:
1.确保类文件遵从java类文件的固定格式。
2.确定类本身符合java语言的语法规定,比如final修饰的类没有子类,以及final类型的方法没有被覆盖(因为可以手动生成class文件,绕过编译过程)。
3.字节码验证:确保字节码流可以被java虚拟机安全的执行。字节码流代表java方法(包括静态方法和实例方法),它是由被称作操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。字节码验证步骤会检查每个操作码是否合法,即是否有合法的操作数。
4.二进制兼容性的验证:确保相互引用的类之间协调一致。(例如Worker类的gotoWork()方法调用了Catr类的run()方法,java虚拟机在验证Worker类时,会检查方法区内是否存在Car类的run()方法,假如不存在(两个类的jdk版本不兼容,就会出现这种问题),就会抛出NoSuchMethodError错误)。
准备:
将class对象的静态变量分配内存空间并赋予默认值,比如int类型的默认值为0,引用对象比如一个自定义的Person对象默认值为null。
解析:
将类的二进制数据中的符号引用转换为直接引用例如Worker类的gotoWork()方法调用了Catr类的run()方法,Worker类的二进制数据中包含了一个对run()方法的符号引用。在解析阶段将符号引用(符号引用:由方法的全名和相关描述组成)变为直接引用(直接引用:指针),也就是说由描述变为内存地址。
初始化:
将类的静态变量赋予初始值。如果在类中静态变量没有被赋予初值,那初始化时将保持他的默认值。
什么情况下触发类初始化?
类被主动使用 6种情况1.静态变量被使用或静态变量被赋值
2.静态方法被调用
3.反射 class.forName()
4.当子类被初始化时发现父类未被初始化
5.创建类的实例
6.java虚拟机启动时被辨明为启动类的类
看到这应该可以明白上面单例的代码为什么会输出1和0了
分析:
首先调用静态getInstance()方法,类加载
类加载过程:
第一步,加载,将类加载到内存中方法区。
第二步,连接时进行准备,将静态变量赋予默认值。
singleton默认值为null
counter1默认值为0
counter2默认值为0,此处的0并不是=0赋值的0,赋值在之后初始化时进行,此时赋予的int类型的默认值。
第三步,类初始化
singleton指向new Singleton 调用了构造方法此时执行构造方法,counter1++,此时counter1的值为1,counter2++,此时counter2的值为1。此时对counter1进行初始化赋值,由于counter1没有在定义变量时赋值,所以counter1的值为1。此时对counter2进行初始化赋值,定义变量时counter2=0,因此counter2的值变为0。
初始化语句
静态变量的声明语句,以及静态代码块都是类的初始化语句(也就是类初始化的时候会被执行的语句),java虚拟机按照初始化语句在类文件中的先后顺序依次执行它们。
父类初始化
程序中对子类的主动使用会导致父类初始化;但对父类的主动使用并不会导致对子类的初始化
class Parent2{ static int a=3; static{ System.out.println("Parent2 static bolck"); } } class child2 extends Parent2{ static int b=4; static { System.out.println("child2 static block"); } } public class Test5 { static { System.out.println("Test5 static block"); } public static void main(String[] args) { Parent2 parent; System.out.println("--------------------"); parent=new Parent2(); System.out.println(Parent2.a); System.out.println(child2.b); } }
类的初始化步骤
1.假如这个类还没有被加载和连接,那就先进行加载和连接。
2.假如类存在直接的父类,并且这个父类还没有被初始化,那么先初始化父类
3.假如类中存在初始化语句,那就依次执行这些初始化语句
主动使用的陷阱
只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用
class Parent3{ static int a=3; static { System.out.println("Parent3 static block"); } static void doSomething(){ System.out.println("doSomething"); } } class Child3 extends Parent3{ static int b=4; static { System.out.println("Child3 static block"); } } public class Test6 { public static void main(String[] args) { System.out.println(Child3.a); Child3.doSomething(); } }
调用常量会导致类初始化么
常量如果在编译期能确定其值,那么常量被调用并不会导致类的初始化(类初始化条件之一为:调用类的静态变量);反之,常量如果在编译期不能能确定其值,必须在运行时确定值那么会导致类的初始化
class FinalTest{ public static final int x=6/3; static { System.out.println("Final static block"); } } public class Test2 { public static void main(String[] args) { System.out.println(FinalTest.x); } }
此时并不会输出 Final static block ,只会输出 2。因为x=2在编译期就被确定了,根据常量传播优化,已经将此常量x的值“2“,存储到了Test2类的常量池中,以后Test2对常量FinalTest.x的引用实际都转化为Test2类对自身常量池的引用了。也就是说,实际上Test2的Class文件中并没有FinalTest类的符号引用入口,这两个类在编译成class文件后就不存在任何关系了。
class FinalTest2{ public static final int x=new Random().nextInt(100); static { System.out.println("FinalTest2 static block"); } } public class Test3 { public static void main(String[] args) { System.out.println(FinalTest2.x); } }
最后
当java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口
1.在初始化一个类时,并不会先初始化它实现的接口
2.在初始化一个接口时,并不会先初始化它的父接口
接口,只有当程序使用接口的静态变量时,才会导致该接口的初始化
参考:张龙老师的JAVASE视频