JVM第二篇:JVM的构成

一 JVM介绍

1.1 什么是JVM

What is the JVM?

1.2 jvm构成

jvm由三个主要的子系统组成:

  • 类装载器子系统(将字节码.class文件装载到运行时数据区中去)
  • 运行时数据区(java虚拟机对应的内存区域叫运行时数据区)
  • 执行引擎(执行java程序,输出结果,包含垃圾收集器模块,用于在程序运行的时候不断清理内存区域中垃圾)

下面图是整个jvm的结构

java源代码文件会被编译成class文件,然后被jvm的类装载器装载到jvm里面。
所有的数据都在我们运行时数据区,所有的数据都在运行时数据区以后,包括代码,方法都进来以后,就由我们jvm的执行引擎来负责执行:

  1. 如果是执行方法,那么它就会在虚拟机栈里面进行方法的依次进栈,出栈等等操作
  2. 如果要调用一些本地方法(指的是我们操作系统暴露的本地方法库/本地库接口)
  3. 包括程序执行到哪一行了:程序计数器

线程隔离的数据区:线程私有,每个线程里面都有:

  • 当前线程调用到哪了(程序计数器)
  • 当前线程调用到哪个方法了(虚拟机栈)
  • 当前线程调用的是本地接口的哪个方法(本地方法栈),每个线程都是相互隔离的,自己保存自己的

java8以后就有了元数据区,元数据区直接操作我们物理内存的
还有JIT编译产物:我们代码运行期间,我们哪个代码没编译,编译期间所有的代码缓存都在这里,这块的调节也是有的,但是我们更多的关注的堆

  1. 方法区是JVM规范的一个概念定义,并不是一个具体的实现,每一个JVM的实现都可以有各自的实现;
  2. 在Java官方的HotSpot 虚拟机中,Java8版本以后,是用元空间来实现的方法区;在Java8之前的版本,则是用永久代实现的方法区;
  3. 也就是说,“元空间” 和 “方法区”,一个是HotSpot 的具体实现技术,一个是JVM规范的抽象定义;

元空间的存储位置是在计算机的内存当中,而永久代的存储位置是在JVM的堆(Heap)中
之所以移除permGen永久代在java8中因为

  1. This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
    这是JRockit和Hotspot融合工作的一部分。JRockit的客户不需要配置永久代(因为JRockit没有永久代),并且习惯于不配置永久代。
  2. 随着Java在Web领域的发展,Java程序变得越来越大,需要加载的内容也越来越多,如果使用永久代实现方法区,那么需要手动扩大堆的大小,而使用元空间之后,就可以直接存储在内存当中,不用手动去修改堆的大小。

以上两部分引用转自:https://www.zhihu.com/question/358312524

二 运行时数据区(JVM内存结构)

java程序运行的时候在一个进程中运行,进程中有很多线程,这些线程是真正去执行我们代码的最小单元,线程运行的时候会使用到一些数据因此是会跟内存进行交互的,内存有可能是所有线程共享的,也有可能是每个线程独自占有的。

线程私有的内存区域

2.1 栈内存

  1. 栈是每个线程独有的,不被其他线程共享
  2. 栈是先进后出的数据结构
  3. 方法进栈以后称之为栈帧,一个栈帧包括四个部分:局部变量表,操作数栈,动态链接,方法出口
案例
public class Math {
    private 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();
        System.out.println("finished");
    }
}

main方法先进栈,compute()后进栈,各自维护了四部分,当compute方法执行完毕后,compute栈帧弹出,然后main方法弹出栈

执行上面的java代码,生成字节码class文件Math.class,然后我们采用jdk提供的方便阅读字节码文件的javap命令来查看生成的易读的字节码文件到txt文件中

javap -c Math.class > math.txt
Compiled from "Math.java"
public class org.radient.jvm.Math {
  public org.radient.jvm.Math();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public int compute();
    Code:
       0: iconst_1  # 将int类型常量1压入栈(操作数栈),即1
       1: istore_1  # 将int类型值存入局部变量1,即a
       2: iconst_2  # 将int类型常量2压入栈
       3: istore_2  # 将int类型值存入局部变量2
       4: iload_1   # 从局部变量1中装载int类型值1(注意:装载的是值1,非变量引用a!!或a=1)
       5: iload_2   # 从局部变量2中装载int类型值2
       6: iadd      # 执行int类型的加法,即a+b
       7: bipush        10     # 将a+b的结果存回操作数栈,
       9: imul      # 执行int类型的乘法,即a+b的结果*10
      10: istore_3  # 将int类型值存入局部变量3,即变量c
      11: iload_3   # 从局部变量3中装载int类型值
      12: ireturn   # 返回结果

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class org/radient/jvm/Math
       3: dup
       4: invokespecial #3                  // Method "":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method compute:()I
      12: pop
      13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      16: ldc           #6                    // String finished
      18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      21: return
}

根据上面可读的字节码文件来分析一下整个的执行过程,上面每行序号后面对应的是JVM指令,具体每个指令的意思,参阅:https://www.cnblogs.com/lsy131479/p/11201241.html

根据上面的指令, 可以分析一下compute()执行过程,具体的解析指令已经在上面代码中标注出来了

案例代码执行过程:(操作数栈用于临时存放操作中的值,局部变量表存放变量和变量的值)
(1)先为局部变量例如a开辟内存空间,入局部变量表a,变量的值1进入操作数栈中进行运算,运算完将结果更新到局部变量表a,有了a=1
(2)同上,处理变量b=2
(3)将局部变量表中要运算的两个变量的1和2放入操作数栈,然后因为操作数栈也是栈结构,遵循后进先出,因此看到iadd(int类型加法)的时候,先将2弹出操作数栈,然后将1弹出操作数栈,然后对这两个元素进行加法运算,获得结果3
(4)将上一步获得的结果3写回操作数栈,并执行bipush 10,将10压如操作数栈,然后弹出10和3,执行乘法运算,将结果30写回操作数栈
(5)istore_3(将int类型值存入局部变量3)将30写入局部变量表给c,c=30
(6)iload_3(将int类型值放到操作数栈)将30放入操作数栈,然后执行ireturn,从方法中返回int类型的30,如果要打印的话,就返回给system.out方法所属的栈祯,栈祯顺序=>>>main->system.out->compute

除了上面的查看方式,也可以是用jclasslib工具来方便查看,它有idea的插件版本,也有可安装版本,github地址:https://github.com/ingokegel/jclasslib/releases

  • Name:方法名
  • Description:描述了方法的参数类型以及返回值类型。比如<([Ljava/lang/String;)V>,说明参数类型是Ljava/lang/String;返回值类型是-V,表示的是void类型。
  • Access flags:访问标识。public static。

其他详细参数说明:
https://www.cnblogs.com/yuexiaoyun/articles/13998443.html

2.1.1 局部变量表

main方法中上来就创建了个math对象,即main()的局部变量是一个对象类型,不是基本类型,根据我们的常识,对象类型new出来的对象是放在堆内存的,那么相比于compute()中的基本变量,main中创建的math对象在局部变量中怎么保存呢?

math()的局部变量表保存math对象的引用,math对象的引用指向的是堆内存中的math对象实体

2.1.2 方法出口

上面图中我们刚才看了局部变量表和操作数栈,那么方法出口是什么呢?
方法出口就是compute方法执行完以后返回main方法,怎么知道要执行下面的打印syso语句呢?就是根据这个方法出口,类似于导游的作用

2.1.3 动态链接

动态链接就是存储当前线程很多不同方法的指令码,只在程序运行的时候创建

我们可以通过下述代码助于理解

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
        Math math2 = new Math();
        math2.compute();
        System.out.println("finished");
    }

