JVM类加载机制
我这里有个Math类,代码如下:该类运行main方法,到方法结束,jvm是底层是如下图所示执行的:其中jvm加载类的过程会经历以下几个步骤:类被加载到方法区之后主要包含运行时常量池、类型信息、字段信息、方法信息、类加载的引用、对应class实例的引用等信息。上述代码中调用了A,但是没调用B,所以只会加载Class A 而不会加载Class B上述代码中调用了A,调用了B,所以逐步加载Class A
类加载运行过程
我这里有个Math类,代码如下:
package com.guaiyu.jvm;
public class Math {
public static final int INIT_DATA = 666;
public static User user = new User();
public int compute(){
int a = 1;
int b = 2;
int c = (a + b) * 10;
System.out.println(c);
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
该类运行main方法,到方法结束,jvm是底层是如下图所示执行的:
其中jvm加载类的过程会经历以下几个步骤:
- 加载: 在硬盘上查找并通过IO读入字节码文件(所以编译后的文件是.class文件,写代码的文件是.java文件),使用到类的时候才会加载,例如调用main()方法,new对象等等,在加载阶段会在内存中生成一个代表类的java.long.Class对象,最为方法区这个累的各种数据访问入口
- 验证::校验字节码**.class**文件的正确性,例如字节码文件是十六进制数组文件组成,所有字节码文件都是 cafe babe开头,字节码文件部分内容如下图所示:

- 准备: 给静态变量分配内存,并赋予默认值,因为静态变量 是随着类加载的时候就被加载的,所以要提前为期分配内存,并赋予默认值,eg:Math类中的静态变量是user,相应的要为iuser赋值默认值null
此处解释一下:被static修饰的是静态变量,代码中int被static final修饰,为常量,常量在准备阶段要赋予实际值,及代码中int赋值为666 - 解析: 将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用
- 初始化: 对类的静态变量初始化为指定的值,执行静态代码块
类被加载到方法区之后主要包含运行时常量池、类型信息、字段信息、方法信息、类加载的引用、对应class实例的引用等信息。
- 类加载器的引用: 这个类到类加载器实例的引用。
- 对饮class实例的引用: 类加载器在加载类信息放到方法区后,会创建一个对应class类型的对象放到 堆(Heap) 中,作为开发人员访问方法区中类定义的入口和切入点。
注意: 主类在运行过程中加载到其他类,才会逐步去加载这些类,也就是说jar包或war包不是一次性加载的,是使用到时才会加载,也就是懒加载 。
以下代码是对类懒加载的代码演示:
package com.guaiyu.jvm;
public class LazyLoadTest {
static {
System.out.println("************ 加载主类静态代码块 ***************");
}
public static void main(String[] args) {
new A();
System.out.println("************* lazy load test *******************");
B b = null;
}
}
class A {
static {
System.out.println("*************** 加载class A的静态代码块 ***************");
}
public A() {
System.out.println("*************** initial A ******************");
}
}
class B {
static {
System.out.println("*************** 加载class B的静态代码块 ***************");
}
public B() {
System.out.println("*************** initial B ******************");
}
}
运行结果:
************ 加载主类静态代码块 ***************
*************** 加载class A的静态代码块 ***************
*************** initial A ******************
************* lazy load test *******************
上述代码中调用了A,但是没调用B,所以只会加载Class A 而不会加载Class B
package com.guaiyu.jvm;
public class LazyLoadTest {
static {
System.out.println("************ 加载主类静态代码块 ***************");
}
public static void main(String[] args) {
new A();
System.out.println("************* lazy load test *******************");
B b = new B();
}
}
class A {
static {
System.out.println("*************** 加载class A的静态代码块 ***************");
}
public A() {
System.out.println("*************** initial A ******************");
}
}
class B {
static {
System.out.println("*************** 加载class B的静态代码块 ***************");
}
public B() {
System.out.println("*************** initial B ******************");
}
}
运行结果:
************ 加载主类静态代码块 ***************
*************** 加载class A的静态代码块 ***************
*************** initial A ******************
************* lazy load test *******************
*************** 加载class B的静态代码块 ***************
*************** initial B ******************
上述代码中调用了A,调用了B,所以逐步加载Class A和Class B
类加载器
上述类加载过程,主要是通过类加载器实现的,类加载器有以下几种类型:
- 引导类加载器: 负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
- 扩展类加载器: 负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
- 应用程序类加载器: 负责加载ClassPath路径下的类包,主要加载的就是自己写的类
- 自定义加载器: 负责加载用户自定义路劲下的类包
以下代码是对加载器类包的佐证:
package com.guaiyu.jvm;
import sun.misc.Launcher;
import java.net.URL;
import java.util.Optional;
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println("bootstrapClassLoader引导类加载加载的文件:");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urLs.length; i++) {
System.out.println(urLs[i]);
}
System.out.println("extClassLoader扩展类加载器加载文件:");
String extPaths = System.getProperty("java.ext.dirs");
String[] extUrls = Optional.ofNullable(extPaths)
.map(S -> extPaths.split(";"))
.orElse(new String[0]);
for (int i = 0; i < extUrls.length; i++) {
System.out.println(extUrls[i]);
}
System.out.println("appClassLoader引用程序类加载器加载文件:");
String appPaths = System.getProperty("java.class.path");
String[] appUrls = Optional.ofNullable(appPaths)
.map(s -> appPaths.split(";"))
.orElse(new String[0]);
for (int i = 0; i < appUrls.length; i++) {
System.out.println(appUrls[i]);
}
}
}
运行结果
bootstrapClassLoader引导类加载加载的文件:
file:/D:/env/jdk8/jre/lib/resources.jar
file:/D:/env/jdk8/jre/lib/rt.jar
file:/D:/env/jdk8/jre/lib/sunrsasign.jar
file:/D:/env/jdk8/jre/lib/jsse.jar
file:/D:/env/jdk8/jre/lib/jce.jar
file:/D:/env/jdk8/jre/lib/charsets.jar
file:/D:/env/jdk8/jre/lib/jfr.jar
file:/D:/env/jdk8/jre/classes
extClassLoader扩展类加载器加载文件:
D:\env\jdk8\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
appClassLoader引用程序类加载器加载文件:
D:\env\jdk8\jre\lib\charsets.jar
D:\env\jdk8\jre\lib\deploy.jar
D:\env\jdk8\jre\lib\ext\access-bridge-64.jar
D:\env\jdk8\jre\lib\ext\cldrdata.jar
D:\env\jdk8\jre\lib\ext\dnsns.jar
D:\env\jdk8\jre\lib\ext\jaccess.jar
D:\env\jdk8\jre\lib\ext\jfxrt.jar
D:\env\jdk8\jre\lib\ext\localedata.jar
D:\env\jdk8\jre\lib\ext\nashorn.jar
D:\env\jdk8\jre\lib\ext\sunec.jar
D:\env\jdk8\jre\lib\ext\sunjce_provider.jar
D:\env\jdk8\jre\lib\ext\sunmscapi.jar
D:\env\jdk8\jre\lib\ext\sunpkcs11.jar
D:\env\jdk8\jre\lib\ext\zipfs.jar
D:\env\jdk8\jre\lib\javaws.jar
D:\env\jdk8\jre\lib\jce.jar
D:\env\jdk8\jre\lib\jfr.jar
D:\env\jdk8\jre\lib\jfxswt.jar
D:\env\jdk8\jre\lib\jsse.jar
D:\env\jdk8\jre\lib\management-agent.jar
D:\env\jdk8\jre\lib\plugin.jar
D:\env\jdk8\jre\lib\resources.jar
D:\env\jdk8\jre\lib\rt.jar
D:\ideaSpacep\study\sourceCode\jvm\target
D:\env\maven\mvn_repo\org\projectlombok\lombok\1.18.36\lombok-1.18.36.jar
D:\env\IntelliJ IDEA 2023.2\lib\idea_rt.jar
双亲委派
上个章节说应用程序类加载器加载的是自己写的代码,但是为什么会加载jre/lib目录下的jar包和jre/lib/ext下的jar包呢,其实这就是我们接下来要说的内容了,是双亲委派机制造成的,废话不多说,先上图,再解释。
这里类加载其实就有双亲委派机制,加载某个类的时候会先看是否已经加载,如果已经加载过,则直接调用,如果未加载,则向上委托,如果委托到引导类加载器也寻找不到目标类,则在自己的类加载路劲中寻找并载入目标类。
比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。
简单点说,双亲委派机制就是现在父亲加载,不行再有儿子自己加载
有个问题,为什么不直接从引导类加载器加载,而要从最子类加载器加载,绕一圈呢?
- 个人理解,没有求证,我们的程序90%以上都是自己写的,我们程序第一次加载由于懒加载机制可能需要绕一圈才能加载到,但是后续在使用该类时,已经加载过直接调用就好,无需再次引导类加载器加载,提高性能
- 沙箱安全机制,加入我们自己写一个java.long.String类,由于一开始没有加载过,会向上委托,找到了该类,这样我们自己写的String类就不会被加载,可以防止核心类库被篡改
代码演示:
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("********* My String Class ************");
}
}
运行结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
- 避免类的重复加载:当父加载器加载了该类,子加载器就没必要在加载,保证被加载类的唯一性
全盘负责委托机制
全盘负责是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。
类加载过程源码解读

