JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。

前言

  1. 切换jdk版本,我目前用的是最高版本18,但是学习需要改成8版本:JDK不同版本切换
  2. 本博文主要讲解:类初始化过程中的 ==类加载的过程细节,也就是 ClassLoader ==

一、类初始化之 类加载

1、总述

在这里插入图片描述
编译好的class文件默默的趟在了硬盘上,怎样才可以到内存里并准备好呢, 如图三大步所示,解析如下:

  1. loading:把class文件 load 到 内存
  2. linking
    1. verification:校验,加载的class 是否符合class文件的标准,比如,一个文件的开头不是CAFEBABE,说明不是class文件,则校验失败。
    2. preparation:很重要,把静态变量赋默认值。比如:如果类中有 i=8,在这里会给i赋值0,并不会在这儿赋值8
    3. resolution:class文件里常量池里面用的到 符号引用 转换成 访问到的内存地址的内容
      将类、方法、属性等 符号引用 解析为 直接引用。常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
      源码的体现如下:
      在这里插入图片描述
  3. initializing:调用静态代码块为静态变量赋值为初始值。

2、类加载器

  • JVM本身有一个类加载器的层次,类加载器就是一个普通的class,分别加载不同的class类。

  • JVM所有的class都是被类加载器加载到内存的,类加载器 简单称为ClassLoader。

  • 不同的类被不同的类加载器加载到内存。

  • 下图中,CustomClassLoader的父加载器是 Application,其父加载器是 Extension 加载器,其父加载器是Bootstrap 加载器,这里指的是父类加载器,并无继承关系。

在这里插入图片描述

  • 有一个顶级的抽象父类如下:
    在这里插入图片描述

  • 如何看类是被哪个ClassLoader加载到内存的呢,很简单,看下面代码。一个类被扔到内存之后会有两块内容,第一块内容是把二进制的内容放到内存里;第二块内容与之同时生成了class类的对象,指向了第一块内容。以后我们写的对象去访问第二块内容生成的对象,然后在引用第一块内容。第一块内容是在metespace(元空间)中。

    package com.mashibing.jvm.c2_classloader;

    public class T002_ClassLoaderLevel {
        public static void main(String[] args) {
            System.out.println(String.class.getClassLoader());
            String i = "sd";
            // 第二种方式
            System.out.println(i.getClass().getClassLoader());
            System.out.println(sun.awt.HKSCS.class.getClassLoader());
            // ext 下的,但是目前我电脑上没有。
    //        System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());

            // 自己写的类,是由Application 加载的
            System.out.println(T002_ClassLoaderLevel.class.getClassLoader());
            System.out.println(T002_ClassLoaderLevel.class.getClassLoader().getClass().getClassLoader());
            System.out.println(new T006_MSBClassLoader().getParent());
            System.out.println(ClassLoader.getSystemClassLoader());
        }
    }
  • 下图可见,有两个,一个null,一个 AppClassLoader。接下来就要说类加载器的层次啦
    在这里插入图片描述

3、类加载器层次

类加载器是分成不同的层次来加载的,不同的类加载器加载不同的class类(上面也说过啦)

  1. 最顶层的叫Bootstrap,最开始的类加载器,负责加载JDK里面最核心的那些jar文件里包类,又c++实现的类加载器,通过ClassLoader 拿到类加载器的结果是null的时候,就是最顶级类加载器
  2. 其次是 Extensionext扩展类加载器,负责加载扩展包里的类文件,在java安装路径中 有个ext 文件,就是扩展包。
  3. 然后就是Appcliaction,加载classpath 指定内容,我们写的类默认在这个路径里。
  4. 最后就是自定义类加载器:CustomClassLoader。自定义的ClassLoader。

    • 加载过程是双亲委派机制 (下一节讲)
      在这里插入图片描述

4、网络上错误的继承关系

下图在很多地方有出现,这是错误的,并无继承关系,只是在语法上有基础之意。
在这里插入图片描述

二、类加载器 详情

1、加载过程之双亲委派机制

class 文件编译后,需要加载到内存,如何加载呢,就是双亲委派机制来进行加载。

a、双亲委派机制

在这里插入图片描述
在这里插入图片描述

