jvm系列-02jvm的类加载子系统以及jclasslib的基本使用

Posted huisheng_qaq

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jvm系列-02jvm的类加载子系统以及jclasslib的基本使用相关的知识,希望对你有一定的参考价值。

JVM系列整体栏目


内容链接地址
【一】初识虚拟机与java虚拟机https://blog.csdn.net/zhenghuishengq/article/details/129544460
【二】jvm的类加载子系统以及jclasslib的基本使用https://blog.csdn.net/zhenghuishengq/article/details/129610963
【三】运行时私有区域之虚拟机栈、程序计数器、本地方发栈https://blog.csdn.net/zhenghuishengq/article/details/129684076

深入理解jvm的类加载器子系统

1,jvm的内存结构

在jvm的内存中结构中,其主要结构如下。

在jvm内部,需要将磁盘上的字节码文件通过这个类加载加载到内存中。在类加载子系统中,也需要经过一定的阶段将才能将这个文件加载到内存的运行时数据区中,如一些加载,验证,准备,解析,初始化等工作。在加载到运行时数据区之后,内部主要由一些共享的方法区、堆,以及私有的程序计数器、虚拟机栈、本地方法栈这些。这些字节码最终是需要通过执行引擎去执行的,执行引擎中主要包括解释器,JIT即时编译器,垃圾回收器等。

2,类加载器加载过程

在类加载器子系统中,主要会经过加载,链接和初始化三个阶段,链接又包括验证,准备和解析三个阶段,所以合起来就是加载,验证,准备,解析,初始化五个阶段。

类加载器主要负责从文件系统或者网络中加载Class文件,并且类加载器只负责将文件加载,至于是否可以运行,还得由Execution Engine执行引擎决定。

2.1,加载阶段

加载阶段的加载器主要有引导类加载器,扩展类加载器,系统类加载器和自定义类加载器,主要是通过一个类的全限定名获取此类的二进制字节流,然后将这个字节流所代表的静态存储结构转化为方法区运行时的数据结构,然后在内存中生成一个java.lang.Class文件,作为方法区这个类的各种数据的访问入口。其主要就是将文件加载出来

常见的类加载方式有以下几种方式

  • 从本地系统直接加载
  • 从网络中获取
  • 从压缩包中获取,如zip
  • 运行时生成,如动态代理
  • 其他文件生成,典型的场景有:JSP应用
  • 数据库中获取 .class文件
  • 从加密文件中获取
  • 反射,序列化,克隆等

2.2,链接阶段

链接阶段又可以分为三个阶段,分别是验证,准备和解析

2.2.1,验证

验证的主要目的在于确保Class文件的字节流中所包含的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全,相当于一种自我保护。如果编译器发现了有违法的信息之后,则编译器可以选择直接抛出异常或者拒绝编译。

主要包括四种验证:文件格式验证,元数据验证,字节码验证和符号引用验证。

2.2.2,准备

在准备阶段为类分配内存,并且设置该类的变量默认初始值,如整型的初始值为0。

public static int x = 10;  //在准备阶段赋值默认值为0,并且分配内存
public static void main(String[] args) 
    System.out.println(j);

这里主要是为变量进行一个默认的初始赋值,如果变量被static final修饰,那么这个变量会被变为常量,并且会在编译阶段就会分配内存。同时这里也不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。

2.2.3,解析

就是将常量池内的符号引用转化为直接引用的过程,并随着JVM在执行完初始化之后再执行。

符号引用:以一组符号来描述引用的目标,只要能无歧义的定位到目标即可
直接引用: 相当于寻址的直接指针或者句柄

解析动作主要针对接口,类,字段,类方法,接口方法,方法类型,句柄和调用点限定符

2.3,初始化阶段(重点)

2.3.1,jclasslib的安装

在查看字节码文件之前,也可以在idea中安装查看对应的字节码指令的插件,在插件中搜索jclasslib即可,安装完成之后需要restart重启。

在安装完成之后,可以在view的位置来打开这个Bytecode字节码文件。

在点击这个Show Bytecode With Jclasslib 之后,就会出现以下的界面,会有一些版本,协议号,当前类,父类,接口数,文件数,方法数,属性数等。

2.3.2,clinit

