JVM 不和包括 Java 在内的任何语言绑定,它只与 "Class文件" 这种特定的二进制文件格式所关联。而 Class 文件也并非只能通过 Java 源文件编译生成,可以通过如下途径而来:
JVM 把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称为虚拟机的「类加载机制」。
即Class 文件中描述的关于类的信息最终要加载到 JVM 中才能被运行和使用。
1.1 类的生命周期
一个类型(类或接口)从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期会经历加载(Loading)、验证(Verification)、准备(Prepare)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析统称为连接(Linking)。如图所示:
1.2 初始化时机
JVM 规范对于“加载”阶段并未强制约束。但对于“初始化”阶段,则规定有且仅有以下六种情况必须立即对其“初始化”:
遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时。场景如下:
使用 new 关键字实例化对象;
读/写静态字段(static 修饰,无 final);
调用静态方法。
使用 java.lang.reflect
的方法对类型进行反射调用时。
初始化类时,若父类尚未初始化,需要先初始化其父类。
虚拟机启动时,需要先初始化用户指定的主类(main 方法所在类)。
使用 JDK 7 新加入的动态语言支持时,若一个 java.lang.invoke.MethodHandle 实例最后解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,且该方法句柄对应的类未初始化,需要先初始化【平时似乎没用到过,暂不深究,以后有机会再分析】。
接口中定义了 JDK 8 加入的默认方法(default 修饰)时,在该接口的实现类初始化之前,需要先初始化这个接口。
注意:当一个“类”在初始化时,要求其父类全都已经初始化;但是,一个“接口”在初始化时,并不要求父接口全都初始化,只有真正使用到父接口时才会初始化(比如引用接口定义的常量)。
1.3 主动引用&被动引用
上述六种情况的行为称为对一个类型的“主动引用”,而除此之外的其他所有引用类型方式都不会触发初始化,称为“被动引用”。被动引用举例如下:
示例代码
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
public static final String HELLO_WORLD = "hello, world";
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
PS: 为了跟踪类加载信息,可配置虚拟机参数
-XX:+TraceClassLoading
。
eg1
/**
* 通过子类引用父类的静态字段,不会导致子类初始化
*/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
/* 类加载情况:SubClass 和 SuperClass 均被加载
*
* 输出结果(父类初始化,子类未初始化):
* SupClass init!
* 123
*/
eg2
/**
* 通过数组定义来引用类,不会触发此类的初始化
*/
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] superClasses = new SuperClass[10];
}
}
/* 类加载情况:SuperClass 被加载
* 输出结果为空,SuperClass 未初始化
*/
eg3
/**
* 常量在【编译阶段】会存入调用类(NotInitialization)的常量池中,
* 本质上并没有直接引用到定义常量的类,因此不会触发其初始化
*/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SuperClass.HELLO_WORLD);
}
}
/* 类加载情况:SubClass 和 SuperClass 均未被加载
*
* 输出结果:
* hello, world
*/
编译阶段通过常量传播优化,已将该常量的值("hello, world")直接存储在 NotInitialization 类的常量池中,以后 NotInitialization 对常量 SuperClass.HELLO_WORLD 的引用实际都被转化为对自身常量池的引用了。
PS: 其实 NotInitialization 类的 Class 文件中并不存在 SuperClass 类的符号引用入口,这两个类在编译成 Class 文件之后就没联系了。
2.1 加载
加载阶段,JVM 主要做了三件事情:
通过一个类的全限定名来获取定义此类的二进制字节流;
将该字节流所代表的静态存储结构转化为方法区的运行时数据结构;
在(堆)内存中生成一个代表该类的 java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
PS: 二进制字节流的来源有很多,例如:从 ZIP 压缩包读取、从网络获取、运行时计算生成(动态代理),从加密文件读取等。
需要注意的是,数组类的加载情况有所不同:数组类本身不通过类加载器创建,而是由 JVM 直接在内存动态构造(newarray 指令)。它的创建过程遵循以下原则:
若数组的组件类型(数组去掉一个维度)为引用类型,则递归加载该组件类型;
若数组的组件类型不是引用类型(例如 int[] 组件类型为 int),JVM 会把数组标记为与引导类加载器关联;
数组类的可访问性与其组件类型的可访问性一致(若组件类型不是引用类型,可访问性默认为 public)。
2.2 验证
主要目的:确保 Class 文件信息符合 JVM 规范,防止恶意代码危害虚拟机自身安全。
有点类似我们平时开发接口时的参数校验,不能因为入参问题把程序搞崩溃了。
该阶段大致会完成下面四个阶段的验证:文件格式验证、元数据验证、字节码验证和符号引用验证。
2.2.1 文件格式验证
验证字节流是否符合 Class 文件格式的规范,且能被当前虚拟机处理。主要验证:
是否以魔数 0xCAFEBABY 开头;
主次版本号是否在当前虚拟机处理范围内;
……
PS: 该阶段是基于二进制字节流进行的,验证通过之后才允许进入 JVM 的方法区。而后面的验证都是基于方法区的存储结构进行的,不再直接读取字节码。
2.2.2 元数据验证
对类的元数据信息进行语义校验,确保不违背 Java 语言规范。比如:
一个类是否有父类;
该父类是否继承了 final 修饰的类;
……
2.2.3 字节码验证
该阶段最复杂,主要是数据流分析和控制流分析,确定语义合法、符合逻辑。验证点如下:
操作数栈的数据类型与指令代码序列能配合工作;
跳转指令不会跳到方法体以外的字节码指令上;
类型转换有效;
……
2.2.4 符号引用验证
发生在虚拟机将符号引用转为直接引用时(即后面的解析阶段),确保解析动作能正常执行。验证点如下:
符号引用中通过字符串描述的全限定名是否能找到对应的类;
符号引用中的类、字段、方法的可访问性验证;
……
验证阶段虽然很重要,却并非必须执行。若程序代码已被反复使用和验证,可以考虑关闭大部分类验证,以缩短类加载的时间。JVM 参数:
-Xverify:none
2.3 准备
主要目的:为类变量(即 static 修饰的静态变量)分配内存并设置初始值。
初始值“通常”情况指的是零值,基本数据类型的零值如下:
// 经过「准备」阶段后,该初始值为 0
// 而把 value 赋值为 123 是在后面的「初始化」阶段
public static int value = 123;
注意,上面的“通常”不包含一种情况,即静态变量被 final 修饰的时候,例如:
public static final int value = 123;
编译阶段会为 value 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 将其设置为 123.
2.4 解析
主要动作:把常量池内的符号引用替换为直接引用。
该阶段主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行。
2.4.1 符号引用
符号引用(Symbolic References):以一组符号描述所引用的目标,可以是任何形式的字面量(比如全限定类名)。引用的目标不一定加载到 JVM 内存中。
代码示例
比如有两个 java 文件,分别为 A.java 和 B.java,如下:
public class A {
}
public class B {
private A a;
}
其中 B 持有对 A 的引用,但此时两个类并未加载到内存中,仅仅是一个标记而已。
2.4.2 直接引用
直接引用(Direct References)可以是:
直接指向目标的指针;
相对偏移量(例如实例变量、实例方法);
能间接定位到目标的句柄。
直接引用就是能够直接在内存中找到相应对象(的内存地址)。若有直接引用,则目标必定已在虚拟机中。
2.5 初始化
初始化阶段就是执行类构造器
由编译器根据源文件中的顺序、自动收集类中的所有静态变量的赋值动作和静态代码块合并产生。
此方法与类的构造方法(虚拟机视角中的实例构造器
PS: 与类不同的是,接口的方法不需要先执行父接口的
() 方法。 接口的实现类在初始化时也不会执行接口的
() 方法。
该方法并不是必需的,若类中无静态语句块和对变量的赋值操作,编译器可以不生成这个方法。
接口中虽然不能使用静态代码块,却可以为变量始化赋值,因此也会生成
() 方法。
JVM 必须保证一个类的
说到这里,设计模式的「单例模式」就有一种写法是利用该机制来保证线程安全性的,示例代码如下:
public class BeanFactory {
private BeanFactory() {
}
public BeanFactory getBeanFactory() {
return BeanFactoryHolder.beanFactory;
}
/**
* 使用内部嵌套类实现单例,利用 JVM 的类加载机制可保证线程安全
*/
private static class BeanFactoryHolder {
private static BeanFactory beanFactory = new BeanFactory();
}
}
所谓类加载器(Class Loader),其实就是一段代码。
这段代码的主要功能就是:通过一个类的全限定名来获取描述类信息的二进制字节流。
3.1 类与类加载器
对于任意一个类,都必须由其「类加载器」和「该类本身」共同确定它在 JVM 中的唯一性。
即,若要比较两个类是否相等,前提是这两个类必须是由同一个类加载器加载(后面代码进行验证)。
PS: 这里的“相等”,包括 equals、isAssignableFrom、isInstance 等方法,还有 instanceof 关键字。
3.2 双亲委派模型
类加载器的分类及其主要特点如下:
启动类加载器(Bootstrap Class Loader)
虚拟机的一部分(C++ 实现);
负责加载 JAVA_HOME\lib 目录,或者 -Xbootclasspath 参数指定路径下,且被 JVM 识别的类库。
扩展类加载器(Extension Class Loader)
由 sun.misc.Launcher$ExtClassLoader 类实现;
负责加载 JAVA_HOME\lib\ext 目录,或者 java.ext.dirs 系统变量指定的路径中的类库。
应用程序类加载器(Application Class Loader)
由 sun.misc.Launcher$AppClassLoader 类实现;
加载用户类路径(ClassPath)下所有的类库;
默认的系统类加载器(若应用程序没有自定义过类加载器,一般使用该类进行加载)。
若有必要,还可以加入自定义的类加载器进行扩展。
JDK 9 之前的 Java 应用都是由这三种类加载器互相配合完成加载的。它们之间的协作关系如图所示:
这种层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。
双亲委派模型的工作流程大致如下:
若一个类加载器收到了加载类的请求,它首先不会自己尝试去加载这个类,而是将其委派给父类加载器,父加载器亦是如此,直至启动类加载器;仅当父加载器无法加载该类的时候,子加载器才会尝试自己进行加载。
注意:这里的它们之间并非「继承」关系,通常是采用「组合」的方式。
3.2.1 实现源码
双亲委派模型的实现代码在 java.lang.ClassLoader 类的 loadClass 方法中,如下:
private final ClassLoader parent;
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先检查请求的类是否已加载过
Class c = findLoadedClass(name);
if (c == null) {
try {
// 若未加载过,调用父类加载器进行加载(父类加载器也会继续该过程)
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 使用启动类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法完成加载
}
// 父类加载器未完成加载时,使用自身的 findClass 方法尝试加载
if (c == null) {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
// JDK 1.2 提供的
protected Class findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
3.2.2 优点
为什么要采用双亲委派模型?这样做有什么好处呢?
一个好处就是:Java 类随着类加载器有了层级关系,把最基础的类,例如 java.lang.Object,交给最顶端的类加载器加载,保证在各个加载器环境中都是同一个 Object 类。
说到这里,有些面试题会问:如果自定义一个 java.lang.Object
类会怎样?
自定义 java.lang.Object 类
这里做下测试,自定义一个 java.lang.Object
类:
package java.lang;
public class Object {
public String toString() {
return "hello";
}
public static void main(String[] args) {
System.out.println("hello");
}
}
如果能正常加载,这里会打印字符串 "hello",结果呢?会报错:
Error: Main method not found in class java.lang.Object, please define the main method as:
public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application
错误原因是 main 方法未找到,就是我们自定义的方法未找到。
查看类加载信息:
[Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar]
...
可以发现,JVM 只加载了 rt.jar 中的 java.lang.Object
,并没有加载我们定义的这个 Object 类,而 rt.jar 中的 Object 是没有 main 方法的。
自定义 java.lang.HelloWorld 类
如果我们定义一个全类名为 java.lang.HelloWorld
的类呢?
package java.lang;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello");
}
}
可以正常加载和运行吗?并不会!
异常如下:
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
...
可以看到,java.lang 这个包名是禁止使用的。
3.3 破坏双亲委派模型
PS: “破坏双亲委派模型“这个概念刚开始听起来可能有些费解,尤其是这个”破坏“,至少我是这样。
其实呢,双亲委派模型可以理解为一个规范,然鹅,某些地方由于某些原因并未遵循这个规范。对于那些没有遵循该规范的地方,就是破坏了双亲委派模型。
总的来说,破坏双亲委派模型的行为大致有三次:
第一次
由于“双亲委派模型”是 JDK 1.2 引入的,但类加载和 java.lang.ClassLoader
类在此之前就已经存在了,为了兼容已有代码,双亲委派模型做了妥协。
由于 ClassLoader 类的 loadClass 方法可以直接被子类重写,这样的类加载机制就不符合双亲委派模型了。
如何实现兼容呢?在 ClassLoader 类添加了 findClass 方法(代码见 3.2.1),并引导用户重写该方法,而非 loadClass 方法。
这就是第一次破坏双亲委派模型,其实就是兼容历史遗留问题。
第二次
双亲委派模型的类加载都是自底向上的(越基础的类由越上层的加载器来加载),但有些场景可能会出现基础类型要反回来调用用户代码,这个场景如何解决呢?
一个典型的例子就是 JNDI (启动类加载器加载)服务,其目的是调用其它厂商实现并部署在应用程序 ClassPath 下的服务提供者接口(Service Provider Interface,SPI)。启动类加载器是不认识这些 SPI 的,如何解决呢?
Java 团队引入了一个线程上下文类加载器(Thread Context ClassLoader),可以设置类加载器,在启动类加载器不认识的地方,调用其它类加载器去加载。这其实也打破了双亲委派模型。
比如 JDBC 的类加载机制,后文再详细分析。
第三次
第三次破坏是对程序动态性的追求导致的,代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。典型的如 IBM 的 OSGi 模块化热部署。
4.1 自定义类加载器
public class MyClassLoader extends ClassLoader {
// 重写 findClass 方法
@Override
protected Class findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
// 读取 class 文件
private byte[] loadClassData(String className) {
String fileName = "~/Code/Java/test/target/classes" +
File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try {
FileInputStream inputStream = new FileInputStream(fileName);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
return outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
4.2 双亲委派模型类加载
自定义一个 Person 类
package loader;
public class Person {
static {
// 当 Person 类初始化时,会打印该代码
System.out.println("Person init!");
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
使用上面自定义的类加载加载 Person 类:
private static void test1() throws Exception {
// 创建类加载器实例
MyClassLoader myClassLoader1 = new MyClassLoader();
// 加载 Person 类(注意这里是 loadClass 方法)
Class aClass1 = myClassLoader1.loadClass("loader.Person");
aClass1.newInstance(); // Person init!
MyClassLoader myClassLoader2 = new MyClassLoader();
Class aClass2 = myClassLoader2.loadClass("loader.Person");
aClass2.newInstance();
System.out.println("--->" + aClass1.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println("--->" + aClass2.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println("--->" + aClass1.equals(aClass2)); // true
}
可以看到,这里虽然使用了两个类加载器实例加载 Person 类,但实际上 aClass1 和 aClass2 的类加载器并不是自定义的 MyClassLoader,而是 Launcher$AppClassLoader,即应用类加载器。为什么会是这个结果呢?
其实这就是前面分析的双亲委派模型,示意图如下:
大体流程分析:
使用 MyClassLoader 加载 Person 类时,它会先委托给 AppClassLoader;
AppClassLoader 委托给 ExtClassLoader;
ExtClassLoader 委托给启动类加载器;
但是,启动类加载器并不认识 Person 类,无法加载,于是就再反回来交给 ExtClassLoader;
ExtClassLoader 也无法加载,于是交给了 AppClassLoader;
AppClassLoader 可以加载 Person 类,加载结束。
4.2 非双亲委派模型类加载
上面演示了双亲委派模型加载一个类,如何破坏双亲委派模型呢?把上面的 loadClass 方法换成 findClass 就行,示例代码:
测试类加载 eg.1
private static void test2() throws Exception {
MyClassLoader cl1 = new MyClassLoader();
// 加载自定义的 Person 类
Class aClass1 = cl1.findClass("loader.Person");
// 实例化 Person 对象
aClass1.newInstance(); // Person init!
MyClassLoader cl2 = new MyClassLoader();
Class aClass2 = cl2.findClass("loader.Person");
aClass2.newInstance(); // Person init!
System.out.println("--->" + aClass1); // class loader.Person
System.out.println("--->" + aClass2); // class loader.Person
System.out.println("--->" + aClass1.getClassLoader()); // loader.MyClassLoader@60e53b93
System.out.println("--->" + aClass2.getClassLoader()); // loader.MyClassLoader@1d44bcfa
System.out.println("--->" + aClass1.equals(aClass2)); // false
}
这里创建了两个自定类加载器 MyClassLoader 的实例,分别用它们来加载 Person 类。
虽然两个打印结果都是 class loader.Person
,但类加载器不同,导致 equals 方法的结果是 false,原因就是二者使用了不同的类加载器。
根据 MyClassLoader 的代码,这里实际并未按照双亲委派模型的层级结构去加载 Person 类,而是直接使用了 MyClassLoader 来加载的。
测试类加载 eg.2
上述代码中,如果使用同一个类加载器进行加载呢?修改代码如下:
private static void test3() throws Exception {
MyClassLoader cl1 = new MyClassLoader();
Class aClass1 = cl1.findClass("loader.Person");
aClass1.newInstance();
// 这里改用上面的类加载进行加载呢?
Class aClass2 = cl1.findClass("loader.Person");
aClass2.newInstance();
System.out.println("--->" + aClass1);
System.out.println("--->" + aClass2);
System.out.println("--->" + aClass1.equals(aClass2)); // true ??
}
这样的比较结果会是 true 吗?似乎应该是的吧。。
然而,这样会报错的:
Exception in thread "main" java.lang.LinkageError: loader (instance of loader/MyClassLoader): attempted duplicate class definition for name: "loader/Person"
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at loader.MyClassLoader.findClass(MyClassLoader.java:21)
at loader.TestClassLoader.test1(TestClassLoader.java:61)
at loader.TestClassLoader.main(TestClassLoader.java:10)
原因是:一个类加载器不能多次加载同一个类。
本文内容就到这里,希望对大家有所帮助~