【Java虚拟机2】Java类加载机制
前言
JAVA代码经过编译从源码变为字节码,字节码可以被JVM解读,使得JVM屏蔽了语言级别的限制。才有了现在的kotlin、Scala、Clojure、Groovy等语言。
字节码文件中描述了类的各种信息,都需要加载到虚拟机之后才能运行和使用。
简单学习了类加载进制后,写一篇文章记录一下以便加深记忆与理解。
类加载概述
- 什么是类加载机制?
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换、解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。 - 类加载机制是在编译期还是运行期?
类型的加载、连接和初始化过程都是在程序运行期间完成的。
缺点:类加载有一些性能消耗,IO
优点:为Java应用提供了极高的扩展性和灵活性
Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。例如,编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类,用户可以通过Java预置的或自定义类加载器,让某个本地的应用程序在运行时从网络或其他地方上加载一个二进制流作为其程序代码的一部分。
- 类加载的7个阶段
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。这七个阶段的发生顺序下图所示。
图中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。强调这点是因为这几个阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。
加载
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,不要混淆这两个看起来很相似的名词。在加载阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
- 相对于类加载过程的其他阶段,非数组类型的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的阶段。这就是下一个章节【Java类加载器】的前提。
- 加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。
我是这样理解的:class文件一般很多,当加载了一部分文件,已经可以开始验证了,比如验证字节码是否符合虚拟机规范的规定。 - 查看加载与卸载的过程,可以分别使用虚拟机参数
-XX:+TraceClassLoading
与-XX:+TraceClassUnloading
虚拟机参数简介:
-XX:+<option> 表示开启option选项
-XX:-<option> 表示关闭option选项
-XX:<option>=<value> 表示把option选项的值设置为value
忘记从哪里看到的了,找了很久也没找到,说的是如果类被加载,但加载失败,但是程序并未主动使用该类,不会报NoClassDefFoundError。这一点待验证(不要参考这一点)。
后来想起是从张龙老师视频中找到的,不知道正确与否啊?
但想想也说得过去,如果加载失败,但是都不参与连接阶段,那么就不需要从二进制流读入内存,不报错也无所谓。但是具体的虚拟机如Hotspot是怎么实现的尚不知。
验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
Java语言本身是相对安全的编程语言(起码对于C/C++来说是相对安全的),但Class文件并不一定只能由Java源码编译而来,它可以使用包括靠键盘0和1直接在二进制编辑器中敲出Class文件在内的任何途径产生。上述Java代码无法做到的事情在字节码层面上都是可以实现的,至少语义上是可以表达出来的。Java虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃,所以验证字节码是Java虚拟机保护自身的一项必要措施。
验证阶段将做一下几个工作:
- 文件格式验证:验证字节码格式是否符合规范
这个地方要说一点和开发者相关的。.class文件的第5~第8个字节表示的是该.class文件的主次版本号,验证的时候会对这4个字节做一个验证,高版本的JDK能向下兼容以前版本的.class文件,但不能运行以后的class文件,即使文件格式未发生任何变化,虚拟机也必须拒绝执行超过其版本号的.class文件。举个具体的例子,如果一段.java代码是在JDK1.6下编译的,那么JDK1.6、JDK1.7的环境能运行这个.java代码生成的.class文件,但是JDK1.5、JDK1.4乃更低的JDK版本是无法运行这个.java代码生成的.class文件的。如果运行,会抛出java.lang.UnsupportedClassVersionError,这个小细节,务必注意。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求
- 字节码验证:是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
这些变量所使用的内存在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了。
- 这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
- 这个阶段赋初始值的变量指的是那些不被final修饰的static变量,【赋零值】
比如"public static int value = 123;",value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;
比如"public static final int value = 123;"就不一样了,编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置将value赋值为123。
解析
解析阶段因为自己对字节码文件格式还不是非常明白,暂时不太懂,先留着,后面加强了字节码文件格式后,再来补充。
初始化
类加载一直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器
但是这个<clinit>()
还不是很懂。自己需要学习class文件结构。
关于这个<clinit>()
可以研究一下这篇文章。从一道题看类的加载与实例化过程、NoClassDefFoundError异常
后面学习了回来补充这一段。
对于初始化阶段,《Java虚拟机规范8》(下方引用文字是原文)则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
·使用new关键字实例化对象的时候。
·读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
·调用一个类型的静态方法的时候。 - 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
- 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
这六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。
引用官网虚拟机jvms8规范5.5章节Initialization
A class or interface C may be initialized only as a result of:
1 The execution of any one of the Java Virtual Machine instructions new,
getstatic, putstatic, or invokestatic that references C (§new, §getstatic, §putstatic, §invokestatic). These instructions reference a class or interface directly or
indirectly through either a field reference or a method reference.
Upon execution of a new instruction, the referenced class is initialized if it has not been initialized already.
Upon execution of a getstatic, putstatic, or invokestatic instruction, the class or interface that declared the resolved field or method is initialized if it has not been
initialized already.
2 The first invocation of a java.lang.invoke.MethodHandle instance which was the result of method handle resolution (§5.4.3.5) for a method handle of kind 2 (REF_getStatic), 4 (REF_putStatic), 6 (REF_invokeStatic), or 8 (REF_newInvokeSpecial).
This implies that the class of a bootstrap method is initialized when the bootstrap method is invoked for an invokedynamic instruction (§invokedynamic), as part of the continuing resolution of the call site specifier.
3 Invocation of certain reflective methods in the class library (§2.12), for example, in class Class or in package java.lang.reflect.
4 If C is a class, the initialization of one of its subclasses.
5 If C is an interface that declares a non-abstract, non-static method, the initialization of a class that implements C directly or indirectly.
6 If C is a class, its designation as the initial class at Java Virtual Machine startup (§5.2).
下面介绍几个被动引用的例子:
例1
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
run with +XX:+TraceClassLoading输出如下,子类会被加载,但是不会初始化。
...
[Loaded com.jamie.basicstudy.jvm01.NotInitialization from file:/D:/CodeFolder/jamie/basic_study/target/classes/]
...
[Loaded com.jamie.basicstudy.jvm01.SuperClass from file:/D:/CodeFolder/jamie/basic_study/target/classes/]
[Loaded com.jamie.basicstudy.jvm01.SubClass from file:/D:/CodeFolder/jamie/basic_study/target/classes/]
SuperClass init!
123
例2
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] a = new SuperClass[10];
}
}
run with +XX:+TraceClassLoading输出如下,子类不会被加载,父类被会加载,但是父类不会初始化。
...
[Loaded com.jamie.basicstudy.jvm01.NotInitialization from file:/D:/CodeFolder/jamie/basic_study/target/classes/]
...
[Loaded com.jamie.basicstudy.jvm01.SuperClass from file:/D:/CodeFolder/jamie/basic_study/target/classes/]
例3
class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final int value = 123;
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.value);
}
}
run with +XX:+TraceClassLoading输出如下,ConstClass类不会被加载,只会加载主类。
...
[Loaded com.jamie.basicstudy.jvm01.NotInitialization from file:/D:/CodeFolder/jamie/basic_study/target/classes/]
...
123
- 在编译阶段通过常量传播优化,已经将此常量的值“hello world”直接存储在NotInitialization类的常量池中,以后NotInitialization对常量ConstClass.HELLOWORLD的引用,实际都被转化为NotInitialization类对自身常量池的引用了。也就是说,实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译成Class文件后就已不存在任何联系了。
- 接口与类真正有所区别的是前面讲述的六种“有且仅有”需要触发初始化场景中的第三种:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
追加例子
静态成员初始化例1
package com.jamie.jvmstidy;
public class TestPreparationInitialization {
public static void main(String[] args) {
Singleton singleton = Singleton.get();
System.out.println("main方法counter1=" + Singleton.counter1);
System.out.println("main方法counter2=" + Singleton.counter2);
}
}
class Singleton {
public static int counter1 = 0;
public static int counter2 = 0;
private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
System.out.println("构造器counter1=" + counter1);
System.out.println("构造器counter2=" + counter2);
}
public static Singleton get() {
return singleton;
}
}
run with +XX:+TraceClassLoading输出
[Loaded com.jamie.jvmstidy.TestPreparationInitialization from file:/F:/CodeFolder/SELF_STUDY/jvmstudy/target/classes/]
...
[Loaded com.jamie.jvmstidy.Singleton from file:/F:/CodeFolder/SELF_STUDY/jvmstudy/target/classes/]
构造器counter1=1
构造器counter2=1
main方法counter1=1
main方法counter2=1
静态成员初始化例2
package com.jamie.jvmstidy;
public class TestPreparationInitialization {
public static void main(String[] args) {
Singleton singleton = Singleton.get();
System.out.println("main方法counter1=" + Singleton.counter1);
System.out.println("main方法counter2=" + Singleton.counter2);
}
}
class Singleton {
public static int counter1 = 0;
private static Singleton singleton = new Singleton();
public static int counter2 = 0;
private Singleton() {
counter1++;
counter2++;
System.out.println("构造器counter1=" + counter1);
System.out.println("构造器counter2=" + counter2);
}
public static Singleton get() {
return singleton;
}
}
run with +XX:+TraceClassLoading输出
[Loaded com.jamie.jvmstidy.TestPreparationInitialization from file:/F:/CodeFolder/SELF_STUDY/jvmstudy/target/classes/]
...
[Loaded com.jamie.jvmstidy.Singleton from file:/F:/CodeFolder/SELF_STUDY/jvmstudy/target/classes/]
构造器counter1=1
构造器counter2=1
main方法counter1=1
main方法counter2=0
为什么结果不同?
static成员的顺序在初始化过程中是很重要的,当类加载进行到准备阶段时,先执行赋零值操作;然后到了初始化阶段,接受外部赋值行为。
例1中,先为counter2 赋0(外部值),然后调用new Singleton()构造方法,为两个变量加1。最终得到counter1=1,counter2=1
例2中,先为调用new Singleton()构造方法,此时值都自增1,得到2个1。然后为counter2 赋0(外部值),最终得到counter1=1,counter2=0
说明
初始化阶段,每个类中的静态成员的初始化赋值过程是从上往下的。所以静态成员的顺序是很重要的。开发中需注意。
(画外音:这个例子除了加深对初始化阶段的认识,没有其他意义,大家看看就好)
静态成员初始化例3
package com.jamie.jvmstidy;
class Test {
static {
System.out.println("Test initialized");
}
}
public class TestClassLoader {
public static void main(String[] args) throws Exception {
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class<?> aClass = loader.loadClass("com.jamie.jvmstidy.Test");
System.out.println(aClass);
System.out.println("=====================");
Thread.sleep(1000);
aClass = Class.forName("com.jamie.jvmstidy.Test");
System.out.println(aClass);
}
}
输出结果:
class com.jamie.jvmstidy.Test
=====================
(这里睡了1秒)
Test initialized
class com.jamie.jvmstidy.Test
查看Class.forName()源码可以看到内部用了反射包Reflection.getCallerClass();
,JVM规范的使用了反射包,就需要立即初始化。
而使用ClassLoader直接loadClass()
是不会触发初始化的。