JVM(一):Java类加载原理深度解析
什么是 JVM
我们知道 Java 中我们所有的代码执行都依赖于 JVM(Java Virtual Machine),也就是 Java 虚拟机。它屏蔽了与操作系统平台相关的信息, 使得 Java 程序只需要生成在 JVM 运行的目标代码(字节码), 就可在多种平台上不加修改的运行,这也是 Java 能够"一次编译,到处运行的"原因。
JRE JDK 和 JVM 的关系
JRE(Java Runtime Environment,Java 运行环境),所有的 Java 程序都必须在 JRE 下才能运行,它包括 JVM 和 Java 核心类库和支持文件。
JDK(Java Development Kit,Java 开发工具包),所有的 Java 程序员都会使用的工具包,主要用来编译、调试 Java 代码,它包括如javac
、java
等和 Java 的基础类库如:System、Runtime 等。
JVM(Java Virtual Machine,Java 虚拟机),JVM 是 JRE 的一部分,主要工作是解释自己的指令集(即字节码)并映射到电脑硬件的 CPU 指令集和 OS(Operating System)的系统调用。
总结来说,我们使用JDK
开发 Java 程序,通过 JDK 编译(javac
)将 Java 程序编译为 Java 字节码,在JRE
上运行这些字节码,JVM
解析这些字节码并映射到电脑硬件的 CPU 指令集和 OS 的系统调用。
JVM 的原理
JVM 是 Java 的核心和基础,在 Java 编译器和 OS 平台之间的虚拟处理器,是整个 Java 平台的基石。它是一种利用软件方法实现的抽象的基于下层的操作系统和硬件平台,是 Java 技术实现硬件无关和操作系统无关的关键环节,可以在上面执行 Java 的字节码程序。
JVM 的生命周期
诞生
当启动一个 Java 程序时,一个 JVM 实例就产生了,任何一个拥有public static void main(String[] args)
函数的 class 都可以作为 JVM 实例运行的起点。
运行
main()
作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM 内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由 JVM 自己使用,java 程序也可以标明自己创建的线程是守护线程。
销毁
当程序中的所有非守护线程都终止时,JVM 才退出。若安全管理器允许,程序也可以使用java.lang.Runtime
类或者java.lang.System.exit()
来退出。
JVM 类加载初始化以及加载类的过程:
Java 类加载机制
首先我们看下面的代码:
package com.ninglz.jvm;
public class Demo {
public int a = 1;
public static User user = new User();
public int add(){
int b = 3;
return (a+b)/2;
}
public static void main(String[] args) {
Demo demo = new Demo();
System.out.println(demo.add());
}
}
当我们的 Java 代码编译完成后,会生成对应的 class 文件。接着我们运行 java Demo 命令的时候,我们其实是启动了 JVM 虚拟机执行 class 字节码文件的内容。而 JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载[^1]。其中验证、准备、解析又统称连接阶段。
加载
加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。
例如调用类的 main()
方法,new
对象等等,在加载阶段会在内存中生成一个代表这个类的 java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
简单来说:加载阶段就是在硬盘上找到文件并通过 IO 读入字节码文件并存入到内存中去。
验证
当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。这个校验过程大致可以分为下面几个类型:
-
JVM 规范校验:
JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。
例如: 文件是否是以cafe babe
开头,主次版本号是否在当前虚拟机处理范围之内。
-
代码逻辑校验
JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。
例如: 一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。一个方法要求返回 String 类型的结果,但是最后却没有返回结果。代码中引用了一个名为 User 的类,但是你实际上却没有定义 User 类等。
简单来说:验证阶段当代码数据被加载到内存中后,虚拟机就会对代码数据进行校验,看看这份代码是不是真的按照 JVM 规范去写的。
准备
当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。
-
内存分配的对象
Java 中的变量有
类变量
和类成员变量
两种类型,类变量指的是被static
修饰的变量,而其他所有类型的变量都属于类成员变量。在准备阶段,JVM 只会为类变量
分配内存,而不会为类成员变量
分配内存。类成员变量
的内存分配需要等到初始化阶段才开始。
例如下面的代码在准备阶段,只会为 class_variable 属性分配内存,而不会为 member_variables 属性分配内存。
//类变量 分配内存
public static int class_variable = 123;
//成员变量 不分配内存,等待初始化阶段开始分配
public String member_variables = "default value";
-
初始化的类型
在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
例如下面的代码在准备阶段之后,class_variable 的值将是 0,而不是 123。
//类变量 分配内存 此时class_variable是0
public static int class_variable = 123;
但如果一个变量是常量(被static final
修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。
例如下面的代码在准备阶段之后,num 的值将是 666,而不是 0。
//变量 在准备阶段直接被赋值为666 不会初始为0
public static final int num = 666;
final 关键字在 Java 中代表不可被改变,所有在一开始就会在准备阶段赋予对应的值。没有 fianl 修饰的类变量在初始化阶段或者运行阶段都有可能发生变化,所以没必要在准备阶段赋予用户想要的值。
简单来说:准备阶段就是给类的静态变量分配内存和赋默认值,给常量直接赋对应值。
解析
当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。
该阶段会把一些静态方法(比如 main() ,符号引用)替换为指向数据所存内存的指针或句柄等(直接引用),这个是多为的静态链接
过程(类加载期间完成)。动态链接
是在程序运行期间完成的将符号引用替换为直接引用。
简单来说:将其在常量池中的符号引用替换成直接其在内存中的直接引用。
初始化
初始化阶段是类的加载过程的最后一个阶段,该阶段主要做一件事情就是执行< clinit>(),该方法会为所有的静态变量赋予正确的值。到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:
-
遇到
new
、getstatic(读取)
、putstatic(修改)
、invokestatic(调用)
这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的 Java 代码场景是:
使用 new 关键字实例化对象; 读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外); 调用一个类的静态方法;
-
使用 java.lang.reflect
包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。 -
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。( 所以 extends
某个类时,会先去找其父类确定是否已被初始化,如果父类还没有被初始化则会优先初始化父类 ) -
当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的那个类),虚拟机会先初始化这个主类。 -
当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle
实例最后的解析结果REF_getstatic
,REF_putstatic
,REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化( 就是调用一个类型的静态方法时)。
简单来说:初始化阶段就是对类的静态变量初始化为指定的值,执行静态代码块。
使用
当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。
卸载
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存,当使用完成之后,还会在方法区垃圾回收的过程中进行卸载。
加载器的种类
在 Java 中有如下几种类加载器:
引导类加载器(BootstrapClassLoader): 负责加载支撑 JVM 运行的位于 JRE 的 lib 目录下的核心类库,比如 rt.jar、charsets.jar 等。
扩展类加载器(ExtClassLoader): 负责加载支撑 JVM 运行的位于 JRE 的 lib 目录下的 ext 扩展目录中的 JAR 类包。
应用程序类加载器(AppClassLoader): 负责加载ClassPath
路径下的类包,主要就是加载你自己写的那些类。
自定义类加载器: 负责加载用户自定义路径下的类包,使用者自己定义的,一般继承java.lang.ClassLoader
的类。
URL 加载器(URLClassLoader): URLClassLoader 继承自 SecureClassLoader,JDK8 中用来加载${JAVA_HOME}/lib/ext
目录下的类,加载时首先去 classload 里判断是否由 BootClassLoader 加载过。
安全管理加载器(SecureClassLoader): 此类扩展了ClassLoader
,并提供了额外的支持,用于定义具有相关代码源和权限的类,这些类由默认情况下由系统策略检索。
ClassLoader: ClassLoader 是类加载器的顶级父类。
我们需要关注:引导类加载器、扩展类加载器、应用程序类加载器、自定义类加载器
双亲委派机制(父类委派模型)(重要)
双亲委派还是父类委派?
在国内我们把加载器的原理称呼为"双亲委派机制",但是当我理解它们的原理后,我感觉"双亲委派机制"有点名不符实,多少有点误导大家的含义。
我们先看下 ClassLoader 类的官方解释(截取片段):
The ClassLoader class uses a delegation model to search for classes and resources。Each instance of ClassLoader has an associated parent class loader。When requested to find a class or resource,a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself。The virtual machine's built-in class loader,called the "bootstrap class loader",does not itself have a parent but may serve as the parent of a ClassLoader instance。
ClassLoader 类使用委托模型搜索类和资源。ClassLoader 的每个实例都有一个关联的父类装入器。当被请求查找类或资源时,ClassLoader 实例将在试图查找类或资源本身之前,将对类或资源的搜索委托给其父类装入器。虚拟机的内置类装入器称为“引导类装入器” ,它本身没有父类,但可以作为 ClassLoader 实例的父类。
所以从我的理解来说:Java 的类加载机制应该叫做"父类委派模型",而不应该称为"双亲委派机制"。
源码分析
看一个类加载器示例:
package com.ninglz.jvm;
public class TestJDKClassLoader {
public static void main(String[] args) {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
//getSystemClassLoader就是获取系统中的类加载器也就是AppClassLoader
System.out.println(systemClassLoader);
Class<TestJDKClassLoader> loaderClass = TestJDKClassLoader.class;
//jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
System.out.println(loaderClass.getClassLoader());
// jdk.internal.loader.ClassLoaders$PlatformClassLoader@58ceff1
System.out.println(loaderClass.getClassLoader().getParent());
// null 引导类加载器,由c++实现,所以去不到信息
System.out.println(loaderClass.getClassLoader().getParent().getParent());
}
}
查看 JDK1.8 源码了解到:
-
JVM 在创建会启动器实例
sun.misc.Launcher
。 -
sun.misc.Launcher
初始化使用单例,保证 JVM 虚拟机只有一个 Launcher 实例。 -
在 Launcher 的构造器内部创建了两个类加载器,分别是
sun.misc.Launcher.ExtClassLoader
(扩展类加载器)和sun.misc.Launcher.AppClassLoader
(应用类加载器)。 -
JVM 默认使用
Launcher
的getClassLoader()
方法返回的类加载器 AppClassLoader 的实例加载。
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//创建ExtClassLoader 扩展类加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader",var10);
}
try {
//创建AppClassLoader 应用类加载器
//在构造的过程中将其父加载器设置为var1 === ExtClassLoader。
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader",var9);
}
//其他不需要关注的代码
//do something。。。。
}
}
AppClassLoader 类的 loadClass
方法是加载类的核心代码模块:
// AppClassLoader 的 loadClass 方法
public Class<?> loadClass(String var1,boolean var2) throws ClassNotFoundException {
//其他可忽略代码
//do something 。。。
//判断是否已加载过,如果加载直接读取
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
// 如果没有加载过,交给super === ClassLoad类加载
} else {
return super.loadClass(var1,var2);
}
}
AppClassLoader 中调用 loadClass
最终会执行到 super.loadClass()
。而 super 则是调用 ClassLoader 类的loadClass()
, ClassLoader 的 loadClass()
是整个 双亲委派/父类委派模型的关键。
ClassLoad 类 的核心模块代码:
protected Class<?> loadClass(String name,boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First,check if the class has already been loaded
// 找到当前类属于哪个模块
Class<?> c = findLoadedClass(name);
//如果该类没有被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//如果父类有加载器
if (parent != null) {
//继续执行父类加载器,再次执行ClassLoad。loadClass()
c = parent.loadClass(name,false);
} else {
//如果没有父类加载器,代表当前是引导类加载器执行C++代码
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//父类加载器没有加载成功
if (c == null) {
// If still not found,then invoke findClass in order
// to find the class。
long t1 = System.nanoTime();
//父类加载器没有找到模块,则调用自身的findClass进行加载
//findClass 实际会去执行URLClassLoader的findClass方法
//也有可能会继续返回null 比如假设当前是ExtClassLoader的调用 模块在AppClassLoader
//ExtClassLoade 的 parent 获取模块失败,进入当先调用 findClass调用自身仍然会失败,最后返回null ,此时 AppClassLoader 类的 c = parent.loadClass(name,false);这一行代码会返回null 后 AppClassLoader 会继续调用自身,这个时候会找到这个模块。
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime()。addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
URLClassLoader 类的 findClass
方法
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.','/').concat(".class");
Resource res = ucp.getResource(path,false);
if (res != null) {
try {
//这儿是在类的加载,验证,准备,解析,初始化
return defineClass(name,res);
} catch (IOException e) {
throw new ClassNotFoundException(name,e);
}
} else {
return null;
}
}
},acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
我们来看下应用程序类加载器 AppClassLoader 加载类的双亲委派/父类委派模型源码,AppClassLoader 的loadClass
方法最终会调用其父类 ClassLoader 的loadClass
方法,该方法的大体逻辑如下:
-
首先检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
-
如果此类没有加载过,那么再判断一下是否有父加载器; 如果有父加载器,则由父加载器加载(即调用
parent.loadClass(name,false)
。或者是调用BootstrapClassLoad
引导类加载器来加载。 -
如果父加载器及
BootstrapClassLoad
引导类加载器都没有找到指定的类,那么调用当前类加载器的 findClass 方法来完成类加载。
最后我们可以总结为一句话:任意一个 ClassLoader 在尝试加载一个类的时候,都会先尝试调用其父类的相关方法去加载类,如果其父类不能加载该类,则交由子类去完成。
为什么要设计双亲委派机制?
沙箱安全机制: 自己写的java.lang.String.class
类不会被加载,这样便可以防止核心 API 库被随意篡改。
避免类的重复加载: 当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一 次,保证被加载类的唯一性。
看一个类加载示例:
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("这是一个自定义String类");
}
}
错误: 在类 java.lang..String 中找不到 main 方法,请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application。Application
这是因为,我们再执行我们自定义的 String 时,因为双亲委派/父类委派模型向上委派时,BootstrapClassLoader
引导类加载器会找到 JDK 的 java.lang.String
类,而在 JDK 的 java.lang.String
类中没有main
函数。
综上所述双亲委派/父类委派模型的好处:对于任意使用者自定义的 ClassLoader,都会先去尝试让 JVM 的 BootClassLoader 去尝试加载(自定义的 ClassLoader 都继承了它们)。那么就能保证 JVM 的类会被优先加载,同时限制了使用者对 JVM 系统的影响。
全盘委托机制
全盘委托机制是指当一个 ClassLoder 装载一个类时,除非显示的使用另外一个 ClassLoder,该类所依赖及引用的类也由这个 ClassLoder 载入。
自定义加载器
对于上诉我们创建的 String 类,如何可以让它可以正常运行呢? 我们需要创建自定义加载器。
自定义类加载器只需要继承 java.lang.ClassLoader
类,该类有两个核心方法,一个是 loadClass(String,boolean)
,实现了双亲委派/父类委派模型,还有一个方法是findClass
,默认实现是空方法,所以我们自定义类加载器主要是重写 findClass 方法。
我们先定义一个自定义类加载器,加载非 JDK 里的类:
package com.ninglz.jvm;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* 自定义类加载器
* @author ninglz
*/
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this。classPath = classPath;
}
/**
* 重写findClass方法实现自定义类加载器
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//根据路径读取为二进制流
byte[] data = getClassBytes(name);
if(data==null){
throw new ClassNotFoundException();
}
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节 数组。
return defineClass(name,data,0,data.length);
}
/**
* 根据url读取二进制流
* @param name
* @return
* @throws IOException
*/
private byte[] getClassBytes(String name) {
name = name.replaceAll("\\.","/");
FileInputStream stream = null;
try {
stream = new FileInputStream(classPath + "/" + name + "。class");
int available = stream.available();
byte[] bytes = new byte[available];
stream.read(bytes);
stream.close();
return bytes;
} catch (IOException e) {
e。printStackTrace();
}
return null;
}
public static void main(String[] args) throws ClassNotFoundException,
IllegalAccessException,InstantiationException,
NoSuchMethodException,InvocationTargetException {
//D盘创建 test/com/ninglz/jvm 目录,将Demo类的编译文件Demo.class丢入该目录,
//重命名项目里的Demo为Demo1.java
CustomClassLoader customClassLoader = new CustomClassLoader("d:/test");
Class<?> aClass = customClassLoader.loadClass("com.ninglz.jvm.Demo");
Object instance = aClass.newInstance();
Method main = aClass.getDeclaredMethod("main",String[].class);
Object[] arguments = new Object[]{args};
Object invoke = main.invoke(instance,arguments);
System.out.println(aClass.getClassLoader().getClass().getName());
}
}
得到如下结果:
2
com.ninglz.jvm.CustomClassLoader@1b6d3586
Process finished with exit code 0
上面的com.ninglz.jvm.CustomClassLoader@1b6d3586
代表我们正在使用一个 CustomClassLoader 的加载器,也就说明自定义加载器创建成功。
那么我们加载java.lang.String
结果会怎么样,修改代码为如下:
public static void main(String[] args) throws ClassNotFoundException,
IllegalAccessException,InstantiationException,
NoSuchMethodException,InvocationTargetException {
//D盘创建 test/java/lang 目录,将String类的编译文件String.class丢入该目录,
CustomClassLoader customClassLoader = new CustomClassLoader("d:/test");
Object[] arguments = new Object[]{args};
Class<?> bClass = customClassLoader.loadClass("java.lang.String");
Object instanceb = bClass.newInstance();
Method mainb = bClass.getDeclaredMethod("main",String[].class);
mainb。invoke(instanceb,arguments);
System.out.println(bClass.getClassLoader());
}
执行报错:
Exception in thread "main" java.lang.NoSuchMethodException: java.lang.String.main([Ljava。lang。String;)
at java.lang.Class.getDeclaredMethod(Class.java:2130)
at com.ninglz.jvm.CustomClassLoader。main(CustomClassLoader.java:67)
Process finished with exit code 1
这是因为我们只是重写了 findClass 方法,让我们再梳理下类加载原理:
我们自定义类加载器后,会调用
ClassLoader
类中的loadClass
方法在loadClass
方法中会委派父类parent
加载器加载,如果到BootStrap ClassLoader
引导加载器都没有找到的情况下会执行finClass
方法再去查找。如果还是没找到就交给子类的类加载器调用findClass
去加载。在我们执行
Demo。class
的时候执行到BootStrap ClassLoader
(没找到文件)->再调用findClass
方法,因为我们重写了findClass
方法,所以会直接调用我们自己写的方法去 D 盘查找,最终执行成功。而执行重写的
java。lang。String
方法的时候我们执行到BootStrap ClassLoader
(找到 JDK 中的文件),直接执行了 JDK 中的 String 类,而类中没有 main 函数,最终报错。
要解决这个问题我们需要重写loadClass
方法去打破双亲委派/父类委派模型。
打破双亲委派/父类委派模型
重写loadClass
方法,代码如下:
package com.ninglz.jvm;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* 自定义类加载器
* @author ninglz
*/
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
/**
* 重写findClass方法实现自定义类加载器
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//根据路径读取为二进制流
byte[] data = getClassBytes(name);
if(data==null){
throw new ClassNotFoundException();
}
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节 数组。
return defineClass(name, data, 0, data.length);
}
/**
* 根据url读取二进制流
* @param name
* @return
* @throws IOException
*/
private byte[] getClassBytes(String name) {
name = name.replaceAll("\\.","/");
FileInputStream stream = null;
try {
stream = new FileInputStream(classPath + "/" + name + ".class");
int available = stream.available();
byte[] bytes = new byte[available];
stream.read(bytes);
stream.close();
return bytes;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 重写loadClass去打破双亲委派/父类委派模型
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
//如果前缀是com.ninglz开头的则调用我们自己的加载器
//不做这个判断会出现一个问题: 因为类中一定包含了一些JDK中的类,
//而JDK的类如:Object String 等在我们自定义的加载器中将加载不到.
if (name.startsWith("com.ninglz") ||
//专门测试自定义的String类,正常情况下需要删除这个判断,否则会异常,原因如上
name.startsWith("java.lang.String")) {
c = findClass(name);
//前缀不是com.ninglz开头的 则继续走 双亲委派/父类委派 模型.
} else {
c = this.getParent().loadClass(name);
}
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//D盘创建 test/java/lang 目录,将String类的编译文件String.class丢入该目录,
CustomClassLoader customClassLoader = new CustomClassLoader("D:\\test");
Object[] arguments = new Object[]{args};
Class<?> bClass = customClassLoader.loadClass("java.lang.String");
Object instanceb = bClass.newInstance();
Method mainb = bClass.getDeclaredMethod("main", String[].class);
mainb.invoke(instanceb, arguments);
System.out.println(bClass.getClassLoader());
}
}
返回如下结果:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655)
at java.lang.ClassLoader.defineClass(ClassLoader.java:754)
at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
at com.ninglz.jvm.CustomClassLoader.findClass(CustomClassLoader.java:30)
at com.ninglz.jvm.CustomClassLoader.loadClass(CustomClassLoader.java:76)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at com.ninglz.jvm.CustomClassLoader.main(CustomClassLoader.java:116)
Disconnected from the target VM, address: '127.0.0.1:59576', transport: 'socket'
Process finished with exit code 1
报 SecurityException 异常。这是因为 JVM 的安全机制导致,在 JVM 中所有java.*
的类包是保留类包,只能有BootstrapClassLoader
引导类加载器加载。
给大家留下一个面试题:
我们可以自定义 String 类吗?如果加载了,会出现什么样的结果呢?异常?那是什么样的异常。如果包名不相同呢?自定义类加载器是否可以加载呢?
我们再切换为加载Demo.class
,修改
if (name.startsWith("com.ninglz") ||
//专门测试自定义的String类,正常情况下需要删除这个判断,否则会异常,原因如上
name.startsWith("java.lang.String"))
删除
|| name.startsWith("java.lang.String")
改为
if (name.startsWith("com.ninglz") )
main 函数改为:
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//D盘创建 test/com/ninglz/jvm 目录,将Demo类的编译文件Demo.class丢入该目录,
//重命名项目里的Demo为Demo1.java
CustomClassLoader customClassLoader = new CustomClassLoader("d:/test");
Class<?> aClass = customClassLoader.loadClass("com.ninglz.jvm.Demo");
Object instance = aClass.newInstance();
Method main = aClass.getDeclaredMethod("main", String[].class);
Object[] arguments = new Object[]{args};
main.invoke(instance, arguments);
System.out.println(aClass.getClassLoader());
}
返回
2
com.ninglz.jvm.CustomClassLoader@1b6d3586
打破双亲委派成功。
Tomcat 的共存隔离[^2]
Tomcat 如果使用默认的双亲委派类加载机制行不行?我们需要思考如下几个问题 1。一个 web 容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
2。部署在同一个 web 容器中相同的类库相同的版本可以共享。否则,如果服务器有 10 个应用程 序,那么要有 10 份相同的类库加载进虚拟机。
3。web 容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的 类库和程序的类库隔离开来。
4。web 容器要支持 jsp 的修改,我们知道,jsp 文件最终也是要编译成 class 文件才能在虚拟机中 运行,但程序运行后修改 jsp 已经是司空见惯的事情, web 容器需要支持 jsp 修改后不用重启
那么,Tomcat 如果使用默认的双亲委派类加载机制行不行?答案是不行的。为什么?
第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认 的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第三个问题和第一个问题一样。
我们再看第四个问题,我们想我们要怎么实现 jsp 文件的热加载,jsp 文件其实也就是 class 文 件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的 jsp 是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这 jsp 文件的类加载器,所以你应该想 到了,每个 jsp 文件对应一个唯一的类加载器,当一个 jsp 文件修改了,就直接卸载这个 jsp 类加载 器。重新创建类加载器,重新加载 jsp 文件。
Tomcat 的几个主要类加载器:
CommonClassLoader:Tomcat 最基本的类加载器,加载路径中的 class 可以被 Tomcat 容器本身以及各个 Webapp 访问;
CatalinaClassLoader:Tomcat 容器私有的类加载器,加载路径中的 class 对于 Webapp 不可见;
SharedClassLoader:各个 Webapp 共享的类加载器,加载路径中的 class 对于所有 Webapp 可见,但是对于 Tomcat 容器不可见;
WebappClassLoader:各个 Webapp 私有的类加载器,加载路径中的 class 只对当前 Webapp 可见,比如加载 war 包里相关的类,每个 war 包应用都有自己的 WebappClassLoader,实现相互隔离,比如不同 war 包应用引入了不同的 spring 版本, 这样实现就能加载各自的 spring 版本;
从图中的委派关系中可以看出:
CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,从而实现了公有类库的共用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。
WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。
而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个.Class 文件,它出现的目的就是为了被丢弃:当 Web 容器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。
至此,我们已经知道了 Tomcat 为什么要这么设计,以及是如何设计的
Tomcat 这种类加载机制违背了 java 推荐的双亲委派/父类委派模型了吗?
答案是违背了,因为**双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。**Tomcat 显然没有这样做,每个 webappClassLoader 加载自己的目录下的 class 文件,不会传递给父类加载器。
但也不尽然就像我们自定义类加载器一样,Tomcat 中核心的加载还是遵从双亲委派/父类委派。
JDK9 之后类加载器的变化[^2]
JDK9 以及后续版本为了模块化的支持,对双亲委派模式做了一些改动:
扩展类加载器被平台类加载器(PlatformClassLoader
)取代。
JDK9 时基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数十个 JMOD 文件),其中的 Java 类库就已天然地满足了可扩展的需求,那自然无须再保留 <JAVA_HOME>\lib\ext
目录,此前使用这个目录或者 java.ext.dirs
系统变量来扩展 JDK 功能的机制已经没有继续存在的价值了。
平台类加载器和应用程序类加载器都不再继承自 java.net.URLClassLoader
。现在启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader
。
如果有程序直接依赖了这种继承关系,或者依赖了 URLClassLoader
类的特定方法,那代码很可能会在 JDK9 及更高版本的 JDK 中崩溃。
启动类加载器现在是在 Java 虚拟机内部和 Java 类库共同协作实现的类加载器(以前是 C++实现)。为了与之前的代码保持兼容,所有在获取启动类加载器的场景(譬如 Object.class.getClassLoader)中仍然会返回 null 来代替,而不会得到 BootClassLoader 的实例。
类加载的委派关系也发生了变动。当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。
目前 JDK 已经升级到了 14 其中对比 JDK1.8,Java 已经发生了很多变化,有兴趣的效果请自行在官网查看[3]。
另外强烈推荐大家去看《深入理解 Java 虚拟机》[4]这本书,本人在看第二遍的时候配合学习诸葛老师的视频受益匪浅。
参考
[1]两道面试题,带你解析 Java 类加载机制[5]
[2]《深入理解 Java 虚拟机(第 2 版)》周志明 著[6]
参考资料
Oracle Docs: https://docs.oracle.com/javase/8/docs/
[2]心源意码: https://zhuanlan.zhihu.com/p/120336342
[3]官网查看: https://www.oracle.com/technetwork/java/javase/overview/index.html
[4]《深入理解 Java 虚拟机》: https://book.douban.com/subject/24722612/
[5]两道面试题,带你解析 Java 类加载机制: https://www.cnblogs.com/chanshuyi/p/the_java_class_load_mechamism.html
[6]《深入理解 Java 虚拟机(第 2 版)》周志明 著: https://book.douban.com/subject/24722612/