结合上图,
如果有个类,要加载到内存中,先从最下边的左边

  1. 的CustomClassLoader 去加载,CustomClassLoader 看缓存里是否加载了,如果没有;
  2. 问父加载器 ApplicationClassLoader 有没有加载进来,ApplicationClassLoader 看缓存里是否加载了,如果没有;
  3. 问父加载器 ExtensionClassLoader 有没有加载进来,ExtensionClassLoader 看缓存里是否加载了,如果没有;
  4. 问父加载器 BootstrapClassLoader 有没有加载进来,BootstrapClassLoader 看缓存里是否加载了,如果没有。
  5. 则从右边回过头来往下走看是否是 BootstrapClassLoader 应该加载的类,如果不是;
  6. 则往下来看是否是ExtensionClassLoader 应该加载的类,如果不是;
  7. 则往下来看是否是ApplicationClassLoader 应该加载的类,如果不是;
  8. 则往下来看是否是 CustomCLassLoader 应该加载的类,如果是则加载进来
  9. 如果还不是,则报错:ClassNotFoundException...
  10. 这就是双亲委派进制
  11. 对应的代码是 ClassLoader.loadClass(),通过递归来实现的。
    在这里插入图片描述

b、类加载过程

也就是双亲委派机制,如下图,更加清晰明了。
在这里插入图片描述

  • 这里的缓存是:自己的类加载器所维护的容器,一个数组。将加载的类文件都放里面了。

  • 双亲:是指从子加载器父加载器 ,再从 父加载器子加载器。不要再上面加类,这里和类是没有关系的。(这里的翻译是比较怪的)

  • 面试题必问之一:为什么要搞双亲委派机制,这么麻烦,直接放到容器不行吗?

    答:因为安全,假如用反证法,假如给你任何一个class,你自定义的class,都可以自由的把它 load 到内存,那么给这么一个类:java.lang.String,自定义的类,直接将oracle内部写的类覆盖掉。按说不应该,需要看其他ClassLoader 是否加载了这个类,如果加载了就不用下载了。

