Java 虚拟机底层原理分析

也可参考其他人的总结相互学习:https://blog.csdn.net/weixin_38766678/article/details/96566161

 

JVM由三个主要的子系统构成:

1.类加载器子系统

2.运行时数据区(内存)

3.执行引擎

认识虚拟机内存模型:

如以下类

package com.lean;
public class JowerJvm {
    public int compute() {
        int a = 2;
        int b = 3;
        int c = (2 + 3) * 8;
        return c;
    }

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

当程序开始执行main方法时,JVM会开辟一块线程栈的内存空间

通过javap -c JowerJvm.class 命令可以查看.class文件的字节码中的执行指令。

如下:

F:\leancode\jvm\out\production\jvm\com\lean>javap -c JowerJvm.class
Compiled from "JowerJvm.java"
public class com.lean.JowerJvm {
  public com.lean.JowerJvm();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int compute();
    Code:
       0: iconst_2
       1: istore_1
       2: iconst_3
       3: istore_2
       4: bipush        40
       6: istore_3
       7: iload_3
       8: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/lean/JowerJvm
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method compute:()I
      12: pop
      13: return
}

根据字节码生成的命令compute()方法令计算过程如下(需查找对应的命令)

public int compute();
    Code:
       0: iconst_2          //备注:int型常量2 放到数据栈中
       1: istore_1          //备注:将数据栈中的值取出来存储到第二个变量中。即给变量a赋值   
       2: iconst_3			//备注:int型常量3 放到数据栈中
       3: istore_2			//备注:将数据栈中的值取出来存储到第三个变量中。即给变量b赋值
       4: bipush        40	//备注:将一个byte型常量40推送至数据栈顶
       6: istore_3			//备注:将数据栈中的值取出来存储到第四个变量中。即给变量c赋值
       7: iload_3			//备注:第四个int型局部变量进数据栈
       8: ireturn			//备注:当前方法返回int

注意:

  1. 局部变量表中默认第一个变量istore_0为this 变量。
  2. 方法栈中的操作数栈作为中间临时栈用来协助JVM执行计算(加减乘除)和赋值给局部变量用。
  3. 字节码指令前的数字0、1、2…8 为程序计数器,记录了当前运行指令的位置,JVM执行引擎会按照该code顺序执行对应的指令。
  4. 方法栈中的方法出口 保存了被调用方法(main方法)的调用返回位置(即main方法的程序计数器12) 12: pop 表示栈顶位置出栈

当compute方法栈执行完成后会将结果返回,同时compute方法栈就会被移出JowerJvm的线程栈,相关内存会释放掉。同理main方法栈执行完后也会释放该部分内存。

方法区:存储常量+静态变量+类元信息(Java classesJava hotspot VM内部表示为类元数据,包括上述描述中的main 方法和compute方法的指令码),JDK1.8之后方法区用的是操作系统分配给JVM的内存以外的物理内存

 

内存中对象包含很多东西,如下:

其中对象头中有类型指针就存储了方法区中对应类(JowerJvm.class)的类元信息的地址. (执行引擎执行对象方法时,通过该地址获取到对应class中方法的执行指令)

 

假如再new一个JowerJvm对象,如下:

package com.lean;
public class JowerJvm {
    public int compute() {
        int a = 2;
        int b = 3;
        int c = (2 + 3) * 8;
        return c;
    }

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

重点:这个时候当程序运行到 调用math.compute()方法时通过堆内存中的对象从方法区获取到compute方法指令序列在类源信息中的内存地址。然后存放到compute方法栈帧的“动态链接”内存中。

 

其中动态链接内存的主要目的就是在运行过程中动态获取当前方法(compute)的执行指令序列

 

可以通过 javap  -v JowerJvm.class  查看更多的执行指令,如下:

F:\leancode\jvm\out\production\jvm\com\lean>javap -v JowerJvm.class
Classfile /F:/leancode/jvm/out/production/jvm/com/lean/JowerJvm.class
  Last modified 2019-11-10; size 616 bytes
  MD5 checksum 77fceff8a826cd9ff400630ae9a864c0
  Compiled from "JowerJvm.java"
public class com.lean.JowerJvm
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#27         // java/lang/Object."<init>":()V
   #2 = Class              #28            // com/lean/JowerJvm
   #3 = Methodref          #2.#27         // com/lean/JowerJvm."<init>":()V
   #4 = Methodref          #2.#29         // com/lean/JowerJvm.compute:()I
   #5 = Class              #30            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcom/lean/JowerJvm;
  #13 = Utf8               compute
  #14 = Utf8               ()I
  #15 = Utf8               a
  #16 = Utf8               I
  #17 = Utf8               b
  #18 = Utf8               c
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               args
  #22 = Utf8               [Ljava/lang/String;
  #23 = Utf8               math
  #24 = Utf8               math2
  #25 = Utf8               SourceFile
  #26 = Utf8               JowerJvm.java
  #27 = NameAndType        #6:#7          // "<init>":()V
  #28 = Utf8               com/lean/JowerJvm
  #29 = NameAndType        #13:#14        // compute:()I
  #30 = Utf8               java/lang/Object
{
  public com.lean.JowerJvm();
    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/lean/JowerJvm;

  public int compute();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=4, args_size=1
         0: iconst_2
         1: istore_1
         2: iconst_3
         3: istore_2
         4: bipush        40
         6: istore_3
         7: iload_3
         8: ireturn
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 4
        line 9: 7
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/lean/JowerJvm;
            2       7     1     a   I
            4       5     2     b   I
            7       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 com/lean/JowerJvm
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method compute:()I
        12: pop
        13: new           #2                  // class com/lean/JowerJvm
        16: dup
        17: invokespecial #3                  // Method "<init>":()V
        20: astore_2
        21: aload_2
        22: invokevirtual #4                  // Method compute:()I
        25: pop
        26: return
      LineNumberTable:
        line 13: 0
        line 14: 8
        line 16: 13
        line 17: 21
        line 18: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  args   [Ljava/lang/String;
            8      19     1  math   Lcom/lean/JowerJvm;
           21       6     2 math2   Lcom/lean/JowerJvm;
}
SourceFile: "JowerJvm.java"

通过main()方法的指令序列可以看到,两次math.compute()方法都存的是符号  #4 ,并通过常量池中查看到#4代表的是方法引用    值为:#2.#29 即JowerJvm.compute()方法。

#2 = Class              #28            // com/lean/JowerJvm
#4 = Methodref          #2.#29         // com/lean/JowerJvm.compute:()I
#28 = Utf8               com/lean/JowerJvm
#29 = NameAndType        #13:#14        // compute:()I

当调用到compute()对应指令时会通过对象的头信息中找到方法区中对应的class指令中compute()方法的指令码位置,并存储到新生成的compute方法栈帧的动态链接内存中。

 

本地方法栈,当程序调用以native修饰的本地方法时(一般为C或者C++语言编写的,如Thread.start方法)会使用本地方法栈。一般用的较少。

 

垃圾收集(GC:Garbage Collection)

1、如何识别垃圾,判定对象是否可被回收?

引用计数法:给每个对象添加一个计数器,当有地方引用该对象时计数器加1,当引用失效时计数器减1。用对象计数器是否为0来判断对象是否可被回收。缺点:无法解决循环引用的问题

根搜索算法:也称可达性分析法,通过“GC ROOTs”的对象作为搜索起始点,通过引用向下搜索,所走过的路径称为引用链。通过对象是否有到达引用链的路径来判断对象是否可被回收(可作为GC ROOTs的对象:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象)

2、Java 中的堆是 GC 收集垃圾的主要区域,GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。

    Minor GC:新生代(Young Gen)空间不足时触发收集,由于Java 中的大部分对象通常不需长久存活,新生代是GC收集频繁区域,所以采用复制算法。

    Full GC:老年代(Old Gen )空间不足或元空间达到高水位线执行收集动作,由于存放大对象及长久存活下的对象,占用内存空间大,回收效率低,所以采用标记-清除算法。

 

Java虚拟机调优主要调的是堆内存的区域

 

1.  当 Eden内存占满的时候

     1)会执行minor gc ,轻量级的垃圾回收  会将无GC Roots根引用的游离状态的的对象进行回收。

