1.1 jvm核心类加载器--jdk源码剖析

前端之家收集整理的这篇文章主要介绍了1.1 jvm核心类加载器--jdk源码剖析前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

目录

前提: 运行环境

1. 类加载的过程

  1.1 类加载器初始化的过程

  1.2 类加载的过程

  1.3 类的懒加载

2. jvm核心类加载器

3. 双亲委派机制

4. 自定义类加载器

5. tomcat类加载机制


 运行环境:

我是在mac上操作的. 先找到mac的java地址. 从~/.bash_profile中可以看到

java的home目录是: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home

 


一. 类加载的过程

1.1 类加载器初始化的过程

假如现在有一个java类 com.lxl.jvm.Math类,里面有一个main方法,

package com.lxl.jvm;

public class Math {
    static int initData = 666;
    static User user = new User();

    int compute() {
        int a = 1;
        int b = 2int c = (a + b) * 10return c;
    }

    void main(String[] args) {
        Math math =  Math();
        math.compute();
    }
}

这个方法很简单,通常我们直接执行main方法就ok,可以运行程序了,那么点击运行main方法,整个过程是如何被加载的呢? 为什么点击执行main,就能得到结果呢?

先来看看答题的类加载流程,如下图:

 备注:

1. windows上的java启动程序是java.exe,mac下,就是java

2. c语言部分,我们做了解,java部门是需要掌握的部分.

 

第一步: java调用底层的jvm.dll文件创建java虚拟机(这一步由C++实现) . 这里java是c++写的代码,调用的jvm.dll也是c++底层的一个函数. 通过调用jvm.dll文件,会创建java虚拟机. java虚拟机的启动都是c++程序实现的. dll文件相当于java的jar包. 开启虚拟机以后,会做哪些操作呢? 看第二步

第二步:在启动虚拟机的过程中,会创建一个引导类加载器的实例. 这个引导类的加载器是C语言实现的. 然后jvm虚拟机就启动起来了. jvm启动起来了,记下来干嘛呢?

第三步: 接下来,C++语言会调用java的启动程序.刚刚只是创建了java虚拟机,java虚拟机里面还有很多启动程序. 其中有一个程序叫做Launcher. 类全称是sun.misc.Launcher. 通过启动这个java类,会由这个类引导加载器加载并创建很多其他的类加载器. 而这些加载器才是真正启动并加载磁盘上的字节码文件.

第四步:真正的去加载本地磁盘的字节码文件,然后启动执行main方法.

第五步:main方法执行完毕,引导类加载器会发起一个c++调用,销毁JVM

 

以上就是启动一个main方法,这个类加载的全部过程

 

下面,我们重点来看一下,我们的类com.lxl.Math是怎么被加载到java虚拟机里面去的?  

1.2 类加载的过程

上面的com.lxl.jvm.Math类最终会生成clas字节码文件. 字节码文件是怎么被加载器加载到JVM虚拟机的呢?

类加载有五步:加载,验证,准备,解析,初始化. 那么这五步都是干什么的呢?我们来看一下

我们的类在哪里呢? 在磁盘里(比如: target文件夹下的class文件),我们先要将class类加载到内存中. 加载到内存区域以后,不是简简单单的转换成二进制字节码文件,他会经过一系列的过程. 比如: 验证,初始化等. 把这一些列的信息转变成内元信息,放到内存里面去. 我们来看看具体的过程

 

 

 

第一步: 加载.  将class类加载到java虚拟机的内存里去,在加载到内存之前,会有一些列的操作,第一个是验证字节码,比如:打开一个字节码文件

 

 

 打眼一看,感觉像是乱码,实际上不是的. 其实,这里面每个字符串都有对应的含义. 那么文件里面的内容我们能不能替换呢?当然不能,一旦替换,就不能执行成功了. 所以,第一步:验证,验证什么呢?