2、父加载器

  • 父加载器

    • 父加载器不是“类加载器的加载器”!!!!!也不是“类加载器的父类加载器”
  • 双亲委派是一个孩子向父亲方向,然后父亲向孩子方向的双亲委 派过程

  • 思考:为什么要搞双亲委派

    • – java.lang.String类由自定义类加载器加载行不行?(2.2中给出了问答
  • 后来 8版本以后 ExtClassLoader 变成了 PlatformClassLoader
package com.mashibing.jvm.c2_classloader;

public class T004_ParentAndChild {
    public static void main(String[] args) {
        //AppClassLoader
        System.out.println(T004_ParentAndChild.class.getClassLoader());
        // AppClassLoader 的class 的 classLoader 是null
        System.out.println(T004_ParentAndChild.class.getClassLoader().getClass().getClassLoader());
        // AppClassLoader 的父加载器 是 PlatformClassLoader (原来是Ext)
        System.out.println(T004_ParentAndChild.class.getClassLoader().getParent());
        // null,类似于上面第二个
        System.out.println(T004_ParentAndChild.class.getClassLoader().getParent().getParent());
        // 空指针报错
        //System.out.println(T004_ParentAndChild.class.getClassLoader().getParent().getParent().getParent());
    }
}

java18版本
在这里插入图片描述
8版本如下图所示
ExtensionClassLoaderPlatformCLassLoader 是等价的,8版本之后
在这里插入图片描述

Launcher 类所在的包是sun.misc

3、类加载器范围

a、三个类加载器:

1. Bootstrap 
2. ExtensionClassLoader
3. AppClassLoader

b、 sun.misc.Launcher$ExtClassLoader@1b6d3586 解析如下

三个类加载器都在 Launcher 类中,为其内部类$ 符号 后面的是加载器名称,也是内部类类名,@符号 后面的该类具体的 哈希code码

c、 Launcher类源码如下

在这里插入图片描述
(来自Launcher源码)

  • sun.boot.class.path

    • Bootstrap ClassLoader加载器的 加载路径
    • 截图如下:
      在这里插入图片描述
  • java.ext.dirs

    • ExtensionClassLoader加载器的 加载路径
    • 截图如下:
      在这里插入图片描述
  • java.class.path

    • AppClassLoader加载器的 加载路径
    • 截图如下:
      在这里插入图片描述

d、三类加载器加载的包位置

找到对应的包,然后用换行符替换分号,便于可视化观看。

package com.mashibing.jvm.c2_classloader;

public class T003_ClassLoaderScope {
    public static void main(String[] args) {
        String pathBoot = System.getProperty("sun.boot.class.path");
        System.out.println(pathBoot.replaceAll(";", System.lineSeparator()));

        System.out.println("--------------------");
        String pathExt = System.getProperty("java.ext.dirs");
        System.out.println(pathExt.replaceAll(";", System.lineSeparator()));

        System.out.println("--------------------");
        String pathApp = System.getProperty("java.class.path");
        System.out.println(pathApp.replaceAll(";", System.lineSeparator()));
    }
}

打印如下

"C:\Program Files\Java\jdk1.8.0_351\bin\java.exe" "-javaagent:D:\devApp\idea\IntelliJ IDEA Educational Edition 2022.1\lib\idea_rt.jar=52418:D:\devApp\idea\IntelliJ IDEA Educational Edition 2022.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_351\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\rt.jar;E:\msb\学习路线\chapter03_JVM\(剪) JVM调优第一版\out\production\JVM" com.mashibing.jvm.c2_classloader.T003_ClassLoaderScope
C:\Program Files\Java\jdk1.8.0_351\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_351\jre\classes
--------------------
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
--------------------
C:\Program Files\Java\jdk1.8.0_351\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\deploy.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\access-bridge-64.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\cldrdata.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\dnsns.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\jaccess.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\jfxrt.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\localedata.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\nashorn.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\sunec.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\sunjce_provider.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\sunmscapi.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\sunpkcs11.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\zipfs.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\javaws.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\jfxswt.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\management-agent.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\plugin.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_351\jre\lib\rt.jar
E:\msb\学习路线\chapter03_JVM\(剪) JVM调优第一版\out\production\JVM
D:\devApp\idea\IntelliJ IDEA Educational Edition 2022.1\lib\idea_rt.jar

进程已结束,退出代码0

这里有个重点:
这里是当前项目的路径,也会从这里加载,所以我们写的代码也属于APPClassLoader 类加载器啦。
最下面那个就是idea的路径啦。
在这里插入图片描述

4、自定义类加载器

a、三步

  1. 继承 ClassLoader
  2. 重写模板方法 findClass
    • 调用 defineClass
  3. 自定义类加载器加载自加密的class
    • 防止反编译
    • 防止篡改

b、案例:加载其他类

package com.mashibing.jvm.c2_classloader;

public class T005_LoadClassByHand {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clazz = T005_LoadClassByHand.class.getClassLoader().loadClass("com.mashibing.jvm.c2_classloader.T002_ClassLoaderLevel");
        System.out.println(clazz.getName());

        //利用类加载器加载资源,参考坦克图片的加载
        //T005_LoadClassByHand.class.getClassLoader().getResourceAsStream("");
    }
}

在这里插入图片描述

  1. T005_LoadClassByHand.class.getClassLoader() 获取的是APPClassLoader 类加载器,
  2. 通过 loadClass 方法进行加载指定的 T002_ClassLoaderLevel类。然后将此类加载进来。
  3. 将此类加载 到内存,会返回class 对象:com.mashibing.jvm.c2_classloader.T002_ClassLoaderLevel
  • 题外话:loadClass() 是反射的基石。
  • 什么时候去加载一个类呢?
    spring 有一个动态代理,会自动加载。

c、读loadClass源码

在这里插入图片描述

  1. 先加锁 synchronized,以防加载过程中,其他类在加载
  2. 首先 findLoadedClass,先查看是否加载过了,如果没有加载过,在进行加载。底边源码直接到native了
  3. 父加载器再进行查找,看是否加载了,进入递归模式委派机制
  4. 如果没有找到在回来进行 加载类 findClass
  5. 所以,如果自定义类加载器,则重写 findClass方法即可。(在上图中的最下面)
  6. 这里是自定义 ClassLoader ,但还是在上面的双亲委派机制下的自定义类加载器。(下面会 有一个打破双亲委派机制的案例和应用)

d、自定义类加载器

d盘下的Hello.class
在这里插入图片描述

package com.mashibing.jvm;

public class Hello {
    public Hello() {
    }

    public void m() {
        System.out.println("Hello JVM!");
    }
}

自定义的 ClassLoader

package com.mashibing.jvm.c2_classloader;

import com.mashibing.jvm.Hello;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;