     2)会将存活的对象从Eden移至survivor From区域

2.  当 Survivor的From区域占满的时候

     1)执行GC垃圾回收,同时对象的分代年龄加1(存储在对象头结构中)

     2)将剩余存活的对象从From移至To区域

3.  当 Survivor的To区域占满的时候

     1)执行GC垃圾回收,同时对象的分代年龄加1(存储在对象头结构中)

     2)将剩余存活的对象从To移至From区域,如果当前对象的分代年龄为15(默认值)的时候,会将该存活的对象从TO移动至老年待区域

4.  到了老年代区域的存活对象,如线程池对象、静态引用的单例对象(可以认为是老不死的对象)。

当老年代区域占满的时候,就会执行full gc (重量级的垃圾回收),但是如果老年代区域中的所有对象还是有对应的引用,继续往里加对象时就会出现OOM(Out of Memory)

注意:造成full gc的原因有很多种,老年代区域占满调用只是其中一种情况。

 

动态演示对象在Eden、Survivor、Old Gen区域的传递:

1.cmd通过jvisualvm 命令打开Oracle提供的虚拟机工具(需配置jdk的环境变量)。

打开之后,通过工具—插件,然后选择Visual GC在线安装

2. jvisualvm安装Visual GC插件(如果无法在线安装可下载后安装)

可参考链接进行安装:https://blog.csdn.net/shuai825644975/article/details/78970371

3. 执行以下代码

package com.lean;

import java.util.ArrayList;
import java.util.List;
public class GCTest {
    private byte[] data = new byte[1024 * 100]; //100k的大小
    public static void main(String[] args) throws InterruptedException {
        List<GCTest> list = new ArrayList<GCTest>();
        while (true) {
            list.add(new GCTest());
            Thread.sleep(100);
        }
    }
}

4.通过GC Visual插件可以动态查看各堆内存区域对象迁移情况

下图可以明显看出来Eden区域执行GC之后对象迁移至Survivor0区域,以及Survivor 的两个区域对象交替存储,以及Old Gen区域对象不断增加。

当Old Gen内存占满的时候就会出现OOM

Files\Java\jdk1.8.0_131\jre\lib\rt.jar;F:\leancode\jvm\out\production\jvm" com.lean.GCTest
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.lean.GCTest.<init>(GCTest.java:8)
	at com.lean.GCTest.main(GCTest.java:13)

Process finished with exit code 1

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值