天天看点

JVM HotSpot架构分析

作者:航淳技术

JVM HotSpot架构

JVM HotSpot架构分析

JVM HotSpot架构思维导图

JVM HotSpot架构分析

JVM HotSpot之类加载子系统

一个Java文件从编码完成到最终执行,一般主要包括两个过程

  • 编译,即把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件。
  • 运行,则是把编译生成的.class文件交给Java虚拟机(JVM)执行。

而我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。

举个通俗点的例子来说,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。

由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。

** 下面以HelloWorld.java为类剖析类加载子系统原理**

package com.example.demo;

public class HelloWorld {
    private static  final int a = 1;
    private static int b = 2;
    public static void main(String[] args) {
         int c = 3;
         test1();
         new HelloWorld().test2();
    }

    public static void test1(){
        System.out.println(a);
    }

    public void test2(){
        int f = 5;
        System.out.println(f);
    }
}
           

分析字节码

  • 用windows下生成查看class字节码过程,打开cmd命令行工具:

    编译:cd E:\study\sourcecode\demo\target\classes\com\example\demo: javac HelloWorld.java

    查看字节码cd E:\study\sourcecode\demo\target\classes\com\example\demo: javap HelloWorld.class > HelloWorld.txt 将生成的字节码输出到HelloWorld.txt

Classfile /E:/study/sourcecode/demo/target/classes/com/example/demo/HelloWorld.class
  Last modified 2021-10-1; size 892 bytes
  MD5 checksum 725ca55458c6d045ad2c25c4b7b8477b
  Compiled from "HelloWorld.java"
public class com.example.demo.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#34         // java/lang/Object."<init>":()V
   #2 = Methodref          #3.#35         // com/example/demo/HelloWorld.test1:()V
   #3 = Class              #36            // com/example/demo/HelloWorld
   #4 = Methodref          #3.#34         // com/example/demo/HelloWorld."<init>":()V
   #5 = Methodref          #3.#37         // com/example/demo/HelloWorld.test2:()V
   #6 = Fieldref           #38.#39        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = Methodref          #40.#41        // java/io/PrintStream.println:(I)V
   #8 = Fieldref           #3.#42         // com/example/demo/HelloWorld.b:I
   #9 = Class              #43            // java/lang/Object
  #10 = Utf8               a
  #11 = Utf8               I
  #12 = Utf8               ConstantValue
  #13 = Integer            1
  #14 = Utf8               b
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/example/demo/HelloWorld;
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               c
  #27 = Utf8               MethodParameters
  #28 = Utf8               test1
  #29 = Utf8               test2
  #30 = Utf8               f
  #31 = Utf8               <clinit>
  #32 = Utf8               SourceFile
  #33 = Utf8               HelloWorld.java
  #34 = NameAndType        #15:#16        // "<init>":()V
  #35 = NameAndType        #28:#16        // test1:()V
  #36 = Utf8               com/example/demo/HelloWorld
  #37 = NameAndType        #29:#16        // test2:()V
  #38 = Class              #44            // java/lang/System
  #39 = NameAndType        #45:#46        // out:Ljava/io/PrintStream;
  #40 = Class              #47            // java/io/PrintStream
  #41 = NameAndType        #48:#49        // println:(I)V
  #42 = NameAndType        #14:#11        // b:I
  #43 = Utf8               java/lang/Object
  #44 = Utf8               java/lang/System
  #45 = Utf8               out
  #46 = Utf8               Ljava/io/PrintStream;
  #47 = Utf8               java/io/PrintStream
  #48 = Utf8               println
  #49 = Utf8               (I)V
{
  public com.example.demo.HelloWorld();
    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/example/demo/HelloWorld;

  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: iconst_3
         1: istore_1
         2: invokestatic  #2                  // Method test1:()V
         5: new           #3                  // class com/example/demo/HelloWorld
         8: dup
         9: invokespecial #4                  // Method "<init>":()V
        12: invokevirtual #5                  // Method test2:()V
        15: return
      LineNumberTable:
        line 7: 0
        line 8: 2
        line 9: 5
        line 10: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
            2      14     1     c   I
    MethodParameters:
      Name                           Flags
      args

  public static void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iconst_1
         4: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
         7: return
      LineNumberTable:
        line 13: 0
        line 14: 7

  public void test2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_5
         1: istore_1
         2: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         5: iload_1
         6: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 17: 0
        line 18: 2
        line 19: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/example/demo/HelloWorld;
            2       8     1     f   I

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_2
         1: putstatic     #8                  // Field b:I
         4: return
      LineNumberTable:
        line 5: 0
}
SourceFile: "HelloWorld.java"
           
  • 用IDEA工具,菜单路径: view > show ByteCode显示字节码,安装jclasslib分析字节码:view > show ByteCode jclasslib

字节码内容分析是一项极其复杂的过程,主要是分析16进制文件与字节码的映射, 下一节JVM运行区域再做部分分析 ,本节内容主要是想说明,类是如何加载.class字节码,内容有哪些

JVM如何启动及类加载分析

  1. 根据JVM内存配置要求,为JVM申请特定大小的内存空间;
  2. 创建一个引导类加载器实例,初步加载系统类到内存方法区区域中;
  3. 创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader;
  4. 使用上述获取的ClassLoader实例加载我们定义的 org.luanlouis.jvm.load.Main类;
  5. 加载完成时候JVM会执行Main类的main方法入口,执行Main类的main方法;
  6. 结束,java程序运行结束,JVM销毁
  • Step 1. 根据JVM内存配置要求,为JVM申请特定大小的内存空间
  • 所有的类的定义信息都会被加载到方法区中。
  • Step 2. 创建一个引导类加载器实例,初步加载系统类到内存方法区区域中
  • JVM申请好内存空间后,JVM会创建一个引导类加载器(Bootstrap Classloader)实例,引导类加载器是使用C++语言实现的,负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String, java.lang.Object等等。 引导类加载器(Bootstrap Classloader)会读取 {JRE_HOME}/lib 下的jar包和配置,然后将这些系统类加载到方法区内。
  • 也可以使用参数 -Xbootclasspath 或 系统变量sun.boot.class.path来指定的目录来加载类
  • 引导类加载JVM内存格局如下:
  • Step 3. 创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader

    JVM虚拟机调用已经加载在方法区的类sun.misc.Launcher的静态方法getLauncher(), 获取sun.misc.Launcher,实例如下:

sun.misc.Launcher launcher = sun.misc.Launcher.getLauncher(); 
//获取Java启动器 
ClassLoader classLoader = launcher.getClassLoader();  
           

//获取类加载器ClassLoader用来加载class到内存来

sun.misc.Launcher 使用了单例模式设计,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。

在Launcher的内部,其定义了两个类加载器(ClassLoader),分别是sun.misc.Launcher.ExtClassLoader和sun.misc.Launcher.AppClassLoader,这两个类加载器分别被称为拓展类加载器(Extension ClassLoader) 和 应用类加载器(Application ClassLoader)

JVM HotSpot架构分析

应用类加载器加载类流程图如下:

JVM HotSpot架构分析

双亲委派模型(parent-delegation model):

上面讨论的应用类加载器AppClassLoader的加载类的模式就是我们常说的双亲委派模型(parent-delegation model).

对于某个特定的类加载器而言,应该为其指定一个父类加载器,当用其进行加载类的时候:

  • 委托父类加载器帮忙加载;
  • 父类加载器加载不了,则查询引导类加载器有没有加载过该类;
  • 如果引导类加载器没有加载过该类,则当前的类加载器应该自己加载该类;
  • 若加载成功,返回 对应的Class 对象;若失败,抛出异常“ClassNotFoundException”。
public Class<?> loadClass(String name) throws ClassNotFoundException {
     return loadClass(name, false);
 }
 protected Class<?> loadClass(String name, boolean resolve)         throws ClassNotFoundException
 {        
     synchronized (getClassLoadingLock(name)) {
     // 首先,检查是否已经被当前的类加载器记载过了,如果已经被加载,直接返回对应的Class<T>实例
     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;
     }
   }
 }
           

双亲委派模型中的"双亲"并不是指它有两个父类加载器的意思,一个类加载器只应该有一个父加载器。上面的步骤中,有两个角色:

  1. 父类加载器(parent classloader):它可以替子加载器尝试加载类
  2. 引导类加载器(bootstrap classloader): 子类加载器只能判断某个类是否被引导类加载器加载过,而不能委托它加载某个类;换句话说,就是子类加载器不能接触到引导类加载器,引导类加载器对其他类加载器而言是透明的

    一般情况下,双亲委派模型图:

  • Step 4. 使用类加载器ClassLoader加载HelloWorld类

    通过launcher.getClassLoader()方法返回AppClassLoader实例,接着就是AppClassLoader加载HelloWorld类的时候了

ClassLoader classloader = launcher.getClassLoader();
//取得AppClassLoader类 
 classLoader.loadClass("com.example.demo.HelloWorld");//加载自定义类
           

定义的com.example.demo.HelloWorld类被编译成com.example.demo.HelloWorld class二进制文件,这个class文件中有一个叫常量池(Constant Pool)的结构体来存储该class的常亮信息,常量池中有CONSTANT_CLASS_INFO类型的常量,表示该class中声明了要用到那些类:

JVM HotSpot架构分析

类子系统加载完成后JVM内存结构图如下:

JVM HotSpot架构分析

Step 5. 使用Main类的main方法作为程序入口运行程序

Step 6. 方法执行完毕,JVM销毁,释放内存

继续阅读