如上创建了两个对象,math和math2,都是来源于模板类Math,每new出来一个对象,该对象头里就有个指针指向对象所属的的那个类(math.class),为什么要指向呢?为什么就知道执行的compute()的代码就是上面那几行呢?是math类的呢?

这并不是理所当然!是因为创建对象的时候,对象里面存储了类元信息(比如:这个类有哪些方法都是属于这个类的类元信息)
一旦有了这个指针以后,再去调用这个对象的compute()的时候,底层做的就是根据math对象的头指针找到对应的Math类的那块(指令码)

所以对象1和对象2都找到了Math类对应的compute()的代码,math.compute()是符号引用

更深层次理解什么叫动态链接:
我们生成更加复杂的可读的指令码文件,采用命令:
javap -v Math.class > math.txt

Classfile /H:/package/�����¼/java-study/target/classes/org/radient/jvm/Math.class
  Last modified 2019-10-14; size 809 bytes
  MD5 checksum 5ce39fe60ec15465be0bf023228c71f6
  Compiled from "Math.java"
public class org.radient.jvm.Math
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#31         // java/lang/Object."":()V
   #2 = Class              #32            // org/radient/jvm/Math
   #3 = Methodref          #2.#31         // org/radient/jvm/Math."":()V
   #4 = Methodref          #2.#33         // org/radient/jvm/Math.compute:()I
   #5 = Fieldref           #34.#35        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = String             #36            // finished
   #7 = Methodref          #37.#38        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #8 = Class              #39            // java/lang/Object
   #9 = Utf8               
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lorg/radient/jvm/Math;
  #16 = Utf8               compute
  #17 = Utf8               ()I
  #18 = Utf8               a
  #19 = Utf8               I
  #20 = Utf8               b
  #21 = Utf8               c
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               math
  #27 = Utf8               math2
  #28 = Utf8               MethodParameters
  #29 = Utf8               SourceFile
  #30 = Utf8               Math.java
  #31 = NameAndType        #9:#10         // "":()V
  #32 = Utf8               org/radient/jvm/Math
  #33 = NameAndType        #16:#17        // compute:()I
  #34 = Class              #40            // java/lang/System
  #35 = NameAndType        #41:#42        // out:Ljava/io/PrintStream;
  #36 = Utf8               finished
  #37 = Class              #43            // java/io/PrintStream
  #38 = NameAndType        #44:#45        // println:(Ljava/lang/String;)V
  #39 = Utf8               java/lang/Object
  #40 = Utf8               java/lang/System
  #41 = Utf8               out
  #42 = Utf8               Ljava/io/PrintStream;
  #43 = Utf8               java/io/PrintStream
  #44 = Utf8               println
  #45 = Utf8               (Ljava/lang/String;)V
{
  public org.radient.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."":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lorg/radient/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 10: 0
        line 11: 2
        line 12: 4
        line 13: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Lorg/radient/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=3, args_size=1
         0: new           #2                  // class org/radient/jvm/Math
         3: dup
         4: invokespecial #3                  // Method "":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method compute:()I
        12: pop
        13: new           #2                  // class org/radient/jvm/Math
        16: dup
        17: invokespecial #3                  // Method "":()V
        20: astore_2
        21: aload_2
        22: invokevirtual #4                  // Method compute:()I
        25: pop
        26: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        29: ldc           #6                  // String finished
        31: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        34: return
      LineNumberTable:
        line 17: 0
        line 18: 8
        line 19: 13
        line 20: 21
        line 21: 26
        line 22: 34
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      35     0  args   [Ljava/lang/String;
            8      27     1  math   Lorg/radient/jvm/Math;
           21      14     2 math2   Lorg/radient/jvm/Math;
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "Math.java"


这个#4是一个引用,指向常量池中的


而#2,#32又是引用,指向Math的Class和

这样我们就将静态的compute方法转化为实际的指令码存放位置
根据堆中对象的头指针,找到方法区中加载的Math.class类的元信息(指令码的内存地址),将这个内存地址放到栈中的动态链接内存中

再执行compute方法的时候,会根据动态链接,返回一条线在方法区中找到放到方法区这块内存中的指令码,这块指令码是程序运行过程中生成的

2.2 程序计数器

(1)程序计数器就是用来存储将要执行那一行JVM指令码的行号(内存地址)
(2)程序计数器同栈结构一样是每个线程自己的,不被其他线程共享
(3)执行第一行代码的时候,程序计数器就有值了,而且每执行完一行,jvm的执行引擎,会将当前线程的程序计数器的值改为下一行的行号,根据这个程序计数器,知道我们将要执行的下一行代码



如上图,可读字节码文件每一行指令前的行号就是程序计数器中记录的东西

2.3 本地方法栈

带native的方法,不是java实现,是c语言实现的,Java执行到这一行的时候,会去java底层的c库里面,找xx.dll结尾的文件(类似于java中的jar包),在这个dll文件中有start0方法的实现

执行引擎会利用本地方法接口来真正调用底层c语言的接口

线程共享的内存区域

2.4 方法区

方法区的基本介绍在上面的前言部分已经阐述,下面来详细讲讲方法区
首先是方法区的一些概念

2.4.1 方法区的演变史

勘误:下面图中的permGen旁边的文字都是永久代实现

  • jdk1.6以及1.6以前
    有永久代,静态变量存放在永久代
  • jdk1.7
    有永久代,但是已经开始着手移除永久代,首当其冲将静态变量和字符串常量池移动到堆中
  • jdk1.8
    随着JDK8的到来,JVM不再有PermGen。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。
2.4.2 class文件常量池、运行时常量池、字符串常量池
  • class文件常量池
    已加载的每个class文件中,都维护着一个常量池,里面存放着编译时期生成的各种字面值和符号引用。\color{red}{字面值有(文本字符串,基本数据类型以及他们包装类的值,final修饰的变量,即在编译期间就能确定下来的值)}
    class文件常量池在类被加载的时候,会被复制到方法区中的运行时常量池,池中的数据项类似数组项一样,是通过索引访问的
    类的加载过程中的链接部分的解析步骤就是

以下内容节选自https://blog.csdn.net/luanlouis/article/details/39960815






  • 运行时常量池
    jvm会将各个class文件中的常量池载入到运行时常量池中,即编译期间产生的字面量、符号引用。类的加载过程中的链接部分的解析步骤就是把符号引用替换为直接引用,即把那些描述符(名字)替换为能直接定位到字段、方法的引用或句柄(地址)。即除了保存class文件中的符号引用,还会把翻译出来的直接引用也存储在运行时常量池
    同时,运行时常量池允许在运行期间将新的变量放入常量池中。最主要的运用便是String类的intern()方法:检查字符串常量池中是否存在String并返回池里的字符串引用;若池中不存在,则将其加入池中,并返回其引用。这样做主要是为了避免在堆中不断地创建新的字符串对象
  • 字符串常量池
    https://zhuanlan.zhihu.com/p/160770086
    https://www.cnblogs.com/tiancai/p/9321338.html

2.5 堆

所有的对象实例以及数组都要在堆上分配。堆是垃圾收集器管理的主要区域,也被称为“GC堆”;也是我们优化最多考虑的地方。
堆可以细分为:

  • 新生代
    • Eden 空间
    • From Survivor 空间
    • To Survivor 空间
  • 老年代
  • 永久代/元空间
    • Java8 以前永久代,受 jvm 管理,java8 以后元空间,直接使用物理内存。因此,
      默认情况下,元空间的大小仅受本地内存限制。

虚拟机启动时创建,用于存放实例对象,几乎所有的对象都在堆上分配内存,对象无法在堆中申请到内存的时候抛出oom异常,同时也是GC管理的主要区域,可通过 -Xmx -Xms参数来分别制定最大堆和最小堆

由上图可以看到,堆由两部分组成,年轻代和老年代,年轻代又由eden区和survivor区组成,年轻代占了1/3的堆内存空间,老年代占据2/3的堆内存空间,eden区又占据年轻代8/10的内存空间
new出来的对象都在eden区(亚当和夏娃在伊甸园造人)

2.6 堆上的GC

新创建的对象,要分配内存,会先去新生代里面,具体呢就是先看eden区,判断eden区内存空间够不够,够的话直接在eden区分配内存,如果不够,此时就要进行一次minorGC,这次gc主要是清理新生代空间,怎么清理呢?
比如之前eden区有10个对象,其中1个对象还在用,其他9个没用了,我们就把这1个对象放到survivor区,剩下的9个剔除出去

然后再次判断minorGC完后eden能不能放下,如果能放下最好,还是放不下的话,我们认为这是一个大对象,我们尝试将它放在老年代

能放下最好,还是放不下的话,那我们就要进行一次FullGC,大屠杀,把老年代存的数据,新生代存的数据,全看一看哪些没用了,全部剔除出去,再来看老年代能不能放下,能放下就分配内存, 放不下就OOM内存溢出

一句话:在新生代没法处理的话,才进入老年代,所以老年代里面存的总是那些生命力持久的对象和那些大对象

minorGC触发时机:eden区内存不够了
fullGC触发时机:老年代内存不够了

survivor区有from和to,这两个是来回交换,目的就是总要腾出一个大白片空间
对象存活超过阈值:每次小屠杀minorGC完后,还存活的对象我们可以认为它年龄大了一岁,如果它15岁了,就把这个对象搬到老年区
如果幸存者区能放下,就放里面了,如果放不下,那就会把这些存活的对象直接搬家放到老年代里面,这也是一个流程
即:一次小的minorGC总是要把我们的eden区清理干净,能放幸存者区就放幸存者区,放不了了就放老年代

为什么年轻代有2个survivor
为了保证任何时候总有一个survivor是空的。
因为将eden区的存活对象复制到survivor区时,必须保证survivor区是空的,如果survivor区中已有上次复制的存活对象时,这次再复制的对象肯定和上次的内存地址是不连续的,会产生内存碎片,浪费survivor空间。
如果只有一个survivor区,第一次GC后,survivor区非空,eden区空,为了保证第二次能复制到一个空的区域,新的对象必须在survivor区中出生,而survivor区是很小的,很容易就会再次引发GC。
而如果有两个survivor区,第一次GC后,把eden区和survivor0区一起复制到survivor1区,然后清空survivor0和eden区,此时survivor1非空,survivor0和eden区为空,下一次GC时把survivor0和survivor1交换,这样就能保证向survivor区复制时始终都有一个survivor区是空的,也就能保证新对象能始终在eden区出生了。

关于年轻代的GC流程
(1)创建的新对象都放到eden区,当eden区满以后,执行一次minor gc,将这次gc结束后,仍然存活的对象放入s0即from中去
(2)后面new出来的对象继续往eden区放,当eden区再次放满,将再进行一次minor gc,又有一些对象放入from,这样from区最终也有放满的时候
(3)随着eden区再次满,再次往from区中迁移对象,终有一天经过eden区结束minor gc再次往from区放对象的时候,放不下了,from区满了,就执行from区的minor gc,from区回收的时候,同样回收的是没有引用的对象,from中仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认是15次)的对象会被移动到老年代中,没有达到阈值的对象会被复制到“To”区域,而Eden区中所有存活的对象都会被复制到“To”,
(4)经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”
(5)不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,在进行一次GC的时候,会将所有对象移动到年老代中。总有一天老年代也会放满,到时候就会触发老年代GC-full gc

你可能感兴趣的:(JVM第二篇:JVM的构成)