验证字节码加载是否正确: 格式是否正确. 验证,验证的就是里面的内容,是否符合java虚拟机的规范. 验证完了,接下来是准备. 准备干什么呢? 比如我们的类Math,他首先会给Math里的静态变量赋值一个初始值. 比如我们Math里有两个静态遍历

;
new User();

在准备的过程中,就会给这两个变量赋初始值,这个初始值并不是真实的值,比如initData的初始值是0. 如果是boolean类型,就赋值为false.  也就是说,准备阶段赋的值是jvm固定的,不是我们定义的值.如果一个final的常量,比如public static final int name="zhangsan",那么他在初始化的时候,是直接赋初始值"zhangsan"的. 这里只是给静态变量赋初始值

记下来说说解析的过程. 解析的过程略微复杂,解析是将"符号引用"转变为直接引用. 

什么是符号引用呢?

比如我们的程序中的main方法. 写法是固定的,我们就可以将main当成一个符号. 比如上面的initData,int,static,我们都可以将其称之为符号,java虚拟机内部有个专业名词,把他叫做符号. 这些符号被加载到内存里都会对应一个地址. 将"符号引用"转变为直接引用,指的就是,将main,initData,int等这些符号转变为对应的内存地址. 这个地址就是代码的直接引用. 根据直接引用的值,我们就可以知道代码在什么位置.然后拿到代码去真正的运行. 

将符号引用转变为"内存地址",这种有一个专业名词,叫静态链接. 上面的解析过程就相当于静态链接的过程. 类加载期间,完成了符号到内存地址的转换. 有静态链接,那么与之对应的还有动态链接

什么是动态链接呢?

 Math();
        math.compute();
    }

比如:上面这段代码,只有当我运行到math.compute()这句话的时候,才回去加载compute()这个方法. 也就是说,在加载的时候,我不一定会把compute()这个方法解析成内存地址. 只有当运行到这行代买的时候,才会解析.

我们来看看汇编代码

javap -v Math.class

 

Classfile /Users/luoxiaoli/Downloads/workspace/project-all/target/classes/com/lxl/jvm/Math.
  Last modified 2020-6-27; size 777 bytes
  MD5 checksum a6834302dc2bf4e93011df4c0b774158
  Compiled from "Math.java"
 com.lxl.jvm.Math
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC,ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#35         // java/lang/Object."<init>":()V
   #2 = Class              #36             com/lxl/jvm/Math
   #3 = Methodref          #2.# com/lxl/jvm/Math."<init>":()V
   #4 = Methodref          #37          com/lxl/jvm/Math.compute:()I
   #5 = Fieldref           #38          com/lxl/jvm/Math.initData:I
   #6 = Class              #39             com/lxl/jvm/User
   #7 = Methodref          #6.# com/lxl/jvm/User."<init>":()V
   #8 = Fieldref           #40          com/lxl/jvm/Math.user:Lcom/lxl/jvm/User;
   #9 = Class              #41             java/lang/Object
  #10 = Utf8               initData
  #11 = Utf8               I
  #12 = Utf8               user
  #13 = Utf8               Lcom/lxl/jvm/User;
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               LocalVariableTable
  #19 = Utf8               this20 = Utf8               Lcom/lxl/jvm/Math;
  #21 = Utf8               compute
  #22 = Utf8               ()I
  #23 = Utf8               a
  #24 = Utf8               b
  #25 = Utf8               c
  #26 = Utf8               main
  #27 = Utf8               ([Ljava/lang/String;)V
  #28 = Utf8               args
  #29 = Utf8               [Ljava/lang/String;
  #30 = Utf8               math
  #31 = Utf8               MethodParameters
  #32 = Utf8               <clinit>33 = Utf8               SourceFile
  #34 = Utf8               Math.java
  #35 = NameAndType        #14:#15         "<init>":()V
  #36 = Utf8               com/lxl/jvm/Math
  #37 = NameAndType        #21:#22         compute:()I
  #38 = NameAndType        #10:#11         initData:I
  #39 = Utf8               com/lxl/jvm/User
  #40 = NameAndType        #12:#13         user:Lcom/lxl/jvm/User;
  #41 = Utf8               java/lang/Object
{
   initData;
    descriptor: I
    flags: ACC_PUBLIC,ACC_STATIC

  static com.lxl.jvm.User user;
    descriptor: Lcom/lxl/jvm/User;
    flags: ACC_PUBLIC,1)">public com.lxl.jvm.Math();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1,locals=1
         : aload_0
         1: invokespecial #1                   Method java/lang/Object."<init>":()V
         4: 
      LineNumberTable:
        line 3: 
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lxl/jvm/Math;

   compute();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2,1)">4,1)">: iconst_1
         : istore_1
         : iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        : istore_3
        11: iload_3
        12: ireturn
      LineNumberTable:
        line 8: 
        line 9: 10: 11: 0      13     Math;
            2      11          a   I
            4       9          b   I
           11       2          c   I

   main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC,ACC_STATIC
    Code:
      stack=0: new           #2                   class com/lxl/jvm/Math
         : dup
         4: invokespecial #3                   Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                   Method compute:()I
        : pop
        13: 15: 16: 17: 1314     0  args   [Ljava/lang/String;
            8       6     1  math   Lcom/lxl/jvm/Math;
    MethodParameters:
      Name                           Flags
      args

   {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=0,1)">0
         0: sipush        666
         3: putstatic     #5                   Field initData:I
         6: 6                   class com/lxl/jvm/User
         : dup
        10: invokespecial #7                   Method com/lxl/jvm/User."<init>":()V
        13: putstatic     #8                   Field user:Lcom/lxl/jvm/User;
        16: 4: 5: 
}
SourceFile: "

使用这个指令,就可以查看Math的二进制文件. 其实这个文件,就是上面那个二进制代码文件.

看看这里面有什么东西?

类的名称,大小,修改时间,大版本,小版本,访问修饰符等等

 Last modified 52

还有一个Constant pool 常量池. 这个常量池里面有很多东西. 我们重点看中间哪一行. 第一列表示一个常量的标志符,这个标识符可能在其他地方会用到.  第二列就表示常量内容

Constant pool:
   #21 = Utf8               compute

这些标识符在后面都会被用到,比如main方法

 new           #2                  4: invokespecial #3                  9: invokevirtual #4                  Math;
    MethodParameters:
      Name                           Flags
      args

这里面就用到了#2 #3 #4,这都是标识符的引用.

第一句: new了一个Math(). 我们看看汇编怎么写的?

          class com/lxl/jvm/Math

new + #2. #2是什么呢? 去常量池里看,#2代表的就是Math类

   # com/lxl/jvm/Math

 

这里要说的还是math.compute()这个方法,不是在类加载的时候就被加载到内存中去了,而是运行main方法的时候,执行到这行代码才被加载进去,这个过程叫做动态链接

类加载的时候,我们可以把"解析"理解为静态加载的过程. 一般像静态方法(例如main方法),获取其他不变的静态方法会被直接加载到内存中,因为考虑到性能,他们加载完以后就不会变了,就直接将其转变为在内存中的代码位置.  

 而像math.compute()方法,在加载过程中可能会变的方法(比如compute是个多态,有多个实现),那么在初始化加载的时候,我们不会到他会调用谁,只有到运行时才能知道代码的实现,所以在运行的时候在动态的去查询他在内存中的位置,这个过程就是动态加载

 

下一个步骤: 初始化: 对类的静态变量初始化为指定的值. 执行静态代码块. 比如代码

666;

在准备阶段将其赋值为0,而在初始化阶段,会将其赋值为设定的666  

 

  

2. 验证

 

3. 准备

 

 

4. 解析

 

 

5. 初始化

 

 1.3 类的懒加载

 类被加载到方法区中以后,主要包含:运行时常量池,类型信息,字段信息,方法信息,类加载器的引用,对应class实例的引用等信息.

