彻底理解JVM类加载机制
文章目录
- 一、类加载器和双亲委派机制
- 1.1、类加载器
- 1.2、双亲委派机制
- 1.3、自定义类加载器
- 1.4、打破双亲委派机制
- 二、类的加载
图片来源:图灵学院
由上图可知,创建对象,执行其中的方法,在java层面,最重要的有获取类加载器
以及加载类
两部分
一、类加载器和双亲委派机制
1.1、类加载器
在JVM中,类加载器分为:
- 启动类加载器:最核心的类加载器,用于加载JRE的lib目录下的
核心类库
。 - 扩展类加载器:负责加载位于JRE的lib目录下的ext扩展目录中的JAR类包。
- 应用程序类加载器:负责加载ClassPath路径下的类包。
- 自定义类加载器:用于加载用户自定义路径的类包。
sun.misc
包下的Launcher
类,会在构造方法中对类加载器进行初始化:
构造方法中的关键部分:
sun.misc.Launcher.AppClassLoader#getAppClassLoader
(获取应用程序类加载器方法)的最底层,将参数中传入的扩展类加载器,赋值给成员变量。
ExtClassLoader
扩展类加载器继承自ClassLoader
1.2、双亲委派机制
双亲委派机制的核心是,向上委派,向下加载:
即当加载某个类时,会从最底层的类加载器开始,逐个向上查找目标类,如果加载过,就直接返回。如果查找到最上层依旧没有,则从最上层自身负责的类加载路径中查找并加载。
- 自己写的一个User类,第一次运行,应用程序类加载器没有找到,向上查找扩展类、启动类加载器,依然没有找到。就从最顶层的启动类加载器开始,查看该类是否在自己的加载范围内。很显然自定义的类,不在启动类加载器的范围内,向下委派到扩展类加载器,发现依旧不在自身的范围内,就再次委派给应用程序类加载器,这次发现在自己的加载范围内,就会加载。
- java.lang包下的String类,第一次运行,应用程序类加载器没有找到,向上查找扩展类、启动类加载器,依然没有找到。就从最顶层的启动类加载器开始,查看该类是否在自己的加载范围内。java.lang包下的String类属于
核心类库
,启动类加载器发现它在自己应该加载的范围内,就会进行加载。
双亲委派机制在源码中的体现在于java.lang.ClassLoader#loadClass(java.lang.String, boolean)
:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先查看当前的类加载器是否加载过目标(底层是c/c++实现的方法)
Class<?> c = findLoadedClass(name);
//当前的类加载器没有加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//扩展类加载器进行查找
//这里的parent属性是当前类加载器的父加载器
if (parent != null) {
c = parent.loadClass(name, false);
}//启动类加载器进行查找
else {
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();
//查看当前的类路径,判断是否应该是自己加载。
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;
}
}
第一次加载某个自定义类时的流程:
应用程序类加载器发现自身没有加载过,去执行扩展类加载器的loadClass
方法:
扩展类加载器发现自身没有加载过,去找启动类加载器(底层由c/c++执行) 启动类加载器返回的c为空,在扩展类加载器进入if判断,尝试自己加载,但是自定义的类不在自己的加载范围内,将null值的c返回到应用程序类加载器:
应用程序类加载器得到扩展类加载器的返回为空,就尝试自己加载并且返回:
1.3、自定义类加载器
自定义类加载器,需要继承ClassLoader
,并且重写其中的findClass
,loadClass
方法:
public class MyClassLoaderTest1 {
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);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
public static void main(String args[]) throws Exception {
//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
//D盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
Class clazz = classLoader.loadClass("com.itbaima.jvm.User");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
上面的自定义类加载器,目的是为了加载D盘test文件夹下的com.itbaima.jvm
的User.class文件,假设目前的classpath下和D盘指定目录下都有该文件?
那么根据双亲委派机制
,User类应该由应用程序类加载器
,而非自定义类加载器
加载。原因很简单,当User类被启动类加载器
向下加载时,在应用程序类加载器
这一层,发现属于自己的classpath范围内有这个类,就会进行加载。
把classpath下的User类删除,则由自定义类加载器
进行加载:
1.4、打破双亲委派机制
打破双亲委派机制的关键在于自定义实现loadClass
方法。在1.3案例的基础上,继续重写loadClass
方法:
@Override
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();
//让父类去加载Object
if (!name.startsWith("com.itbaima.jvm")) {
c = this.getParent().loadClass(name);
}else {
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;
}
在重写的loadClass
方法中,去除了向上委派的逻辑,而是由自身进行加载,为什么还要加上
if (!name.startsWith("com.itbaima.jvm")) {
c = this.getParent().loadClass(name);
}
的判断?原因在于,用当前的类加载器去加载User类没有问题,但是User类继承了java.lang
包下的Object类,如果把判断条件去除:
如果把Object.class文件复制一份到指定的目录呢?
答案也是否定的,因为在类加载的过程中JVM还有自己的安全监测机制,是不会允许核心的类被自定义加载的。
二、类的加载
类的加载,通过loadClass
方法,会经历验证
、准备
、解析
、初始化
四个阶段:
- 验证阶段:主要是校验class文件是否符合规范,以及对正确性进行校验,最经典的是cafe babe模数校验。
- 准备阶段:给类的静态属性分配内存,并且赋初值(基本数据类型为默认值,引用类型为null)。
- 解析阶段:将
符号引用
转变为直接引用
,符号引用
简单可以理解为类中的方法名,变量名等。而直接引用
,是符号引用
真正指向的内存地址。 - 初始化:在这一步才是给类的静态属性赋值,并且执行静态代码块。
静态代码块的执行先于构造方法: