JVM之内存结构分析

一、Java 内存结构

Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域是在Java虚拟机启动时创建的,只有在Java虚拟机退出时才会被销毁。其他数据区域是每个线程的。每线程数据区域在创建线程时创建,在线程退出时销毁。

JVM结构官方文档

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.5

1、图解运行时数据区

在这里插入图片描述

2、方法区-Method Area

方法区是各个线程共享的内存区域,在虚拟机启动时创建。

用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

类加载机制中的装载阶段将class文件中的静态结构转换为方法区的运行时数据结构。

2.1)永久代/元空间

方法区在JDK8中是Metaspace(元空间),在JDK6或JDK7中是Perm Space(永久代)。

2.2)运行时常量池

Run-Time Constant Pool

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译时期生成的各种字面量和符号引用,这些内容将在类加载后存放到方法区的运行时常量池中。

每个运行时常量池都是从Java虚拟机的方法区域分配的。类或接口的运行时常量池是在Java虚拟机创建类或接口时构造的。

在创建类或接口时,如果构建运行时常量池所需的内存超过了Java虚拟机的方法区域中可用的内存,则Java虚拟机将抛出OutOfMemoryError。

3、堆-Heap

Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。

Java对象实例以及数组都在堆上分配。
在这里插入图片描述

3.1、新生代 - Young区

Young区分为Eden区、From区、To区 三个区域,其比例默认为8:1:1

Young区的GC操作,叫做Minor GC

1)伊甸园 - Eden区

一般情况下,新创建的对象都会分配到Eden区,一些特殊的大的对象会直接分配到Old区。

比如有A,B,C三个对象创建在Eden区,但是Eden区的内存空间肯定有限,比如有100M,假如已经使用了100M或者达到一个设定的临界值,这时就需要对Eden内存空间进行清理,即垃圾收集(Garbage Collect), 这样的GC称之为 Minor GCMinor GC 指得是Young区的GC。

经过GC之后,有些对象会被清理掉,有些对象可能还存活着,对于存活着的对象需要将其复制到Survivor区,然后再清空Eden区中的这些对象。每一次复制,对象的年龄会+1,当存活对象的年龄超过15时,会进入到Old区。

2)幸存区 - Survivor区

Survivor区 分为 S0 和 S1 两个区域,也就是From 和 To区。通过参数 –XX:SurvivorRatio 来设定。

Survivor区主要是为了减少送到老年代的对象,到达减少Full GC的目的。

在同一个时间点上,S0和S1只有一个区有数据,另外一个是空的。

接着上面的GC来说,比如一开始只有Eden区和From中有对象,To区是空的。

此时进行一次GC操作,From区中对象的年龄就会+1,我们知道Eden区中所有存活的对象会被复制到To区,From区中还存活的对象会有两个去处。

若对象年龄达到设置的年龄阈值,这类对象会被移动到Old区,而Eden区和From区没有达到阈值的对象会被复制到To区。 此时Eden区和From区已经被清空(被GC的对象已被清理,没有被GC的对象也有各自的去处)。

这时From和To交换角色,之前的From变成了To,之前的To变成了From。 也就是说无论如何都要保证名称为To的Survivor区域是空的。

Minor GC会一直重复这个过程,直到To区被填满,然后会将所有对象复制到老年代中。

3.2、老生代 - Old区

一般情况下,Old区都是年龄比较大的对象,或者相对超过了某个阈值的对象。

Old区的GC的操作,叫做 Major GC

3.3、常见问题

1)Minor/Major/Full GC的区别?

Minor GC: 针对新生代的GC。

Major GC: 针对老年代的GC。

Full GC: 针对新生代+老年代的GC。

2)为什么需要Survivor区?只有Eden区不行吗?

如果没有Survivor区,Eden区每进行一次Minor GC,并且没有年龄限制的话,存活的对象就会被送到老年代。 这样一来,老年代很快被填满并触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。

老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。

执行时间长有什么坏处?频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。

所以Survivor区存在的意义就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor区的预筛选保证只有经历16次Minor GC还在新生代中存活的对象,才会被移动到老年代。

3)为什么需要两个Survivor区?

解决内存碎片问题。

有一个Survivor space永远是空的,另一个非空的Survivor space无碎片。

4)为什么Eden:S0:S1是8:1:1?

1、实际上GC是在新生代内存使用达到**90%**时开始进行的,通过复制回收算法将存活的对象复制到S1区。此时可知道S1区占比10%,也就是说新生代使用内存和S1的比例为9:1。

2、GC结束后在S1区存活下来的对象,需要放回给S0区,也就是S1和S0对调(名称互换)。既然可以对调,那么可知S1和S0的大小是一样的。由此可得出结论:还有一个占新生代10%空间的S0区域,所以新生代中Eden:S0:S1 = 8:1:1。

