JVM类加载过程详细分析
双亲委派加载模型
为什么需要双亲委派加载模型
主要是为了安全,避免用户恶意加载破坏JVM
正常运行的字节码文件,比如说加载一个自己写的java.util.HashMap.class
。这样就有可能造成包冲突问题。
类加载器种类
file- 启动类加载器:用于加载
jdk
中rt.jar
的字节码文件 - 扩展类加载器:用于加载
jdk
中/jre/lib/ext
文件夹下的字节码文件 - 应用程序类加载器:加载
classPath
下的字节码文件 - 自定义类加载器:用户在程序中自己定义的加载器
源码分析
1、ClassLoader.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);
// 如果这个Class对象还没有被加载,下面就准备加载
if (c == null) {
long t0 = System.nanoTime();
try {
// 查看当前类加载器有没有父类加载器
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
}
// 如果父类加载器也没有加载这个Class对象,就由自己来加载
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;
}
}
不遵守双亲委派加载模型的例子
双亲委派加载模型仅仅是一个约定,后面实现类加载器时,是可以不遵守这个约定。ClassLoader
是在JDK1.0
的时候就设计好的,而双亲委派加载模型在JDK1.2
引入的。所以,有些机制是没有遵守这个约定的。比如:Service Provider Interface
机制的JDBC
就没有遵守这个约定。
1、为什么JDBC
无法遵守这个约定?
JDBC
是SPI
机制的一个例子,JDK
定义了java.sql.Connection
核心接口,后续MySQL
、Oracle
为其提供实现类。在运行中是通过java.sql.DriverManager
来获取指定实现类的实例。这里需要明白三个问题:
-
java.sql.DriverManager
是在rt.jar
中,由核心类加载器加载的; - 第三方所提供
Collection
的实现类都是在classpath
中; - 类中方法想加载新的字节码文件时,其初始类加载器就是当前这个类的定义类加载器;
也就是说当JVM
在java.sql.DriverManager
类的getConnection()
方法中获取Collection
实现类的字节码时,当前类的定义类加载器是启动类加载器,而按照约定启动类加载器是不允许加载classpath
下的字节码。所以,JDBC
就无法遵守这个约定。
2、JDBC
是如何解决上面的问题的?
为了解决这个,java
在线程中放入一个类加载器Thread.currentThread().getContextClassLoader()
;而这个类加载器可以是随意的。比如你想加载classpath
包下的字节码文件,只需要设置当前线程的类加载器为应用程序类加载器即可。
JVM
类加载过程
JVM
本质的工作就是读取字节码文件、执行字节码文件中的指令。其中JVM
将读取字节码文件的过程称为JVM
类加载过程。
JVM
读取的字节码文件将放在方法区里;
JVM
类加载机制分为五个部分:加载、验证、准备、解析、初始化。如下图所示:
一、Loading
:加载
这一步是将JVM
外的字节码文件加载到JVM
内部方法区中的Class
对象。
JVM
可以通过几种方式来加载外部的字节码文件?
- 从本地读字节码文件;
- 从网络读取字节码文件;
- 通过动态生成的字节码文件;
初始类加载器和定义类加载器
由于双亲委派加载模型的存在,一个Class
对象的初始类加载器initiating class loader
和定义类加载器defining class loader
有可能不是同一个。
- 初始类加载器:它是指让
JVM
加载这个字节码文件 - 定义类加载器:它是真正调用
defineClass
方法,将字节码转换成Class
对象
java
在判断instanceof
时,只有类名、defining class loader
都相等,才表示是同一个类的实例。
Class.getClassLoader()
得到的是定义类加载器
相关实验代码
1、验证使用不同ClassLoader
加载字节码文件
// 这种方法是不遵守双亲委派加载模型的约定
public class ClassLoaderLoading {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 这个Class对象是由当前方法的类加载器加载
Class c1 = MiniJVM.class;
Class c2 = new MyClassLoader().loadClass("com.github.hcsp.MiniJVM");
// 使用c2创建一个MiniJVM实例
Object o = c2.getConstructor().newInstance();
System.out.println(o instanceof MiniJVM);
MiniJVM demo = (MiniJVM) o;
}
private static class MyClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.contains("MiniJVM")) {
try {
byte[] bytes = Files.readAllBytes(new File("target/classes/com/github/hcsp/MiniJVM.class").toPath());
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
return super.loadClass(name);
}
}
}
}
2、实现一个遵守双亲委派加载模型的类加载器
public class ClassLoaderLoading {
public static void main(String[] args) throws ClassNotFoundException {
Class c1 = MiniJVM.class;
Class c2 = new MyClassLoader(ClassLoader.getSystemClassLoader()).loadClass("com.github.hcsp.MiniJVM");
System.out.println("c2 = " + c2);
}
private static class MyClassLoader extends ClassLoader {
public MyClassLoader(ClassLoader systemClassLoader) {
super(systemClassLoader);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 加载你想让这个类加载器加载的字节码文件
if (name.contains("MiniJVM")) {
try {
byte[] bytes = Files.readAllBytes(new File("target/classes/com/github/hcsp/MiniJVM.class").toPath());
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
// 其他的字节码文件交由父类加载器加载
return super.loadClass(name);
}
}
}
}
二、Linking
:链接
当一个.java
文件编译成.class
文件时,里面含有一个符号引用,比如/java/utils/HashMap
。Linking
是指将这符号引用与具体的class
对象链接起来。
每个字节码结构都有一个运行时常量池,它会存储每个符号引用和所对应的具体对象,以此实现链接。
-
Verification
:验证字节码的正确性 -
Preparation
:为static
成员赋默认初始值 -
Resolution
:解析当前字节码里包含的其他符号引用
三、Initializing
执行初始化方法。比如下面的四个虚拟机指令:new
、getstatic
、putstatic
、invokestatic