左边是有c++实现,主要看右边的加载过程
一 laucnher类实例化
c++创建jvm启动器后,实例化launcher类,launcher.getLauncher()初始化其他类加载器,源码如下:
// launcher在加载时创建了launcher对象
private static Launcher launcher = new Launcher();
public static Launcher getLauncher() {
return launcher;
}
// launcher的构造方法
// 这段代码我们可以知道,
//1. 扩展类加载器的父加载器就是引导类加载器,此处为null是因为引导类加载器是c++创建的对象,在java中其实看不到,所以为null,
//2. 应用程序类加载器父加载器就是扩展类加载器
public Launcher() {
ExtClassLoader var1;
try {
// 在构造器中直接创建了扩展类加载器,并将他的父加载器设置为null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 在构造器中直接创建应用程序类加载器,并将他的服务器设置为var1,var1为扩展类加载器,这个代码和extClassLoader实例化代码相似,可以自己翻一下
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 安全校验代码可以省略
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
// extClassLoader部分代码
static class ExtClassLoader extends URLClassLoader {
private static volatile ExtClassLoader instance;
public static ExtClassLoader getExtClassLoader() throws IOException {
if (instance == null) {
Class var0 = ExtClassLoader.class;
synchronized(ExtClassLoader.class) {
if (instance == null) {
// 此处创建了extClassLoader
instance = createExtClassLoader();
}
}
}
return instance;
}
// 创建extClassLoader
private static ExtClassLoader createExtClassLoader() throws IOException {
try {
return (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
File[] var1 = Launcher.ExtClassLoader.getExtDirs();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
MetaIndex.registerDirectory(var1[var3]);
}
// 实例化extClassLoader
return new ExtClassLoader(var1);
}
});
} catch (PrivilegedActionException var1) {
throw (IOException)var1.getException();
}
}
// 并且指定父extClassLoader的附加载器为空
public ExtClassLoader(File[] var1) throws IOException {
//这一行代码调用父类指定父加载器
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
// 这段代码是supper点进去的,此时的parent为null
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
acc = AccessController.getContext();
ucp = new URLClassPath(urls, factory, acc);
}
// 点到最后有一个为parent赋值的操作,parent父加载器为空
private final ClassLoader parent;
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
二 加载器调用Math.class
此处就是调用了launcher.loadclass()和classLoader.loadclass(“com.guaiyu.jvm.Math”)加载类到内存的过程,也是双亲委派机制的源码实现,这一块的源码如下:
在Launcher类中,ctrl+alt+u,可以看到类结构如下
// launcher.loadclass方法,最终调用到父类的loadclass
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
// 安全校验代码,前面的可以不看
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
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);
}
} else {
// 调用父类的loadclass方法,var1表示加载的类名:com.guaiyu.jvm.Math
return super.loadClass(var1, var2);
}
}
// 父类(ClassLoader类)的loadclass源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 此处表示先去是否已经加载了该类,该方法是本地方法调用了,是C++的代码了,我们不需要管,native代表本地方法的意思,下面注释的代码是Class<?> c = findLoadedClass(name);点进去的代码
/**
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
private native final Class<?> findLoadedClass0(String name);
*/
Class<?> c = findLoadedClass(name);
// 如果加载了,直接返回该类,如果没加载执行下面的方法
if (c == null) {
long t0 = System.nanoTime();
try {
// 此时是appClassLoader加载器,所以父加载器extClassLoader加载器
if (parent != null) {
// 调用extClassLoader.loadClass()方法,其实最终调用到父类的该方法,这里可以理解成一个递归调用,首先也会调用:
// Class<?> c = findLoadedClass(name); 由于c未null,又会判断parent是否为空,
c = parent.loadClass(name, false);
} else {
// 因为extClassLoader的parent为空,所以调用引导类加载器去调用,此处也印证了扩展类加载器的父加载器是引导类加载
// 此处其实也是调用到本地方法到C++了,感兴趣的可以点击查看,这块也找不到com.guaiyu.jvm.Math
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 代码执行到此处,c为空
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 此时其实就是向上委托结束,未找到,子类自己加载的概念了,都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
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("com.guaiyu.jvm.Math")寻找类的方法
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
// 此处相当于吧com.guaiyu.jvm.Math转换成com/guaiyu/jvm/Math.class到磁盘中target目录下寻找该十六进制文件
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;
}
自定义类加载器
上述我们看到了有自定义类加载器,自定义类加载器其实就是继承java.lang.ClassLoader类,通过上述源码我们可以知道其实主要重写loadclass(String,Boolean)(实现双亲委派机制)和findclass()方法,loadclass()直接调用父类就好,默认实现空方法,我们只需要重写findclass确保能找到我们的目标类就好
**前置条件:**我模拟类加载器,将我target中的User.class文件拷贝到D:/test/com/guaiyu/jvm/User.class,然后为了区别效果,我在User中写了一个打印方法,打印一句话:应用类加载器/自定义类加载调用方法,看输出内容
// 我们项目中的user.java源码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String nickName;
public void print(){
System.out.println("***********应用程序类加载器加载类调用方法*************");
}
}
// D:/test/com/guaiyu/jvm/User.class的源码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String nickName;
public void print(){
System.out.println("***********自定义类加载器加载类调用方法*************");
}
}
// 自定义类加载器实现
public class MyLoadClassTest {
// 继承classloader,重写findClass方法
static class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath){
this.classPath = classPath;
}
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
try {
// findClass核心代码转化为类路劲转化,然后执行defineClass方法加载类
byte[] data = loadByte(name);
return defineClass(name,data,0,data.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
private byte[] loadByte(String name) throws UnsupportedEncodingException {
// 转化类路径
name = name.replaceAll("\\.", "/");
// 找到类的全路径 D:/test/com/guaiyu/jvm/User.class
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
}
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class<?> clazz = classLoader.loadClass("com.guaiyu.jvm.User");
Object obj = clazz.newInstance();
// 利用发射机制获取类方法
Method method = clazz.getDeclaredMethod("print", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
运行结果:
***********应用程序类加载器加载类调用方法*************
sun.misc.Launcher$AppClassLoader
我们可以看到,自定义类加载器并没有被执行,这个进一步验证了我们双亲委派机制,因为我们在应用类加载器里面有个同样的com.guaiyu.jvm.User.class,直接加载应用程序类里的类了,也证明了自定义类加载器的父加载器是应用程序类加载器。
那我们现在要想加载自定义加载器的类,我们把自定义类加载器的类名称改成User1,将编译文件放入D:test/com/guaiyu/jvm/User1.class
重新修改main方法,代码如下:
// 修改类名User -> User1
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User1 {
private Integer id;
private String nickName;
public void print(){
System.out.println("***********自定义类加载器加载类调用方法*************");
}
}
// 修改main()
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class<?> clazz = classLoader.loadClass("com.guaiyu.jvm.User1");
Object obj = clazz.newInstance();
// 利用发射机制获取类方法
Method method = clazz.getDeclaredMethod("print", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
运行结果:
***********自定义类加载器加载类调用方法*************
com.guaiyu.jvm.MyLoadClassTest$MyClassLoader
自定义类加载器验证完毕了!!!!
打破双亲委派机制
现在我们尝试打破双亲委派机制,这个时候我们在加载类的时候不交给父加载器,直接调用自己写的findClass方法,所以在上述代码中我们还需要重写loadClass()
public class MyLoadClassTest {
// 继承classloader,重写findClass方法
static class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath){
this.classPath = classPath;
}
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
try {
// findClass核心代码转化为类路劲转化,然后执行defineClass方法加载类
byte[] data = loadByte(name);
return defineClass(name,data,0,data.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
private byte[] loadByte(String name) throws Exception {
// 转化类路径
name = name.replaceAll("\\.", "/");
// 找到类的全路径 D:/test/com/guaiyu/jvm/User.class
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
// 重写loadClass方法,不委托父加载器加载,
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();
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 不委托父加载器去加载,而是直接调用重写的findClass去找目标列
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;
}
}
}
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class<?> clazz = classLoader.loadClass("com.guaiyu.jvm.User1");
Object obj = clazz.newInstance();
// 利用发射机制获取类方法
Method method = clazz.getDeclaredMethod("print", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
运行结果:
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.guaiyu.jvm.MyLoadClassTest$MyClassLoader.findClass(MyLoadClassTest.java:25)
at com.guaiyu.jvm.MyLoadClassTest$MyClassLoader.loadClass(MyLoadClassTest.java:55)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at com.guaiyu.jvm.MyLoadClassTest.main(MyLoadClassTest.java:73)
Caused by: java.lang.ClassNotFoundException
at com.guaiyu.jvm.MyLoadClassTest$MyClassLoader.findClass(MyLoadClassTest.java:27)
at com.guaiyu.jvm.MyLoadClassTest$MyClassLoader.loadClass(MyLoadClassTest.java:55)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 7 more
可以看到在调用的时候会依赖到好多类,我们不可能一个一个去复制class文件到我们自己的类加载器,所以我们需要动态判断类路径,是自己写的类不向上上委托,如果是其他类则向上委托,而且有些是核心类库,也不允许我们修改
所以我们要修改loadClass方法,修改代码如下:
public class MyLoadClassTest {
// 继承classloader,重写findClass方法
static class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath){
this.classPath = classPath;
}
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
try {
// findClass核心代码转化为类路劲转化,然后执行defineClass方法加载类
byte[] data = loadByte(name);
return defineClass(name,data,0,data.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
private byte[] loadByte(String name) throws Exception {
// 转化类路径
name = name.replaceAll("\\.", "/");
// 找到类的全路径 D:/test/com/guaiyu/jvm/User.class
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
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();
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 动态判断类路径,是自己写的类不向上上委托,如果是其他类则向上委托
if (name.equals("com.guaiyu.jvm.User1")){
c = this.findClass(name);
}else {
c = super.loadClass(name,false);
}
// 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 Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class<?> clazz = classLoader.loadClass("com.guaiyu.jvm.User1");
Object obj = clazz.newInstance();
// 利用发射机制获取类方法
Method method = clazz.getDeclaredMethod("print", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
运行结果:
***********自定义类加载器加载类调用方法*************
com.guaiyu.jvm.MyLoadClassTest$MyClassLoader
假设我们现在修改核心类库,java.lang.String,看是否可以打破双亲委派机制
我们把上面写的java.long.String类复制到我们D:/test/java/long/String.class,代码如下:
package com.guaiyu.jvm;
import java.io.FileInputStream;
import java.lang.reflect.Method;
public class MyLoadClassTest {
// 继承classloader,重写findClass方法
static class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath){
this.classPath = classPath;
}
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
try {
// findClass核心代码转化为类路劲转化,然后执行defineClass方法加载类
byte[] data = loadByte(name);
return defineClass(name,data,0,data.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
private byte[] loadByte(String name) throws Exception {
// 转化类路径
name = name.replaceAll("\\.", "/");
// 找到类的全路径 D:/test/com/guaiyu/jvm/User.class
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
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();
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 动态判断类路径,是自己写的类不向上上委托,如果是其他类则向上委托
if (name.equals("java.long.String")){
c = this.findClass(name);
}else {
c = super.loadClass(name,false);
}
// 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 Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class<?> clazz = classLoader.loadClass("java.long.String");
Object obj = clazz.newInstance();
// 利用发射机制获取类方法
Method method = clazz.getDeclaredMethod("print", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
运行结果:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
所以对于核心类库,是不允许打破双亲委派机制的!!!
tomcat打破双亲委派机制
以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
- 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
- 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
- web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
- web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。
再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行?
答案是不行的。为什么?
第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第三个问题和第一个问题一样。
我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
Tomcat自定义加载器详解
tomcat的几个主要类加载器:
commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader:各个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文件的热加载功能。
tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?答案是:违背了。
很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。
模拟实现Tomcat的webappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离
依照上面自定义加载器,我们在创建一个D:/test1/com/guaiyu/jvm/User1.class
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
/**
* 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//非自定义的类还是走双亲委派加载
if (!name.startsWith("com.guaiyu.jvm")){
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
public static void main(String args[]) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class clazz = classLoader.loadClass("com.guaiyu.jvm.User1");
Object obj = clazz.newInstance();
Method method= clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader());
System.out.println();
MyClassLoader classLoader1 = new MyClassLoader("D:/test1");
Class clazz1 = classLoader1.loadClass("com.guaiyu.jvm.User1");
Object obj1 = clazz1.newInstance();
Method method1= clazz1.getDeclaredMethod("sout", null);
method1.invoke(obj1, null);
System.out.println(clazz1.getClassLoader());
}
}
运行结果:
=======自己的加载器加载类调用方法=======
com.guaiyu.jvm.MyClassLoaderTest$MyClassLoader@266474c2
=======另外一个User1版本:自己的加载器加载类调用方法=======
com.guaiyu.jvm.MyClassLoaderTest$MyClassLoader@66d3c617
注意:同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。
模拟实现Tomcat的JasperLoader热加载
原理:后台启动线程监听jsp文件变化,如果变化了找到该jsp对应的servlet类的加载器引用(gcroot),重新生成新的JasperLoader加载器赋值给引用,然后加载新的jsp对应的servlet类,之前的那个加载器因为没有gcroot引用了,下一次gc的时候会被销毁。
更多推荐



所有评论(0)