public class T006_MSBClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = new File("d:/test/", name.replace(".", "/").concat(".class"));
        try {
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;

            while ((b=fis.read()) !=0) {
                baos.write(b);
            }

            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();//可以写的更加严谨

            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name); //throws ClassNotFoundException
    }

    public static void main(String[] args) throws Exception {
        ClassLoader l = new T006_MSBClassLoader();
        Class clazz = l.loadClass("com.mashibing.jvm.Hello");
        Class clazz1 = l.loadClass("com.mashibing.jvm.Hello");

        System.out.println(clazz == clazz1);

        Hello h = (Hello)clazz.newInstance();
        h.m();

        System.out.println(l.getClass().getClassLoader());
        System.out.println(l.getParent());

        System.out.println(getSystemClassLoader());
    }
}

在这里插入图片描述

e、加载流程

  1. 从底层加载器到顶级加载器 检查 对应缓存看是否加载过。先进入到自定义类加载器看是否有没有被加载,若没有,则到父加载器APPClassLoader ,若APPClassLoader也没有加载过,则到其父加载器ExtCLassLoader,若ExtClassLoader也没有加载过,则到顶级父类加载器 BootstrapClassLoader ,若也没有加载。则进行下面的加载逻辑
  2. 再从顶级加载器到底层加载器开始 加载 该类。该类BootstrapClassLoader 加载器 看到该类(D:\test\com\mashibing\jvm\Hello.class)不属于自己的加载路径,则交给ExtClassLoader加载器,ExtClassLoader 发现也不属于自己的加载路径,则交给APPClassLoader加载器,但APPClassLoader 加载器发现也不属于自己的加载路径,则交给自定义加载路径。然后加载。
  3. 然后从自定义的 findClass()去定义class并进行返回。
  4. 因为还是双亲委派机制下的自定义类加载器,所以 clazz 加载后,在加载 clazz1 ,后者就是走的前者加载后的缓存机制,也就是双亲委派机制在起作用。所以会输出 TRUE。

5、加密

加密,进行了异或操作。

package com.mashibing.jvm.c2_classloader;

import com.mashibing.jvm.Hello;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class T007_MSBClassLoaderWithEncription extends ClassLoader {

    public static int seed = 0B10110110;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = new File("c:/test/", name.replace('.', '/').concat(".msbclass"));

        try {
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;

            while ((b=fis.read()) !=0) {
                baos.write(b ^ seed);
            }

            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();//可以写的更加严谨

            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name); //throws ClassNotFoundException
    }

    public static void main(String[] args) throws Exception {

        encFile("com.mashibing.jvm.hello");

        ClassLoader l = new T007_MSBClassLoaderWithEncription();
        Class clazz = l.loadClass("com.mashibing.jvm.Hello");
        Hello h = (Hello)clazz.newInstance();
        h.m();

        System.out.println(l.getClass().getClassLoader());
        System.out.println(l.getParent());
    }

    private static void encFile(String name) throws Exception {
        File f = new File("c:/test/", name.replace('.', '/').concat(".class"));
        FileInputStream fis = new FileInputStream(f);
        FileOutputStream fos = new FileOutputStream(new File("c:/test/", name.replaceAll(".", "/").concat(".msbclass")));
        int b = 0;

        while((b = fis.read()) != -1) {
            fos.write(b ^ seed);
        }

        fis.close();
        fos.close();
    }
}

6、编译器

a、编译器描述

java 是解释执行的,class文件load到内存,可以通过java解释器 interpret 解释执行。

java有一个 JIT(just in time)编译器,有某些代码需要编译成为本地代码,相当于exe,java代码也可以编译成为本地代码来执行。

