JVM类加载机制
一 类加载过程
类加载:类加载器将class文件加载到jvm内存中

(1) 加载:从磁盘查找并在找到后通过IO读入字节码文件
(2)建立连接做如下工作:
验证:验证字节码文件内容是否正确,有相应的语法逻辑
准备:给类的静态变量分配内存,并赋予java虚拟机规定的默认值,比如 static int a=5,会赋予默认值int a=0
解析:类加载器将类所引用的其他类装载
(3)初始化:对类的静态变量初始化为用户代码中指定的值,比如static int a=5,在这一步赋予值5
二 类加载器分类
JVM角度:
- 启动类加载器(bootstrap ClassLoader),该加载器使用C++实现,是虚拟机自身一部分
- 其他所有的类加载器,这些加载器都由java实现,独立于JVM,全部继承自java.lang.ClassLoader
开发人员角度:
- 启动类加载器(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 应用程序类加载器(Application ClassLoader)
应用程序由着三种类加载器互相配合完成加载,三者关系如下图

图中的自定义加载器在我们自己写程序的时候基本不会用到,一般如tomcat之类的中间件会使用到自定义类加载器
我们通过程序来看一下这几种加载器
public class Test2 {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(Test2.class.getClassLoader().getClass().getName());
// 系统类加载器就是应用程序类加载器,通过上面一行代码和下面一行代码均可以打印
System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
}
}


之所以第一个string的类加载器没有打印结果是因为,string属于rt.jar,而rt.jar中含有jdk中最核心的类,是c和c++语言写的,因此根本就不会知道他的名字
三 类加载机制
3.1 全盘负责委托机制
当一个加载器加载了类A,则类A所依赖和引用的类比如类B和类C都将由加载类A的加载器加载
3.2 双亲委派机制
上图中类加载器之间的层次关系,就叫做类加载器的双亲委派模型(parents delegation model)。双亲委派机制于jdk1.2引入。特点是除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器
父类加载器不是说子类加载器继承了父类加载器,而是子类加载器持有父类加载器的引用,可以通过这个引用找到父类加载器
3.2.1 双亲委派机制加载过程
如果一个类加载器收到类加载的请求,他首先不会去自己尝试加载这个类,而是将这个请求委派给自己的父类加载器完成,每个层次的类加载器都是这样,因此所有的加载请求最后都会传送给顶层的启动类加载器,只有当父类加载器反馈自己无法完成这个加载请求(他的搜索范围没有找到所需的类)时候,子加载器才会尝试去加载
比如加载我们自己写的类A.class,类A中引用了我们自己写的java.lang.string.class,则应用程序加载器在加载类A的时候,会顺便尝试加载我们自己写的string.class,加载的话不是直接加载而是委托给父类加载器一级一级上去直到启动类加载器加载到正确的string.class
测试案例:
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("hello world");
}
}

可以看到程序没有打印这句话,因为并没有加载我们自己的string.class而是加载了jdk自己的string.class,实际运行的是jdk的string类,jdk的string类是没有main方法的因此报错
3.2.2 双亲委派机制优点
- 沙箱安全机制:
自己写的java.lang.string.class类不会被加载,这样可以防止jdk核心rt.jar中类被随意篡改
因为加 - 避免类的重复加载
当父类已经加载了该类以后,子类加载器将不再加载该类
四 JVM加载jar包是否将jar包中所有类加载到内存
jvm对class文件的加载是按需加载,不会一次性加载
代码示例:
下面代码执行的时候,需要在执行前添加执行参数: -verbose:class ,加上以后除了打印正常输出之外,还会打印jvm中类加载相关的信息

public class TestDynamicLoad {
static {
System.out.println("*************static code************");
}
public static void main(String[] args){
new A();
System.out.println("*************load test************");
new B();
}
}
class A{
public A(){
System.out.println("*************initial A************");
}
}
class B{
public B(){
System.out.println("*************initial B************");
}
}

从上面的运行结果可以看到,由之前学到的静态代码块是在类初始化的时候执行的,这个时候已经完成了类加载,因此我们在前面可以看到当前类被加载的信息,但是当前类应用的类A和类B并未在开始的时候就加载,而是在用到了以后才加载