Jvm类加载机制

类的运行过程 以一个main方法举例:
Jvm类加载机制
文章图片

类加载的具体流程为: Jvm类加载机制
文章图片

1.加载:把class字节码文件通过类加载器加载到内存中
2.验证:校验字节码文件是否符合jvm规范
3.准备:给静态变量赋初始值
比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段tmp的初值就是456
4.解析:将符号引用转为直接引用
符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
如果在类加载期间解析的则为“静态链接”,代码运行到相应代码行才解析的则为“动态链接”。
举个例子:

点击查看代码
public static void main(String[] args) { Math math = new Math(); math.compute(); }

其中main方法是在类加载时放到内存中的,为静态链接;math.compute()是运行到这一行的时候才加载的,为动态链接。
5.初始化:对类的静态变量初始化为指定的值,执行静态代码块
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
注意:java中的类加载为懒加载,一个jar包启动的时候不会加载所有的类,他只有用到了才会加载。
举个例子:
点击查看代码
public class Main4 {static { System.out.println("*************load TestDynamicLoad************"); }public static void main(String[] args) { new A(); System.out.println("*************load test************"); B b = null; } }class A { static { System.out.println("*************load A************"); }public A() { System.out.println("*************initial A************"); } }class B { static { System.out.println("*************load B************"); }public B() { System.out.println("*************initial B************"); }}

运行结果:
点击查看代码
*************load TestDynamicLoad************ *************load A************ *************initial A************ *************load test************

如果将main方法中的B b = null; 修改为B b = new B(); ,运行结果会变为:
点击查看代码
*************load TestDynamicLoad************ *************load A************ *************initial A************ *************load test************ *************load B************ *************initial B************

类加载器与双亲委派 类加载器 java中的类加载器有引导类加载器、拓展类加载器、应用程序类加载器与自定义类加载器;
1.引导类加载器:负责加载位于jre的lib目录下的核心类库;
2.拓展类加载器:负责加载位于jre的lib目录下ext目录下的jar包;
3.应用程序类加载器:负责加载ClassPath路径下的类包(自己写的类);
4.自定义类加载器:负责加载用户自定义路径下的类包。
Jvm类加载机制
文章图片

双亲委派 双亲委派的具体流程如下图,在加载某个类时,首先会从AppClassLoader中判断是否加载过此类,如果没有加载,则委托父类加载器去判断;如果都没加载过,则由父类先判断是否可以加载,父类可以加载则由父类加载,否则交给子类去加载。
双亲委派机制简单说就是优先由父类去加载,父类加载不了再由子类加载。
Jvm类加载机制
文章图片

【Jvm类加载机制】这样的好处是:
1.沙箱安全:自己写的类不会覆盖核心库中的同名类,例如自己写个java.lang.String,然后你想篡改它的实现,由于BootstrapClassLoader中已经有同名类,因此不会被加载,从一定程度防止了危险代码的注入。
2.避免类的重复加载;
全盘负责委托机制
ClassLoader加载类用的是全盘负责委托机制,“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类通常也由这个ClassLoder载入。
实现代码如下:
点击查看代码
//ClassLoader的loadClass方法,里面实现了双亲委派机制 protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 检查当前类加载器是否已经加载了该类 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(); //都会调用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; } }

打破双亲委派 首先看自定义类加载器:自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,一般来说我们自定义类加载器主要是重写findClass方法,如果想打破双亲委派,则需要我们覆写loadClass方法。
实现代码如下:
点击查看代码
/** * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载 * * @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(); 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; } } }

思考:Tomcat使用默认双亲委派行不行? 我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的
    不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是
    独立的,保证相互隔离。
  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程
    序,那么要有10份相同的类库加载进虚拟机。
  3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的
    类库和程序的类库隔离开来。
  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中
    运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。
因此Tomcat不能使用默认的双亲委派。为什么?
第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认
的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第三个问题和第一个问题一样。
我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文
件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp
是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想
到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载
器。重新创建类加载器,重新加载jsp文件。

    推荐阅读