jvm核心类加载器--jdk源码剖析 (上)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 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

1187916-20200627180251652-1130784281.png

一. 类加载的过程


1.1 类加载器初始化的过程


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


package com.lxl.jvm;
public class Math {
    public static int initData = 666;
    public static User user = new User();
    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }
    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }
}


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

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


1187916-20200627191708469-1781924332.png


备注:


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类加载到内存中. 加载到内存区域以后, 不是简简单单的转换成二进制字节码文件,他会经过一系列的过程. 比如: 验证, 准备, 解析, 初始化等. 把这一些列的信息转变成内元信息, 放到内存里面去. 我们来看看具体的过程

 1187916-20200627213226895-792217364.png

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

image.png


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


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


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

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


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

什么是符号引用呢?


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

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


什么是动态链接呢?


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

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


我们来看看汇编代码

javap -v Math.class
Classfile /Users/luoxiaoli/Downloads/workspace/project-all/target/classes/com/lxl/jvm/Math.class
  Last modified 2020-6-27; size 777 bytes
  MD5 checksum a6834302dc2bf4e93011df4c0b774158
  Compiled from "Math.java"
public class 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.#35         // com/lxl/jvm/Math."<init>":()V
   #4 = Methodref          #2.#37         // com/lxl/jvm/Math.compute:()I
   #5 = Fieldref           #2.#38         // com/lxl/jvm/Math.initData:I
   #6 = Class              #39            // com/lxl/jvm/User
   #7 = Methodref          #6.#35         // com/lxl/jvm/User."<init>":()V
   #8 = Fieldref           #2.#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               this
  #20 = 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
{
  public static int initData;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC
  public static com.lxl.jvm.User user;
    descriptor: Lcom/lxl/jvm/User;
    flags: ACC_PUBLIC, ACC_STATIC
  public com.lxl.jvm.Math();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lxl/jvm/Math;
  public int compute();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn
      LineNumberTable:
        line 8: 0
        line 9: 2
        line 10: 4
        line 11: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Lcom/lxl/jvm/Math;
            2      11     1     a   I
            4       9     2     b   I
           11       2     3     c   I
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/lxl/jvm/Math
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method compute:()I
        12: pop
        13: return
      LineNumberTable:
        line 15: 0
        line 16: 8
        line 17: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  args   [Ljava/lang/String;
            8       6     1  math   Lcom/lxl/jvm/Math;
    MethodParameters:
      Name                           Flags
      args
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: sipush        666
         3: putstatic     #5                  // Field initData:I
         6: new           #6                  // class com/lxl/jvm/User
         9: dup
        10: invokespecial #7                  // Method com/lxl/jvm/User."<init>":()V
        13: putstatic     #8                  // Field user:Lcom/lxl/jvm/User;
        16: return
      LineNumberTable:
        line 4: 0
        line 5: 6
}
SourceFile: "Math.java"

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


看看这里面有什么东西?


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

 Last modified 2020-6-27; size 777 bytes
  MD5 checksum a6834302dc2bf4e93011df4c0b774158
  Compiled from "Math.java"
public class com.lxl.jvm.Math
  minor version: 0
  major version: 52

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

Constant pool:
   #1 = Methodref          #9.#35         // java/lang/Object."<init>":()V
   #2 = Class              #36            // com/lxl/jvm/Math
   #3 = Methodref          #2.#35         // com/lxl/jvm/Math."<init>":()V
   #4 = Methodref          #2.#37         // com/lxl/jvm/Math.compute:()I
   #5 = Fieldref           #2.#38         // com/lxl/jvm/Math.initData:I
   #6 = Class              #39            // com/lxl/jvm/User
   #7 = Methodref          #6.#35         // com/lxl/jvm/User."<init>":()V
   #8 = Fieldref           #2.#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               this
  #20 = Utf8               Lcom/lxl/jvm/Math;
  #21 = Utf8               compute

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

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/lxl/jvm/Math
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method compute:()I
        12: pop
        13: return
      LineNumberTable:
        line 15: 0
        line 16: 8
        line 17: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  args   [Ljava/lang/String;
            8       6     1  math   Lcom/lxl/jvm/Math;
    MethodParameters:
      Name                           Flags
      args


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


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


     0: new           #2                  // class com/lxl/jvm/Math

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

 #2 = Class              #36            // com/lxl/jvm/Math


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


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


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

 

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


public static int initData = 666;


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

 

 

2. 验证

 

3. 准备

 

 

4. 解析

 

 

5. 初始化

 

1.3 类的懒加载


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


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

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


package com.lxl.jvm;
public class TestDynamicLoad {
    static {
        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 {
    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**************");
    }
}

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


运行结果

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

总结几点如下:


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

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

相关文章
|
4月前
|
存储 算法 Java
jvm性能优化(一)-基于JDK1.8
jvm性能优化(一)-基于JDK1.8
|
5月前
|
安全 前端开发 Java
【JVM的秘密揭秘】深入理解类加载器与双亲委派机制的奥秘!
【8月更文挑战第25天】在Java技术栈中,深入理解JVM类加载机制及其双亲委派模型是至关重要的。JVM类加载器作为运行时系统的关键组件,负责将字节码文件加载至内存并转换为可执行的数据结构。其采用层级结构,包括引导、扩展、应用及用户自定义类加载器,通过双亲委派机制协同工作,确保Java核心库的安全性与稳定性。本文通过解析类加载器的分类、双亲委派机制原理及示例代码,帮助读者全面掌握这一核心概念,为开发更安全高效的Java应用程序奠定基础。
98 0
|
2月前
|
Oracle 安全 Java
深入理解Java生态:JDK与JVM的区分与协作
Java作为一种广泛使用的编程语言,其生态中有两个核心组件:JDK(Java Development Kit)和JVM(Java Virtual Machine)。本文将深入探讨这两个组件的区别、联系以及它们在Java开发和运行中的作用。
89 1
|
2月前
|
Java 编译器 API
深入解析:JDK与JVM的区别及联系
在Java开发和运行环境中,JDK(Java Development Kit)和JVM(Java Virtual Machine)是两个核心概念,它们在Java程序的开发、编译和运行过程中扮演着不同的角色。本文将深入解析JDK与JVM的区别及其内在联系,为Java开发者提供清晰的技术干货。
35 1
|
4月前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
109 35
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
3月前
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
83 3
|
4月前
|
Arthas Java 测试技术
JVM —— 类加载器的分类,双亲委派机制
类加载器的分类,双亲委派机制:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器;JDK8及之前的版本,JDK9之后的版本;什么是双亲委派模型,双亲委派模型的作用,如何打破双亲委派机制
JVM —— 类加载器的分类,双亲委派机制
|
3月前
|
前端开发 Java 应用服务中间件
JVM进阶调优系列(1)类加载器原理一文讲透
本文详细介绍了JVM类加载机制。首先解释了类加载器的概念及其工作原理,接着阐述了四种类型的类加载器:启动类加载器、扩展类加载器、应用类加载器及用户自定义类加载器。文中重点讲解了双亲委派机制,包括其优点和缺点,并探讨了打破这一机制的方法。最后,通过Tomcat的实际应用示例,展示了如何通过自定义类加载器打破双亲委派机制,实现应用间的隔离。
|
5月前
|
算法 安全 Java
深入JDK源码:揭开ConcurrentHashMap底层结构的神秘面纱
【8月更文挑战第24天】`ConcurrentHashMap`是Java并发编程中不可或缺的线程安全哈希表实现。它通过精巧的锁机制和无锁算法显著提升了并发性能。本文首先介绍了早期版本中使用的“段”结构,每个段是一个带有独立锁的小型哈希表,能够减少线程间竞争并支持动态扩容以应对高并发场景。随后探讨了JDK 8的重大改进:取消段的概念,采用更细粒度的锁控制,并引入`Node`等内部类以及CAS操作,有效解决了哈希冲突并实现了高性能的并发访问。这些设计使得`ConcurrentHashMap`成为构建高效多线程应用的强大工具。
59 2
|
5月前
|
数据库 C# 开发者
WPF开发者必读:揭秘ADO.NET与Entity Framework数据库交互秘籍,轻松实现企业级应用!
【8月更文挑战第31天】在现代软件开发中,WPF 与数据库的交互对于构建企业级应用至关重要。本文介绍了如何利用 ADO.NET 和 Entity Framework 在 WPF 应用中访问和操作数据库。ADO.NET 是 .NET Framework 中用于访问各类数据库(如 SQL Server、MySQL 等)的类库;Entity Framework 则是一种 ORM 框架,支持面向对象的数据操作。文章通过示例展示了如何在 WPF 应用中集成这两种技术,提高开发效率。
77 0