写点什么

Java 内存模型

用户头像
懒AI患者
关注
发布于: 2020 年 12 月 13 日

一、整体结构


二、详细解析

java 运行程序(进程)时,会有对应的栈、堆、非堆空间,其大小根据默认值或配置参数指定。

1、堆

堆内存是所有线程共用的内存空间,JVM 将 Heap 内存分为年轻代(Young generation)和 老年代(Old generation, 也叫 Tenured)两部分。 年轻代还划分为 3 个内存池,新生代(Eden space)和存活区(Survivor space), 在大部分 GC 算法中有 2 个存活区(S0, S1),在我们可以观察到的任何时刻,S0 和 S1 总有一个是空的, 但一般较小,也不浪费多少空间。

异常

OutOfMemoryError

配置参数

(1)-Xms,初始堆大小,默认大小为物理内存的 1/64(<1GB),当(MinHeapFreeRatio 参数可以调整)空余堆内存小于 40%时,JVM 就会增大堆直到-Xmx 的最大限制

(2)-Xmx,最大堆大小,默认大小为物理内存的 1/4,当(MaxHeapFreeRatio 参数可以调整)空余堆内存大于 70%时,JVM 会减少堆直到 -Xms 的最小限制

(3)-Xmn,年轻代大小(1.4or lator) 注意:此处的大小为 eden+ 2 survivor space,增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun 官方推荐配置为整个堆的 3/8

(4)-XX:NewRatio,年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值(除去持久代) -XX:NewRatio=4 表示年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1/5

(5)-XX:SurvivorRatio,Eden 区与 Survivor 区的大小比值,如果设置为 8,则两个 Survivor 区与一个 Eden 区的比值为 2:8,一个 Survivor 区占整个年轻代的 1/10

2、Non-Heap

Non-Heap 本质上还是 Heap,只是一般不归 GC 管理,里面划分为 3 个内存池。 Metaspace, 以前叫持久代(永久代, Permanent generation), Java8 换了个名字叫 Metaspace. CCS, Compressed Class Space, 存放 class 信 息的,和 Metaspace 有交叉。 Code Cache, 存放 JIT 编译器编译后的本地机器 代码。

异常

OutOfMemoryError

配置参数

(1)-XX:MetaspaceSize,初始元数据区大小,达到该值就会触发垃圾收集进行类型卸载,同时 GC 会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过 MaxMetaspaceSize 时,适当提高该值。

(2)-XX:MaxMetaspaceSize,设置元空间的最大值,默认是没有上限的,也就是说你的系统内存上限是多少它就是多少。默认没有上限,在技术上,Metaspace 的尺寸可以增长到交换空间。

3、栈

每启动一个线程,JVM 就会在栈空间栈分配对应的线程栈, 比如 1MB 的空间(- Xss1m)。 线程栈也叫做 Java 方法栈。 如果使用了 JNI 方法,则会分配一个单独的本地方法栈 (Native Stack)。 线程执行过程中,一般会有多个方法组成调用栈(Stack Trace), 比如 A 调用 B,B 调用 C。。。每执行到一个方法,就会创建对应的栈帧(Frame)。

异常

OutOfMemoryError、StackOverflowError

配置参数

(1)-Xss,设置线程栈占用内存大小。

(2)-XX:ThreadStackSize;Thread Stack Size,(0 means use default stack size)

栈帧

栈帧是一个逻辑上的概念,具体的大小在一个方法编写完成后基本上就能确定。 比如返回值需要有一个空间存放,每个局部变量都需要对应的地址空间,此外还有给指令使用的 操作数栈,以及 class 指 针(标识这个栈帧对应的是哪个类的方法, 指向非堆里面的 Class 对象)。

(1)局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java 虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个 32 位以内的数据类型。


在 Java 程序编译为 Class 文件时,就在方法的 Code 属性中的 max_locals 数据项中确定了该方法所需分配的局部变量表的最大容量。(最大 Slot 数量)

一个局部变量可以保存一个类型为 boolean、byte、char、short、int、float、reference 和 returnAddress 类型的数据。reference 类型表示对一个对象实例的引用。returnAddress 类型是为 jsr、jsr_w 和 ret 指令服务的,目前已经很少使用了。


虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从 0~局部变量表最大容量。如果 Slot 是 32 位的,则遇到一个 64 位数据类型的变量(如 long 或 double 型),则会连续使用两个连续的 Slot 来存储。


(2)操作数栈

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的 Code 属性的 max_stacks 数据项中。


操作数栈的每一个元素可以是任意 Java 数据类型,32 位的数据类型占一个栈容量,64 位的数据类型占 2 个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过 max_stacks 中设置的最大值。


当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。


