JVMJVM

jvm第一节+谈谈方法区

2019-10-14  本文已影响0人  今年五年级

一 基本概念

1.1 什么是jvm

虚拟机指用软件的方式模拟完整硬件系统功能,运行在一个安全隔离环境中的完整计算机系统,是物理机的软件实现。常用的虚拟机软件有VMWare,Virtual Box,Java Virtual Machine

常见的java虚拟机:Sun HotSpot VM、BEA JRockit VM、IBM J9 VM、Google Dalvik VM。java8基于(Sun HotSpot VM、BEA JRockit VM)整合

1.2 jvm构成

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

下面图是整个jvm的结构

  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."<init>":()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 "<init>":()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

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."<init>":()V
   #2 = Class              #32            // org/radient/jvm/Math
   #3 = Methodref          #2.#31         // org/radient/jvm/Math."<init>":()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               <init>
  #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         // "<init>":()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."<init>":()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 "<init>":()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 "<init>":()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 方法区的演变史
2.4.2 class文件常量池、运行时常量池、字符串常量池

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






2.5 堆

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

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

eden区内存占用满了以后,就会触发轻量级GC:minor GC。minor GC会将内存中没有引用指向他的无用对象回收,释放掉这部分内存空间,避免浪费内存空间(因为还有新的对象需要放进去)

为什么年轻代有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,将尚存活的对象放入s0即from中去
(2)后面new出来的对象继续往eden区放,当eden区再次放满,将再进行一次minor gc,又有一些对象放入from,from区最终也有放满的时候
(3)当from区被持续放对象,放满以后,也会进行一次minor gc,这次gc不仅仅会回收eden区,也会回收from区,from区回收的时候,同样回收的是没有引用的对象,from中仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认是16次)的对象会被移动到老年代中,没有达到阈值的对象会被复制到“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

上一篇下一篇

猜你喜欢

热点阅读