3、GC所清理的90%的新生代内存就是Eden区(80%)和S0区(10%)

4、虚拟机栈-Java Virtual Machine Stack

虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态,为线程私有。

一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。

每调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
在这里插入图片描述

4.1、栈帧结构 Frames

每次调用方法时都会创建一个新栈帧。当一个栈帧的方法调用完成时,不管该完成是正常的还是突然的(它抛出一个未捕获的异常),它都会被销毁。帧是从创建帧的线程的Java虚拟机堆栈分配的。每个帧都有自己的局部变量数组、自己的操作数堆栈和对当前方法类的运行时常量池的引用。

一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

1)局部变量表 Local variable Table

方法中定义的局部变量以及方法的参数存放在这张表中,局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。

  • 在编译程序代码的时候就可以确定栈帧中需要多大的局部变量表,具体大小可在编译后的 Class 文件中看到。

  • 局部变量表的容量以 Variable Slot(变量槽)为最小单位,每个变量槽都可以存储 32 位长度的内存空间。

  • 在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用(在方法中可以通过关键字 this 来访问到这个隐含的参数)。

  • 其余参数则按照参数表顺序排列,占用从 1 开始的局部变量 Slot。

  • 基本类型数据以及引用和 returnAddress(返回地址)占用一个变量槽,long 和 double 需要两个。

2)操作数栈 Operand Stacks
  • 方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作(与 Java 栈中栈帧操作类似)。

  • 操作栈调用其它有返回结果的方法时,会把结果 push 到栈上(通过操作数栈来进行参数传递)。

3)动态链接 Dynamic Linking
  • 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

  • 类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在运行时转化为直接引用,这部分称为动态链接。

4)返回地址 Return Address

当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。

4.2、结合字节码指令理解Java虚拟机栈和栈帧

1)源代码
package com.coy.gupaoedu.study.jvm;

/**
 * 编译:javac StackFrameTest.java
 * 反编译:javap -p -v StackFrameTest.class
 *
 * @author chenck
 * @date 2020/7/30 10:01
 */
public class StackFrameTest {

    public static void main(String[] args) {
        add(5, 7);
    }

    private static int add(int a, int b) {
        int c = 0;
        c = a + b;
        return c;
    }
}
2)反编译

反编译后生成字节码。

javap -p -v StackFrameTest.class
3)字节码分析

主要分析 StackFrameTest.add(int,int) 方法。下面去掉了不相关的字节码。

JVM指令可参考官方文档

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.html

Compiled from "StackFrameTest.java"
public class com.coy.study.jvm.StackFrameTest
  minor version: 0					// JDK 最低版本号
  major version: 52					// JDK 最高版本号
  flags: ACC_PUBLIC, ACC_SUPER		// 访问标志
Constant pool:						// 常量池
  ... ...
  private static int add(int, int);
    // 方法描述
    // 括号内表示入参类型。即 (II) 表示两个 int 类型参数
    // 括号外表示返回类型。即 I 表示返回 int 类型
    descriptor: (II)I
    // 访问标志:这里表示私有静态方法
    flags: ACC_PRIVATE, ACC_STATIC
    // 代码块
    Code:
      // 操作数栈为2
      // 本地变量数为3
      // 入参个数为2
      stack=2, locals=3, args_size=2
         0: iconst_0    // 将int类型常量0压入[操作数栈]
         1: istore_2    // 将[操作数栈]栈顶元素取出,并保存到[局部变量2]
         2: iload_0     // 从[局部变量0]中装载值压入[操作数栈]
         3: iload_1     // 从[局部变量1]中装载值压入[操作数栈]
         4: iadd        // 将[操作数栈]栈顶元素取出,执行int类型的加法,结果压入[操作数栈]
         5: istore_2    // 将[操作数栈]栈顶元素保存到[局部变量2]
         6: iload_2     // 从[局部变量2]中装载值压入[操作数栈]
         7: ireturn     // 返回[操作数栈]栈顶元素
      // 行号表,表示代码行号与字节码行号的对应关系
      LineNumberTable:
        line 17: 0      // 表示第17行代码,对应的指令为0
        line 18: 2      // 表示第18行代码,对应的指令为2
        line 19: 6      // 表示第19行代码,对应的指令为6
}
SourceFile: "StackFrameTest.java"
4)方法执行过程中栈帧的变化

分析 add(5,7) 的执行过程。最后 ireturn 会将操作数栈的栈顶元素返回调用方。
在这里插入图片描述

5、程序计数器-The PC Register

程序计数器记录的是正在执行的虚拟机字节码指令的地址,可以认作是当前线程的行号指示器。 为线程私有。

在执行Native方法时程序计数器为空。

程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)。

6、本地方法栈-Native Method Stack

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行,为线程私有。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白云coy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值