很早前就看了很多类加载机制的文章,但都零零碎碎的,此次借着阅读深入理解Java虚拟机一书的契机,归纳一下碎片化的知识。
所谓的“类加载机制”并不单单指“加载”这一过程。我们的类在编译后会形成.class
文件,在运行时,JVM先要将.class文件加载
进虚拟机内存。这个过程,其实就是虚拟机将文件以一串二进制流的形式读到自己空间中来。
上面表述可能不太正确,Java虚拟机规范并没有指出以哪种方式读取文件,可能是一串二进制流,有可能是其他方式,这个取决于JVM的实现。
具体过程如下,其中验证、准备、解析三个阶段又叫做连接
,关于这个连接的理解,我个人觉得就是虚拟机真正访问class文件流里内容
的过程。而为了防止class文件中有破坏虚拟机正常执行的指令,所以在加载后,才需要验证
这一步骤。怎样验证呢?如文件格式验证:
此外,还有字节码验证
和符号引用验证
,其中,字节码验证
为了确保没有危害虚拟机安全的指令,比如一个跳转指令是否会其他方法体的字节码上。符号引用验证
则验证类自身以外的信息,比如,通过编译存放于常量池用来描述某个类文件的全限定名符号,来判断是否可以正常访问到该类。符号引用验证
是为了保证解后边的解析阶段
的正常执行。
验证
之后是准备
阶段,在这个阶段JVM为(static修饰的
)变量分配内存
和初始值
,注意这里不包括实例变量,实例变量会随着对象的初始化,一起在堆中分配内存。另外,这里的初始化应该也是属于半初始化
阶段,例如下面代码,此时这个阶段是int的默认0值而不是100,关于100的赋值指令,在编译后,存放到构造方法
内了,需要在初始化阶段
才会真正的赋值:
private static int a = 100;
关于private static final int a = 100;
则有所不同,final修饰的变量,在编译阶段,会将a字段属性会被表示为ConstantValue
这样,在准备阶段a就能被赋值为100了,这也恰好可以解释如下代码。
byte a = 12;
byte b = 6;
byte c = a + b;(编译报错)
byte c = (byte)(a + b)(编译通过)
加了final 修饰的a、b在编译阶段就正常赋值了,此时可以直接得出18,即使相加运算会转换成默默向上装int类型
,但18属于byte的范围,所以byte c = a + b此时不需要强转
也不会报错。
final byte a = 12;
final byte b = 6;
byte c = a + b;(编译通过)
这里截取部分字节码做对比,不加final:
public static void main(java.lang.String[]);
Code:
0: bipush 12
2: istore_1
3: bipush 6
5: istore_2
加了final之后
public static void main(java.lang.String[]);
Code:
0: bipush 18
2: istore_3
3: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
6: iload_3
7: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
10: return
}
准备
之后是解析
阶段了,在这个阶段符号引用
为被替代为直接引用
,所谓的符号引用,个人理解为引用某个目标(可能会使用的意思)的符号,这个目标这个阶段只代表可能使用,此时可能还没有在内存中,而将符号引用
转为直接引用
,可以表示目标一定存在于内存中了。
注意,解析的运行时机不是确定的,它可能也在初始化之后才发生,譬如通过反射进行运行时的动态绑定。
最后是,初始化
阶段,在上面有提到过“半初始化”,那时的静态变量a的初始值并不是我们预先设置的100,而是0,这个阶段的初始化,则会将100赋值给a。这个阶段会真正执行我们编译后的字节码,注意,这里的初始化应该还是指的类层次的初始化
,或者说
的初始化过程。而
或者普通实例对象的内存分配,应该是在使用时才初始化和分配的。
cinit为静态类构造函数,编译器自动帮我们生成的,init为普通构造函数的指令。
何时会进行类加载机制的第一步“加载”呢?java虚拟机规范指出的我们主动引用一个类的时候,一定会加载的,其他情况并没有强制固定。
这个主动引用,大致分为如下情况:
JDK1.7引入了MethodHandle
类,这个Method Handles的引入和java.lang.reflect API相配合。当我们解析后得到的是一个静态的方法时,如果该方法对应的类还没初始化,此时会进行初始化。
// 获取方法类型
// 参数为:1.返回值类型,2方法中参数类型
MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle mh = null;
try {
mh = MethodHandles.lookup().findVirtual(MHTest.class, "toString", mt); //查找方法句柄
} catch (NoSuchMethodException | IllegalAccessException e) {
e.printStackTrace();
}
return mh;