(3)动态链接

在一个 class 文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。


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


这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。


(4)调用栈

当一个方法开始执行时,可能有两种方式退出该方法:正常完成出口或异常完成出口

正常完成出口是指方法正常完成并退出,没有抛出任何异常(包括 Java 虚拟机异常以及执行时通过 throw 语句显示抛出的异常)。如果当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者(调用它的方法),或者无返回值。具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令确定。


异常完成出口是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。


无论是 Java 虚拟机抛出的异常还是代码中使用 athrow 指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器,就会导致方法退出。

无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态。


方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压如调用者的操作数栈中,调整 PC 计数器的值以指向方法调用指令后的下一条指令。

一般来说,方法正常退出时,调用者的 PC 计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。


下图为方法执行过程中栈帧工作流程:

图中局部变量区即为局部变量表,通过指令 istore 将变量 ab 放到局部变量表,该区域也在编译阶段就确定大小;

图中求值栈即为操作数栈,在编译为字节码时就确定了操作数栈最大深度。图中先求解 ab 之和,通过指令 iload 将 ab 的值分别加载到操作数栈,然后执行 iadd 指令求得结果后将结果放到栈顶;


我们自己写了段代码,看看其编译后 class 文件:

源代码如下:

public class MethodStackLearn {
public static int add(int a, int b) { double c = 2; return a + b; }
public static void main(String[] args) { System.out.println(add(1, 2)); }}
复制代码

javac -g 编译后通过 javap -verbose 查看字节码如下:

  Last modified 2020年12月13日; size 661 bytes  MD5 checksum e49a4103e5fb42d06db60dbdb880338f  Compiled from "MethodStackLearn.java"public class MethodStackLearn  minor version: 0  major version: 52  flags: ACC_PUBLIC, ACC_SUPERConstant pool:   #1 = Methodref          #8.#29         // java/lang/Object."<init>":()V   #2 = Double             2.0d   #4 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;   #5 = Methodref          #7.#32         // MethodStackLearn.add:(II)I   #6 = Methodref          #33.#34        // java/io/PrintStream.println:(I)V   #7 = Class              #35            // MethodStackLearn   #8 = Class              #36            // java/lang/Object   #9 = Utf8               <init>  #10 = Utf8               ()V  #11 = Utf8               Code  #12 = Utf8               LineNumberTable  #13 = Utf8               LocalVariableTable  #14 = Utf8               this  #15 = Utf8               LMethodStackLearn;  #16 = Utf8               add  #17 = Utf8               (II)I  #18 = Utf8               a  #19 = Utf8               I  #20 = Utf8               b  #21 = Utf8               c  #22 = Utf8               D  #23 = Utf8               main  #24 = Utf8               ([Ljava/lang/String;)V  #25 = Utf8               args  #26 = Utf8               [Ljava/lang/String;  #27 = Utf8               SourceFile  #28 = Utf8               MethodStackLearn.java  #29 = NameAndType        #9:#10         // "<init>":()V  #30 = Class              #37            // java/lang/System  #31 = NameAndType        #38:#39        // out:Ljava/io/PrintStream;  #32 = NameAndType        #16:#17        // add:(II)I  #33 = Class              #40            // java/io/PrintStream  #34 = NameAndType        #41:#42        // println:(I)V  #35 = Utf8               MethodStackLearn  #36 = Utf8               java/lang/Object  #37 = Utf8               java/lang/System  #38 = Utf8               out  #39 = Utf8               Ljava/io/PrintStream;  #40 = Utf8               java/io/PrintStream  #41 = Utf8               println  #42 = Utf8               (I)V{  public MethodStackLearn();    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 4: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       5     0  this   LMethodStackLearn;
public static int add(int, int); descriptor: (II)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=2 0: ldc2_w #2 // double 2.0d 3: dstore_2 4: iload_0 5: iload_1 6: iadd 7: ireturn LineNumberTable: line 7: 0 line 8: 4 LocalVariableTable: Start Length Slot Name Signature 0 8 0 a I 0 8 1 b I 4 4 2 c D
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=1, args_size=1 0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 3: iconst_1 4: iconst_2 5: invokestatic #5 // Method add:(II)I 8: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 11: return LineNumberTable: line 12: 0 line 13: 11 LocalVariableTable: Start Length Slot Name Signature 0 12 0 args [Ljava/lang/String;}SourceFile: "MethodStackLearn.java"
复制代码

此处我们只关注 add 方法,可以看到字节码中包含

descriptor,括号内为参数类型都是 I,表示 int,括号外的为返回值类型 int

flags,表示方法属性,为 public static 的