初始化阶段就是执行类构造器方法()的过程,通过javac编译器自动收集类中的所有类变量赋值动作和静态代码块中的语句合并而来的。就是说这个clinit会将类变量的显示的初始化和静态代码块的初始化合并到一起,如果没有类变量的赋值操作或者静态代码块的赋值操作,那么这个clinit就不会出现在字节码文件中。

并且在整个流程中,变量的初始赋值是在这个准备阶段,而真正的赋值是在这个初始化阶段。

public static int x = 10;  //当前阶段中此时x的值为10

在这个Methods中,可以看到给这个类变量赋值,是有这个clinit的

或者再静态代码块中给类变量赋值,也是可以有这个clinit的,可以看下图右边Methods中的第二点。

如果该类具有父类,那么JVM会保证先加载父类的 ,再加载子类的 。并且在多线程中,虚拟机会保证一个类的 方法会加同步锁

2.3.3,init

在每个类中,都会有一个隐示的构造方法或者显示的构造方法,通过 来进行初始化。如在以下的代码中,显示的写了一个代码的构造器,先将初始值加载,或者再加载构造器里面的值。

3,类加载器

3.1,类加载器的分类

在加载阶段中,主要有引导类加载器,扩展类加载器,应用程序类加载器和自定义加载器。在jvm中,规定支持两种类加载器,分別是引导类加载器和自定义类加载器,而扩展类和系统类都是属于自定义类加载器。

并且在这几个来加载器中,这个引导类加载器是用c语言写的,而其他的类加载器都是使用这个JAVA语言写的。接下来通过代码查看一下这个类加载器,也可以发现这个引导类加载器不是java语言写的,所以获取不到,并且这个自定义类的加载器是通过系统类加载器来加载的。

//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(systemClassLoader);

//获取上层扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
//sun.misc.Launcher$ExtClassLoader@15615099
System.out.println(extClassLoader);

//获取上层引导类加载器
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
//null 尝试获取失败,该类由c语言编写
System.out.println(bootStrapClassLoader);

//获取自定义类类加载器,以当前类为例
ClassLoader classLoader = ClassLoad.class.getClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2 
// 可以发现当前自定义类的了地价在其为系统类加载器
System.out.println(classLoader);

而像一些系统的核心类库,如String这种,是通过引导类加载器加载的。并且该加载器作为扩展类和系统类加载器的父类加载器,该加载器主要加载包名为java,javax,sun等开头的类

ClassLoader StringClassLoader = String.class.getClassLoader();
System.out.println(StringClassLoader);  //null

接下来可以获取一下这个引导类中,加载的全部内容

//获取引导类加载器可以加载的全部url
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urLs.length; i++) 
    System.out.println(urLs[i]);

3.2,自定义类加载器的场景

一般情况使用引导类,扩展类和系统类是可以满足日常的开发需求的,但是在必要时,也可以手动自定义其他的类加载器。

引入自定义类加载器的原因

  • 隔离加载类
  • 修改类加载方式
  • 扩展加载源
  • 防止源码泄漏

自定义类加载器的实现步骤

  • 1,可以通过继承抽象类 java.lang.ClassLoader 类,实现自定义类加载器
  • 2,重写findClass()方法,然后将逻辑写在方法内部
  • 3,如果没有特别复杂的要求,可以直接继承URLClassLoader类

获取ClassLoader的途径

  • 获取当前类的ClassLoader:clazz.getClassLoader()
  • 获取上下文线程方式:Thread.currentThread.getContextClassLoader()
  • 获取系统的ClassLoader:ClassLoader.getSystemClassLoader()
  • 获取调用者的ClassLoader:DriverManager.getCallerClassLoader()

3.3,双亲委派机制

在jvm中,对class文件采用的是按需加载的方式,也就是说在需要使用到该类时才会加载,然后将class文件加载到内存生成class对象。并且java虚拟机采用的是一种双亲委派机制模式

其工作原理如下:

  • 1,如果一个类加载器收到了加载请求,他并不会自己去加载,而是将这个请求委托给父类加载器去执行
  • 2,如果父加载器还有其他的父加载器,那么会进一步的向上委托,一次递归到顶点
  • 3,如果父类可以完成任务,则将值返回;反之,则由子类尝试去加载


其源码如下

// 检查当前类加载器是否已经加载了该类
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);

通过源码也可以知道:
1,首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。

