前言
Java中ClassLoader负责加载class文件到JVM中,ClassLoader是一个抽象类。在给定一个class的二进制文件后,它会尝试加载并且在JVM中生成构成这个类的各个数据结构,分布在JVM相对应的内存区域中。
类的加载过程
加载阶段:查找并且加载类的二进制数据文件,即class文件。
连接阶段:
- 验证:确保类文件的正确性,例如class版本号,class文件的魔术因子是否正确等。
- 准备:为类的静态变量分配内存,并且为其初始化默认值。
- 解析:把类中的符号引用转换为直接引用。
初始化阶段:类的静态变量赋予正确的初始值,在代码编写阶段给定的值。
类的主动使用和被动使用
JVM虚拟机规范规定了,每一个类或者接口被Java程序首次主动使用时才会对其进行初始化。JVM同时规定了以下6种主动使用类的场景。
1.通过new关键字会导致类的初始化。
2.访问类的静态变量,包括读取和更新,示例代码如下:
1 public class Simple { 2 3 static { 4 System.out.println("Simple被初始化"); 5 } 6 7 public static int x = 10; 8 9 } 10 11 class Test { 12 public static void main(String[] args) { 13 System.out.println(Simple.x); 14 } 15 }
3.访问类的静态方法,会导致类的初始化。
4.对某个类进行反射操作,会导致类的初始化
1 public class Simple { 2 3 static { 4 System.out.println("Simple被初始化"); 5 } 6 7 public static int x = 10; 8 9 } 10 11 class Test { 12 public static void main(String[] args) throws ClassNotFoundException { 13 Class.forName("test.classloader.Simple"); 14 } 15 }
5.初始化子类会导致父类的初始化。这里有一点需要注意下:通过子类使用父类的静态变量只会导致父类的初始化,子类不会被初始化。
6.启动类。执行main方法所在的类会导致该类的初始化。
除了上述6种情况外,其余的都是被动使用,不会导致类的加载和初始化。
有几点要注意下:
1.构造某一个类的数组时,并不会导致该类的初始化。实际上,这种操作只是在堆内存中开辟了一段连续的地址空间。
2.引用类的静态常量不会导致类的初始化。看看下面这个例子:
1 public class Simple { 2 3 static { 4 System.out.println("Simple被初始化"); 5 } 6 7 public final static int MAX = 100; 8 9 public final static int RANDOM = new Random().nextInt(); 10 11 }
在其他类中使用Max不会导致初始化,虽然它也是被static修饰的,但是访问RANDOM则会导致类的初始化。是因为它要进行随机函数的计算,在类的加载,连接阶段是无法对其进行计算的,需要进行初始化后才能对其赋予正确的值。
类的加载过程
先看下面的一段程序代码:结果?
1 public class Simple { 2 3 // 1. 4 private static int x = 0; 5 private static int y; 6 7 private static Simple instance = new Simple(); // 2. 8 9 private Simple() { 10 x++; 11 y++; 12 } 13 14 public static Simple getInstance() { 15 return instance; 16 } 17 18 public static void main(String[] args) { 19 Simple instance = Simple.getInstance(); 20 System.out.println(instance.x); 21 System.out.println(instance.y); 22 } 23 24 }
1)加载二进制class文件
2)连接阶段
验证
- 验证文件格式
- 验证该二进制文件是属于什么类型的文件
- 主次版本号,高版本的jdk编译的class不能被低版本的jvm兼容,检查当前的class文件版本是否符合当前的jdk所处理的范围。
- 常量池中的常量是否存在不被支持的变量类型。如int64
- 指向常量中的引用是否指到了不存在的常量或者是该常量的类型不被支持。
- ...
- 元数据的验证
- 对class字节流进行语义分析的过程。
- ...
- 字节码验证
...
- 符号引用验证
主要是为了验证符号引用装换为直接引用的合法性。
1.通过符号引用描述的字符串全限定名称是否能够顺利的找到相关的类。
2.符号引用中的类、字段、方法、是否对当前类可见,比如不能访问引用类的私有方法
...
注:符号引用验证是为了保证解析动作的顺利执行,比如:如果某个类的字段不存在,则会抛出NoSuchFieldError。
准备
准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配。关于这点,有两个地方注意一下:
1、这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。
2、这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如"public static int value = 123;",value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如"public static final int value = 123;"就不一样了,在准备阶段,虚拟机就会给value赋值为123。
各个数据类型的零值如下图:
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。来了解一下符号引用和直接引用有什么区别:
1、符号引用。
这个其实是属于编译原理方面的概念,符号引用包括了下面三类常量:
· 类和接口的全限定名
· 字段的名称和描述符
· 方法的名称和描述符
2、直接引用
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经存在在内存中了。
初始化
初始化阶段是类加载过程的最后一步,初始化阶段是真正执行类中定义的Java程序代码(或者说是字节码)的过程。初始化过程是一个执行类构造器<clinit>()方法的过程,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。把这句话说白一点,其实初始化阶段做的事就是给static变量赋予用户指定的值以及执行静态代码块。
注意一下,虚拟机会保证类的初始化在多线程环境中被正确地加锁、同步,即如果多个线程同时去初始化一个类,那么只会有一个类去执行这个类的<clinit>()方法,其他线程都要阻塞等待,直至活动线程执行<clinit>()方法完毕。因此如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。不过其他线程虽然会阻塞,但是执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程不会再次进入<clinit>()方法了,因为同一个类加载器下,一个类只会初始化一次。实际应用中这种阻塞往往是比较隐蔽的,要小心。