什么意思呢? 就是说,当一个类被加载到内存,这个类的常量,有常量名,类型,域信息等; 方法方法名,返回值类型,参数类型,方法作用域等符号信息都会被加载放入不同的区域.

注意: 如果主类在运行中用到其他类,会逐步加载这些类,也就是说懒加载. 用到的时候才加载.

 TestDynamicLoad {
     {
        System.out.println(********Dynamic load class**************");
    }

    public static void main(String[] args) {
        new A();
        System.out.println("*********load test*****************");
        B b = null; // 这里的b不会被加载,除非new B();
    }

}

class A {
    ********load A**************);
    }

     A(){
        System.********initial A**************);
    }
}


 B {
    ********load B************** B(){
        System.********initial B**************);
    }
}

这里定义了两个类A和B,当使用到哪一个的时候,那个类才会被加载,比如:main方法中,B没有被用到,所以,他不会被加载到内存中.

 运行结果

********Dynamic load class**************
********load A**************
********initial A**************
*********load test*****************

总结几点如下:

1. 静态代码块在构造方法之前执行

2. 没有被真正使用的类不会被加载

 

 

二. jvm核心类加载器

 类主要通过类加载器来加载,java里面有如下几种类加载器

1. 引导类加载器:负责加载支撑JVM运行的,位于jre目录的lib目录下的核心类. 比如:rt.jar,charset.jar等,

2. 扩展类加载器: 负责加载支撑JVM运行的,位于jre目录的lib/ext扩展目录中的jar包

3. 应用程序类加载器: 负责加载classPath路径下的类包,主要加载自己写的类

4. 自定义类加载器: 负责加载用户自定义路径下的类包

  引导类加载器是由C++帮我们实现的,然后c++语言会通过一个Launcher类将扩展类加载器(ExtClassLoader)和应用程序类加载器(AppClassLoader)构造出来,并且把他们之间的关系构建好. 

例1: 

package com.lxl.jvm;

import sun.misc.Launcher;

import java.net.URL;

 TestJDKClassLoader {

     main(String[] args) {
        /**
         * 第一个: String 是jdk自身自带的类,他的类加载器是引导类加载器
         * 第二个: 加密类的classloader,这是jdk扩展包的一个类
         * 第三个: 是我们当前自己定义的类,会被应用类加载器加载
         */
        System.out.println(String..getClassLoader());
        System.out.println(com.sun.crypto.provider.DESKeyFactory..getClassLoader().getClass().getName());
        System.out.println(TestJDKClassLoader..getClassLoader().getClass().getName());
    }
}

我们来看这个简单的代码,运行结果:

null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
 第一个: String 是jdk自身自带的类,他的类加载器是引导类加载器
 第二个: 加密类的classloader,这是jdk扩展包的一个类,jdk扩展包里面使用的是extClassLoader类加载器加载的
 第三个: 是我们当前自己定义的类,会被AppClassLoader应用程序加载器加载.

我们看到ExtClassLoader和AppClassLoader都是Launcher类的一部分. 那Launcher类是什么东西呢?

上面有提到,Launcher类是jvm启动的时候由C++调用启动的一个类. 

 

那么,第一个bootstrap引导类加载器,那引导类加载器返回的为什么是null呢? 

因为bootstrap引导类加载器,他不是java的对象,他是c++生成的对象,所以这里是看不到的

 

 例2: ExtClassLoader和AppClassLoader是怎么生成的?

 从这个图中我们可以看出,C++调用java创建JVM启动器,其中一个启动器是Launcher,他实际是调用了sun.misc.Launcher的getLauncher()方法. 那我们就从这个方法入手看看到底是如何运行的?

我们看到Lanucher.java类是在核心的rt.jar包里的

我们看到getLauncher()类直接返回了launcher. 而launcher是一个静态对象变量,这是一个单例模式