2,如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。

3,如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。ClassLoader的loadClass方法,里面实现了双亲委派机制。

双亲委派机制的好处

1,沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
2,避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

3.4,自定义类加载器

需要继承ClassLoader类,并且重写里面的findClass()方法。可以在本地磁盘里面创建一个类,如何加载的时候直接通过本地磁盘加载,而不需要使用到那几个类加载器加载,这样就完成了自定义类的加载器

import java.io.FileInputStream;
import java.lang.reflect.Method;

/**
 * @Author: zhenghuisheng
 * @Date: 2023/3/16 23:09
 */
public class MyClassLoaderTest 
    static class MyClassLoader extends ClassLoader 
    private String classPath;
    public MyClassLoader(String classPath) 
            this.classPath = classPath;
    

    private byte[] loadByte(String name) throws Exception 
        name = name.replaceAll("\\\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
    
    protected Class<?> findClass(String name) throws ClassNotFoundException 
        try 
             byte[] data = loadByte(name);
             //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
             return defineClass(name, data, 0, data.length);
              catch (Exception e) 
             e.printStackTrace();
             throw new ClassNotFoundException();
            
        
    

    /**
     * 下面的磁盘路径需要手动创建
     * @param args
     * @throws Exception
     */
    public static void main(String args[]) throws Exception 
        //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        //D盘创建 test/com/zhenghuisheng/jvm 几级目录,将User类的复制类User.class丢入该目录
        //需要创建一个User类在这个路径下
        Class clazz = classLoader.loadClass("com.zhenghuisheng.jvm.User");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    

3.5,打破双亲委派模型

应用程序里面有这个类,磁盘里面也有这个自定义的类,直接通过磁盘一次性加载,不需要利用到父类加载器,这样就打破了双亲委派机制。主要就是通过重写里面的findClass()方法,将里面的双亲委派机制的逻辑修改即可。让这个findClass直接找磁盘里面的路径,而不需要再写那些层层找父加载器加载即可。

3.6,其他

JVM中的两个class对象是否为同一个类

  • 类的完整名必须一致
  • 加载这个类的ClassLoader必须相同

在jvm中,即使这两个对象来源于同一个Class文件,被同一个虚拟机所加载,但只要加载他们的ClassLoader实例对象不一致,那么这两个类对象也是不相等的。

JVM的类加载器,以及双亲委派模型都是什么?

文章目录

类加载器

Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)

双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间

这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

双亲委派模型

自JDK1.2以来,Java一直保持着三层类加载器、双亲委派的类加载架构。

对于这个时期的Java应用,绝大多数Java程序都会使用到以下3个系统提供的类加载器来进行加载。

启动类加载器(Bootstrap Class Loader)

启动类加载器(Bootstrap Class Loader):这个类加载器负责加载存放在<JAVA_HOME>\\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中

启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可。

扩展类加载器(Extension Class Loader)

扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。

它负责加载<JAVA_HOME>\\lib\\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库

根据“扩展类加载器”这个名称,就可以推断出这是一种Java系统类库的扩展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,在JDK 9之后,这种扩展机制被模块化带来的天然的扩展能力所取代。由于扩展类加载器是由Java代码实现的,开发者可以直接在程序中使用扩展类加载器来加载Class文件。

应用程序类加载器(Application Class Loader)

应用程序类加载器(Application Class Loader):这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。

它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器

如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

这些类加载器之间的协作关系“通常”会如下图所示:

图中展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型”(Parents Delegation Model)。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

双亲委派模型工作过程

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载

使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系

例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

双亲委派源码追踪

双亲委派模型对于保证Java程序的稳定运作极为重要,但它的实现却异常简单,用以实现双亲委派的代码只有短短十余行,全部集中在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();
                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;
    

首先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败,抛出ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。

以上是关于jvm系列-02jvm的类加载子系统以及jclasslib的基本使用的主要内容,如果未能解决你的问题,请参考以下文章

jvm系列-04精通运行时数据区共享区域---堆

jvm系列-05精通运行时数据区共享区域---方法区

jvm系列学习之--Java的类加载机制

jvm系列-03精通运行时数据区私有区域---虚拟机栈程序计数器本地方法栈

一文理解JVM的类加载系统

JVM的类加载器,以及双亲委派模型都是什么?