所以java是即可以解释也可以编译。默认是混合模式。

  • 解释器
    • bytecode intepreter
  • JIT
    • Just In-Time compiler :即时编译器。
  • 混合模式
    • 混合使用解释器 + 热点代码编译 (代码先解释执行,发现有一段代码执行了上万次,则编译成本地代码执行)
    • 起始阶段采用解释执行
    • 热点代码检测 (如果确定热点代码呢?
      • 多次被调用的方法(方法计数器:监测方法执行频率
      • 多次被调用的循环(循环计数器:检测循环执行频率
      • 进行编译

在这里插入图片描述

  • -Xmixed 默认为 混合模式

    • 开始解释执行,启动速度较快 对热点代码实行检测和编译
  • -Xint 使用解释模式

    • 启动很快 执行稍慢
  • -Xcomp 使用纯编译模式

    • 执行 很快,启动很慢
  • exe 是 Windows 的本地代码格式,Linux 的本地代码格式是elf

b、小案例

package com.mashibing.jvm.c2_classloader;

public class T009_WayToRun {
    public static void main(String[] args) {
        for(int i=0; i<10_0000; i++)
            m();

        long start = System.currentTimeMillis();
        for(int i=0; i<10_0000; i++) {
            m();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    public static void m() {
        for(long i=0; i<10_0000L; i++) {
            long j = i%3;
        }
    }
}
  • 代码中m()方法重复执行了很多遍。所以会是编译执行
  • 其中的long start = System.currentTimeMillis();会是解释执行
  • 下图是该类运行的配置,红框中是没有参数的,说明是 默认的混合模式(解释+编译),执行大约是4秒左右。
    在这里插入图片描述
    在这里插入图片描述
  • 然后修改配置为解释编译如下,-Xint 是纯解释型。没有内部编译的过程。大约4.5左右。
    在这里插入图片描述
    在这里插入图片描述
  • 改成 编译模式,-Xcomp 大约4秒,显示区别不大,但是类特别多的情况下,区别还是很大的。
    在这里插入图片描述
    在这里插入图片描述

7、JVM的懒加载模式

a、懒加载

  1. 严格讲应该叫 lazyInitializing

  2. JVM 规范并没有规定何时加载。

  3. JVM 严格规定了什么时候必须初始化。(五种情况进行懒加载)

    • new getstatic putstatic invokestatic 指令,访问final变量除外
    • java.lang.reflect 对类进行反射调用时
    • 初始化子类的时候,父类首先初始化
    • 虚拟机启动时,被执行的主类必须初始化
    • 动态语言支持 java.lang.invoke.MethodHandle 解析的结果为
      • REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初 始化
  4. 按需加载

b、 案例代码

package com.mashibing.jvm.c2_classloader;

/**
 * 严格讲应该叫 lazy initialzing,
 * 因为java虚拟机规范并没有严格规定什么时候必须 loading,但严格规定了什么时候 initialzing
 */
public class T008_LazyLoading {
    public static void main(String[] args) throws Exception {
        P p;
        X x = new X();
        System.out.println(P.i);
        System.out.println(P.j);
        Class.forName("com.mashibing.jvm.c2_classloader.T008_LazyLoading$P");

    }

    public static class P {
        final static int i = 8;
        static int j = 9;
        static {
            System.out.println("P");
        }
    }

    public static class X extends P {
        static {
            System.out.println("X");
        }
    }
}

在这里插入图片描述

三、本章总结

1、加载过程

  • Loading

    1. 双亲委派,主要出于安全来考虑
    2. LazyLoading 五种情况
      • –new getstatic putstatic invokestatic指令,访问final变量除外
      • –java.lang.reflect对类进行反射调用时
      • –初始化子类的时候,父类首先初始化
      • –虚拟机启动时,被执行的主类必须初始化
      • –动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化
    3. ClassLoader的源码
      • findInCache -> parent.loadClass -> findClass()
    4. 自定义类加载器
      1. extends ClassLoader
      2. overwrite findClass() -> defineClass(byte[] -> Class clazz)
      3. 加密
      4. 第一节课遗留问题:parent是如何指定的,打破双亲委派,学生问题桌面图片
        1. 用super(parent)指定
        2. 双亲委派的打破
          1. 如何打破:重写loadClass()
          2. 何时打破过?
            1. JDK1.2之前,自定义ClassLoader都必须重写loadClass()
            2. ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定
            3. 热启动,热部署
              1. osgi tomcat 都有自己的模块指定classloader(可以加载同一类库的不同版本)
    5. 混合执行 编译执行 解释执行
    
       1. 检测热点代码:-XX:CompileThreshold = 10000
    
    1. Linking

      1. Verification
        1. 验证文件是否符合JVM规定
      2. Preparation
        1. 静态成员变量赋默认值
      3. Resolution
        1. 将类、方法、属性等符号引用解析为直接引用
          常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
    2. Initializing

      1. 调用类初始化代码 ,给静态成员变量赋初始值

2、小总结:

  1. load - 默认值 - 初始值
  2. new - 申请内存 - 默认值 - 初始值

四、打破双亲委派机制-热部署应用

1、双亲委派机制案例

a、自定义类加载器:T006_MSBClassLoader

package com.mashibing.jvm.c2_classloader;

import com.mashibing.jvm.Hello;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;

public class T006_MSBClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = new File("d:/test/", name.replace(".", "/").concat(".class"));
        try {
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;

            while ((b=fis.read()) !=0) {
                baos.write(b);
            }

            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();//可以写的更加严谨

            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name); //throws ClassNotFoundException
    }

    public static void main(String[] args) throws Exception {
        ClassLoader l = new T006_MSBClassLoader();
        Class clazz = l.loadClass("com.mashibing.jvm.Hello");
        Class clazz1 = l.loadClass("com.mashibing.jvm.Hello");

        System.out.println(clazz == clazz1);

        Hello h = (Hello)clazz.newInstance();
        h.m();

        System.out.println(l.getClass().getClassLoader());
        System.out.println(l.getParent());

        System.out.println(getSystemClassLoader());
    }
}

在这里插入图片描述

b、双亲委派机制代码

package com.mashibing.jvm.c2_classloader;

public class T011_ClassReloading1 {
    public static void main(String[] args) throws Exception {
        T006_MSBClassLoader msbClassLoader = new T006_MSBClassLoader();
        Class clazz = msbClassLoader.loadClass("com.mashibing.jvm.Hello");

        msbClassLoader = null;
        System.out.println(clazz.hashCode());

        msbClassLoader = null;

        msbClassLoader = new T006_MSBClassLoader();
        Class clazz1 = msbClassLoader.loadClass("com.mashibing.jvm.Hello");
        System.out.println(clazz1.hashCode());

        System.out.println(clazz == clazz1);
    }
}

在这里插入图片描述
解析:
因为自定义类加载器时,重写的是 findClass() 方法,所以执行的还是双亲委派机制。所以第二次 msbClassLoader = new T006_MSBClassLoader();的时候,先进行查找是否加载过,加载过的话就找到直接返回。
因为第一次new加载过了,所以第二次new就不用加载了,找到直接返回即可。
所以打印的两个hashcode值是一样的,而且返回TRUE。

2、打破双亲委派机制

这里重写的是 loadClass() 方法,所以就双亲委派机制就被我们给替代了(因为源代码中,双亲委派机制是在 loadClass() 方法中的,这里重新了,需要我们自定义加载机制了,所以这里就可以打破双亲委派机制啦 )。

package com.mashibing.jvm.c2_classloader;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class T012_ClassReloading2 {
    private static class MyLoader extends ClassLoader {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {

            File f = new File("d:/test/" + name.replace(".", "/").concat(".class"));

            if(!f.exists()) return super.loadClass(name);

            try {

                InputStream is = new FileInputStream(f);

                byte[] b = new byte[is.available()];
                is.read(b);
                return defineClass(name, b, 0, b.length);
            } catch (IOException e) {
                e.printStackTrace();
            }

            return super.loadClass(name);
        }
    }

    public static void main(String[] args) throws Exception {
        MyLoader m = new MyLoader();
        Class clazz = m.loadClass("com.mashibing.jvm.Hello");

        m = new MyLoader();
        Class clazzNew = m.loadClass("com.mashibing.jvm.Hello");

        System.out.println(clazz == clazzNew);
    }
}

在这里插入图片描述

解析:
首先自定义类加载器时,这里重写的是 loadClass,所以就不会走源代码写死的双亲委派机制。

所以这里加载的逻辑是:如果类文件不存在则交给父类加载器去加载,但是一旦类文件存在,则直接加载到内存。所以就会出现,只要是自己的类文件则一定会加载。

所以这里返回的FALSE,因为 两次new 都是重新加载到内存的

所以热部署的应用也是依靠这个打破双亲委派机制而进行改造的。

相关文章
|
3月前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
105 35
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
2月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
58 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
3月前
|
Arthas Java 测试技术
JVM —— 类加载器的分类,双亲委派机制
类加载器的分类,双亲委派机制:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器;JDK8及之前的版本,JDK9之后的版本;什么是双亲委派模型,双亲委派模型的作用,如何打破双亲委派机制
JVM —— 类加载器的分类,双亲委派机制
|
25天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
200 1
|
2月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
40 4
|
14天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
23天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
24天前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
19 3
|
25天前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
45 1
|
1月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。