C++通过getLauncher()-->直接返回了lanucher对象,而launcher对象是在构建类的时候就已经初始化好了. 那么,初始化的时候做了哪些操作呢?接下来看看他的构造方法.

 

 在构造方法里,首先定义了一个ExtClassLoader. 这是一个扩展类加载器,扩展类加载器调用的是getExtClassLoader(). 接下来看一看getExtClassLoader这个方法做了什么?

 

 在这里,判断当前对象是否初始化过,如果没有,那么就创建一个ExtClassLoader()对象,看看createExtClassLoader()这个方法做了什么事呢?

 

 doPrivileged是一个权限校验的操作,我们可以先不用管,直接看最后一句,return new Launcher.ExtClassLoader(var1). 直接new了一个ExtClassLoader,其中参数是var1,代表的是ext扩展目录下的文件.

 

 

在ExtClassLoader(File[] var1)这个方法中,  这里第一步就是调用父类的super构造方法. 而ExtClassLoader继承了谁呢? 我么你可以看到他继承了URLClassLoader.

 

 而URLClassLoader是干什么用的呢? 其实联想一下大概能够猜数来,这里有一些文件路径,通过文件路径加载class类.

我们继续看调用的super(parent),我们继续往下走,就会看到一下内容

 

 这里设置了ExtClassLoader的parent是谁? 注意看的话,我们发现,ExtClassLoader的parent类是null. 

 

 

 这就是传递过来的parent类加载器,那么这里的parent类加载器为什么是null呢? 因为,ExtClassLoader的父类加载器是谁呢? 他是Bootstrap ClassLoader. 而BootStrap ClassLoader是C++的类加载器,我们不能直接调用它,设置为null.

 

其实,ExtClassLoader在初始化阶段就是调用了ExtClassLoader方法,初始化了ExtClassLoader类

 

接下来,我们回到Launcher的构造方法,看看Launcher接下来又做了什么?

 

 可以看到,接下来调了AppClassLoader的getAppClassLoader(var1),这个方法. 需要注意一下的是var1这个参数. var1是谁呢? 向上看,可以看到var1是ExtClassLoader.

 

 这是AppClassLoader,应用程序类加载器,这个类是加载我们自己定义的类的类加载器. 他也是继承自URLClassLoader. 

我们来看看getAppClassLoader(final ClassLoader var0)方法. 这个方法的参数就是上面传递过来的ExtClassLoader

这里第一句话就是获取当前项目的class 文件路径,然后将其转换为URL. 并调用了Launcher.AppClassLoader(var1x,var0),其中var1x是class类所在的路径集合,var0是扩展的类加载器ExtClassLoader,接下来,我们进入到这个方法里看一看

 

AppClassLoader直接调用了其父类的构造方法,参数是class类路径集合,和ExtClassLoader

 

 

 

 最后,我们看到,将ExtClassLoader传递给了parent变量. 这是定义在ClassLoader中的属性,而ClassLoader类是所有类加载器的父类. 因此,我们也可以看到AppClassLoader的父类加载器是ExtClassLoader 

 

同时,我们也看到了,C++在启动JVM的时候,调用了Launcher启动类,这个启动类同时加载了ExtClassLoader和AppClassLoader.

 

 main(String[] args) {
        
  ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
  ClassLoader extClassLoader = appClassLoader.getParent();
  ClassLoader bootstrapClassLoad = extClassLoader.getParent();


  System.bootstrap class loader: " + bootstrapClassLoad);
  System.ext class loader  extClassLoader);
  System.app class loader "+ appClassLoader);
}

通过这个demo,我们也可以看出,appClassLoader的父类是extClassLoader,extClassLoader的父类是bootstrapClassLoader

输出结果:

bootstrap class loader: 
ext  loader sun.misc.Launcher$ExtClassLoader@2a84aee7
app class loader sun.misc.Launcher$AppClassLoader@18b4aac2 

 

 

原文链接:/jvm/997183.html

猜你在找的JVM相关文章