Code,表示方法内部代码,stack 即为操作数栈大小为 2,locals 为局部变量区大小占用 4 个 slot,args_size 为参数个数

LocalVariableTable,为局部变量表,Start 表示该变量有效起始范围,Length 表示变量作用范围长度,Name 表示变量名称,Signature 表示变量类型,比如 c 变量在 3 行生成存储后,即第四行生效,该方法一共长度为 8,所以 c 的 Length 为 4,类型为 D,即 double

4、直接内存

jdk1.4 加入了 NIO,引入了基于通道与缓冲区的 IO 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过存储在 java 堆中的 DirectByteBuffer 对象作为内存引用进行操作,可减少 Java 堆和 native 堆复制数据,提高性能,其分配大小受制于机器内存大小。

异常

OutOfMemoryError


三、问题收集分析

1、Java 中的各种常量池存在哪里?各常量池使用流程有啥区别?

全局字符串池(string pool 也有叫做 string literal pool)

全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到 string pool 中(记住:string pool 中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)

字符串池在 JDK1.6 以前是存放在永久区中,但是在 JDK1.7 以后就被转移到堆上。

例子:

public class StringTest {    public static void main(String[] args) {        //创建了两个对象,一份存在字符串常量池中,一份存在堆中        String s = new String("aa");        //检查常量池中是否存在字符串aa,此处存在则直接返回        String s1 = s.intern();        String s2 = "aa";
System.out.println(s == s2); //false System.out.println(s1 == s2); //true }}
复制代码

intern 方法逻辑:

在 JDK1.6 中,如果字符串常量池中已经存在该字符串对象,则直接返回池中此字符串对象的引用。否则,将此字符串的对象添加到字符串常量池中,然后返回该字符串对象的引用。

在 JDK1.7 中,如果字符串常量池中已经存在该字符串对象,则返回池中此字符串对象的引用。否则,如果堆中已经有这个字符串对象了,则把此字符串对象的引用添加到字符串常量池中并返回该引用,如果堆中没有此字符串对象,则先在堆中创建字符串对象,再返回其引用。


class 文件常量池(class constant pool)

用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。

一般包括下面三类常量:

  • 类和接口的全限定名

  • 字段的名称和描述符

  • 方法的名称和描述符


运行时常量池(runtime constant pool)

jvm 在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm 就会将 class 常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。

2、程序计数器和操作数栈区别?

程序计数器为线程所有,每个线程都会创建一个程序计数器,用于记录当前线程执行到哪个指令;操作数栈为栈帧中一部分,每个方法调用都会创建栈帧,用于记录存储方法中操作的数据。

3、线程私有分配缓冲区有什么作用?

TLAB 是线程的一块私有内存,它是虚拟机在堆内存的 eden 划分出来的,如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存, 只给当前线程使用, 这样每个线程都单独拥有一个 Buffer,如果需要分配内存,就在自己的 Buffer 上分配,这样就不存在竞争的情况,可以大大提升分配效率,当 Buffer 容量不够的时候,再重新从 Eden 区域申请一块继续使用。

TLAB 只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已,当一个 TLAB 用满 ,就会新申请一个 TLAB。

4、直接内存为非运行时数据区,不受 jvm 管控,使用场景除了 NIO 还有啥?


主要用作缓存。

除了 NIO 的 DirectByteBuffer 外,还可以通过 Unsafe 类创建堆外内存。但两种方法都只能手动分配和释放内存,无法做到像堆内存一样支持垃圾回收。

Ehcache 支持分配堆外内存,又支持 KV 操作,还无需关心 GC。

5、触发 Young GC 和 Full GC 条件是什么?

Young GC

(1)当 young gen 中的 eden 区分配满的时候触发。young GC 后 old gen 的占用量通常会有所升高。

Full GC

(1)触发 young GC 前,如果发现统计数据说之前 young GC 的平均晋升大小比目前 old gen 剩余的空间大,则触发 full GC;

(2)创建直接分配到老年代的大对象或大数组时,如果老年代空间不足,会触发 full ;

(2)在元数据区分配空间但已经没有足够空间时,会触发 full GC;

(3)System.gc()、heap dump 带 GC,默认也是触发 full GC


感谢 KiKiMing 老师以下各位大佬:

Java JVM 参数设置大全

Java程序计数器刨根问底,大部分程序员都收藏起来了

虚拟机栈的局部变量

JVM元数据区

字符串常量池、class常量池和运行时常量池

深入理解字符串常量池

深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)

JAVA堆外内存的简介和使用


发布于: 2020 年 12 月 13 日阅读数: 36
用户头像

懒AI患者

关注

喜欢新东西 2018.09.20 加入

慢慢成长的码农

评论

发布
暂无评论
Java内存模型