JVM类加载
前言
在我们写一个.java 文件时,这个文件是怎么被处理的呢。Java 可以解释执行也可以编译执行,大多数JVM采用第三种混合的方式。冯诺依曼体系的计算机模型中,任何程序都需要加载到内存中才能和CPU进行交流。.java文件被编译成.class的字节码文件之后交给JVM执行时也同样需要被加载到内存中,才可以实例化类。
JVM的类加载机制指的是虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。简单来说就是将.class字节码文件实例化成Class对象并进行相关初始化的过程。
1. 类加载过程
通过双亲委派模型来进行类加载的,其中的重要部分就是类加载器。类加载器对类进行加载(Loding)、连接(Linkind)、初始化(Init)。其中连接阶段指的是验证、准备、解析这三部分。
加载:
Load 阶段读取类文件产生二进制流,并转化为特定的数据结构,初步校验 cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的 java.lang.Class 实例。
连接:
- 验证: 验证是更详细的校验,比如final 否合规、类型是否正确、静态变量是否合理等
- 准备: 准备阶段是为静态变量分
配内存,并设定默认值 - 解析: 解析指找到相关引用,进行相关的类加载。
初始化: Init 阶段执行类构造器<clinit> 方法
类加载器有哪些:
- 启动类加载器(Bootstrap ClassLoader)
- 使用 C++ 语言实现,是虚拟机自身的一部分
- 负责<JAVA_HOME>\lib 目录中的
下面的类加载器都是由 Java 语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader
-
拓展类加载器(Extension ClassLoader)
- 负责加载<JAVA_HOME>\lib\ext 目录
-
应用程序类加载器(Application ClassLoader)
- 负责加载用户路径上所指定的类库
-
自定义类加载器(User ClassLoader)
- 开发者自己拓展定义的
2. 双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把则会个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此所有的加载请求都是应该传送到顶层的类加载器去处理,只有当父加载器反馈自己无法完成这个加载(它的搜索范围内没有找到所需的类时),子类才会去尝试自己去加载。
双亲委派模型是如何实现的?
java.lang.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);
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
// 都没加载到说明父类无法处理请求,自己进行加载
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 调用自身的 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;
}
}
双亲委派模型的好处就是Java类会有一种带有优先级的层次关系,比如java.lang.String 它存放在rt.jar 中 无论那个类加载都会往上级询问,也就是最终到BootStrap类加载器进行加载。也就是说如果你自己实现一个名为java.lang.String的类,并放在程序的ClassPath中,可以正常编译,但无法被加载运行。(如果可以的话,程序就会产生混乱了)。
自定义类加载器:
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] result = getClassFromMyPath(name);
try {
if (result == null) {
throw new FileNotFoundException();
} else {
return defineClass(name, result, 0, result.length);
}
} catch (Exception e) {
e.printStackTrace();
}
throw new ClassNotFoundException();
}
private byte[] getClassFromMyPath(String name) {
// 从自定义路径中加载指定类
return null;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
try {
Class<?> clazz = Class.forName("Student", true, classLoader);
Object obj = clazz.getInterfaces();
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
ctrl + alt + t idea 快捷 try catch
3. 那种情况需要自定义类加载器
-
隔离加载类
- 在某些框架内进行中间件与应用的模块隔离 把类加载到不
同的环境。比如 阿里内某容器框架通过自定义类加载器确保应用中依赖的 jar 包不
会影响到中间件运行时使用的 jar 包。
- 在某些框架内进行中间件与应用的模块隔离 把类加载到不
-
修改类加载方式
- 类的加载模型并非强制 Bootstrap 其他的加载并非一
定要引入 或者根据实际情况在某个时间点进行按需进行动态加载。
- 类的加载模型并非强制 Bootstrap 其他的加载并非一
-
扩展加载源
- 比如从数据库、网 ,甚 是电视机机顶盒进行加载
-
防止源码泄露
- Java 代码容易被编译和篡改,可以进行编译加密 那么
加载器也需要自定义,还原加密的字节码。
- Java 代码容易被编译和篡改,可以进行编译加密 那么
Reference
- 《深入理解Java虚拟机》
- 《码出高效》