1. JVM 简化架构概览

1.1 运行时数据区(即内存区域,又称 JVM 内存结构)
如上面的 JVM 简化架构图所示,内存区域分为如下五个部分(这五个部分统称为运行时数据区):
-
PC寄存器(又称程序计数器); -
Java栈(又称Java虚拟机栈,或虚拟机栈); -
Java堆; -
方法区;
注意:运行时常量池是划分在方法区中的。
-
本地方法栈(
Native栈)
1.1.1 PC 寄存器(又称程序计数器)
PC(Program Counter)寄存器说明:
-
PC寄存器是线程私有的,即每个线程拥有一个PC寄存器,用来存储指向下一条指令的地址。PC寄存器中指令的存储顺序就是线程中代码的执行顺序。 -
在创建线程的时候,创建相应的
PC寄存器。 -
执行本地方法(即
native修饰的jni方法)时,PC寄存器的值为undefined。 -
PC寄存器是一块较小的内存空间,是唯一一个在JVM规范中没有规定OutOfMemoryError的内存区域。
1.1.2 Java 栈(又称 Java 虚拟机栈,或虚拟机栈)
Java 栈的说明:
-
Java栈是由一系列的帧(Frame)组成的。因此,Java栈也称为帧栈。Java栈和PC寄存器一样,都是线程私有的。由帧组成的栈叫做帧栈;
存放在帧栈中的帧叫做栈帧。
-
帧(即 栈帧)可以用来保存一个方法的局部变量、操作数栈(
Java没有寄存器,所有参数传递使用操作数栈)、常量池指针、动态链接、方法返回值等。 -
每一次方法调用时都会创建一个帧,并压栈。退出方法时,修改栈顶指针就可以把栈帧中的内容销毁。
帧是方法调用相关的。方法调用开始时创建帧,方法调用结束后销毁帧。
-
局部变量表 存放了编译期可知的各种基本数据类型和引用类型,每个
slot(槽位) 存放32位的数据,long、double占两个slot。局部变量表中的槽位(
slot)是可以复用的。参考
Code属性在class文件中的解析举例。参考 《字节码执行引擎》 中的 局部变量表
Java 栈的优缺点:
-
优点:存取速度比堆快,仅次于寄存器。
-
缺点:存在栈中的数据大小、生存期是在编译期决定的,缺乏灵活性。
1.1.3 Java 堆
Java 堆说明:
-
用来存放应用系统创建的对象和数组。所有线程共享
Java堆。 -
GC主要就是管理堆空间。对分代GC来说,堆也是分代的。分代只是一种思想,内存中并没有分代的说法。
Java 堆的优缺点:
-
优点:运行期动态分配内存大小;自动进行垃圾回收。
-
缺点:效率相对较慢。
1.1.4 方法区
方法区的说明:
-
方法区是线程共享的,通常用来保存加载的类的结构信息(即
class文件中的二进制字节流数据,包括:类名、继承关系、字段、方法、附加属性等信息数据)。参考 类加载要完成的功能。
参考
Class文件的格式。 -
方法区通常和元空间(
Meta Space)关联在一起。但具体的跟JVM实现和JDK版本有关。JDK 8之前,方法区和永久区关联在一起;JDK 8之后,废除了永久区,将方法区和元空间关联在一起。 -
JVM规范把方法区描述为堆的一个逻辑部分,但方法区有一个别名(即Non-heap非堆),应该是为了与Java堆相互区分开来。
1.1.5 运行时常量池
运行时常量池是 class 文件中每个类或接口的 常量池表,在运行期间的表示形式,通常包括:JDK 版本、类名、父类或父接口名、字段、方法等信息。
运行时常量池分配在方法区中。
因此,说方法区中保存了类的结构信息是正确的。
通常在加载类和接口到 JVM 后,就创建了相应的运行时常量池。
1.1.6 本地方法栈(Native 栈)
在 JVM 中用来支持 native 方法执行的栈就是本地方法栈。
1.2 Java 栈、Java 堆、方法区三者间的交互关系

如上图所示:
-
对于
Java栈中的局部变量表中的类类型的局部变量,会指向Java堆中的类对象; -
对于
Java堆中的类对象,它不仅包括实例数据,还包括指向该类元数据信息的指针; -
对于类的元数据信息,就是方法区中保存的类的结构信息等,即
Java堆中的类对象还会持有对方法区中数据的引用。
2. Java 堆
2.1 堆内存概述
Java 堆用来存放应用系统创建的对象和数组,所有线程共享 Java 堆。
在运行时期为 Java 堆动态分配内存大小,并自动进行垃圾回收。
Java 垃圾回收(GC)主要就是回收堆内存。
对分代
GC来说,堆也是分代的。
堆内存在逻辑上是连续的,在物理上可以是不连续的。
2.2 堆内存的结构

如上图所示:整个堆大小 = 新生代 + 老年代
2.2.1 新生代(Young Generation)
新生代 = Eden + 存活区
新生代用来存放新分配的对象。
新生代中经过垃圾回收后,没有回收掉的对象被复制到老年代。
2.2.1.1 Eden(伊甸园)区
2.2.1.2 存活区(From Space 和 To Space)
存活区(Survivor Space)又分为 From Space 和 To Space。
但使用时,只能使用其中一个。
之所以分为两个,是为了在复制时用到。
2.2.2 老年代(Old Generation,又称年老代)
老年代中只有一个 Tenured Space(年老区)。
老年大存储的对象比新生代存储的对象的年龄大得多。
老年代存储一些大对象。
对一些占用空间较大的对象,可能不会先进入新生代,而是直接存放在老年代。
2.2.3 永久代(又称持久代,已废弃)& 元空间
从前的持久代(即用来存放 Class、Method 等元信息的区域)从 JDK 8 开始去掉了。被元空间(MetaSpace)所取代。
元空间并不在虚拟机里面,而是直接使用本地内存。
2.3 对象的内存布局(对象头、实例数据、对齐填充)
以 HotSpot 虚拟机为例,对象在内存中的存储布局,分为:
-
对象头
对象头包含两个部分: 1. Mark Word:存储对象自身的运行数据,如:HashCode、GC 分代年龄、锁状态标志等。 2. 类型指针:对象指向它的类的元数据(即类的结构信息)的指针(指向方法区)。 -
实例数据
真正存放对象实例数据的地方。 -
对齐填充
这部分不一定存在。也没有特别的含义,仅仅是占位符。 因为 HotSpot 虚拟机要求对象起始地址都是 8 字节的整数倍,如果不是,就要对齐填充。
2.4 对象的访问方式(使用句柄、使用指针)
在 JVM 规范中,只规定了 reference 类型(引用类型)是一个指向对象的引用,但没有规定这个引用具体如何去定位,并访问堆中对象的具体位置。
因此,对象的访问方式取决于 JVM 的实现。目前主流的访问方式有:
-
使用句柄:
Java 堆中会划分出一块内存来做为句柄池,reference 中保存句柄的地址。 即引用指向句柄池中的句柄。 句柄中存储对象的实例数据,以及类的元数据的地址。
使用句柄的优缺点:
-
优点:当对象发生改变时,只需要修改句柄中到对象数据的指针即可。不需要修改
reference的指向。’ -
缺点:使用句柄其实就是采用间接引用的方式访问对象,于是,
reference需要2次寻址才能访问到对象。即访问速度较慢。
-
-
使用指针(
HotSpot虚拟机采用这种方式来访问对象):Java 堆中会存放访问类的元数据的地址, reference 存储的就直接是对象的地址。
使用指针的优缺点:
-
优点:
reference直接指向对象,只需1次寻址。即访问速度较快。 -
缺点:对象发生改变时,需要修改
reference的指向。
-
3. Java 内存分配相关的参数设置
3.1 Trace 跟踪参数
3.1.1 -Xlog:gc(打印 GC 日志的简要信息)
3.1.2 -Xlog:gc*(打印 GC 日志的详细信息)
3.1.3 -Xlog:gc:<filename>(输出 GC 日志信息到指定文件)
如 -Xlog:gc:garbage-collection.log,其中 garbage-collection.log 是保存 GC 日志的文件。文件名前面还可以加路径,路径目录必须事先创建好。
3.1.4 -Xlog:gc+heap=debug(打印 GC 日志信息和堆信息)
3.1.5 GC 日志信息的格式
不同垃圾收集器的 GC 日志格式可能不同。GC 日志信息中主要包含以下内容:
-
GC发生的时间,也就是JVM从启动以来经过的秒数。 -
日志级别信息,和日志类型标记。
-
GC识别号。 -
GC类型,和GC的原因说明。 -
容量:
GC前容量-> GC后容量(该区域总容量)。 -
GC持续时间,单位:秒。有的收集器会有更详细的描述,比如: 1. user:表示应用程序消耗的时间; 2. sys:表示系统内核消耗的时间; 3. real:表示操作从开始到结束的时间。
举例如下:

3.2 Java 堆的参数
3.2.1 -Xms<size>(初始堆大小)
设置堆的最小空间大小为 size,同时也将堆的初始空间大小设置为 size。size 必须是 1024 的倍数,且 size 必须大于 1MB。如:-Xms6291456-Xms6144k-Xms6m以上三种方式只是 size 的单位不同,结果都是将 size 设置为 6m。默认为物理内存的 1/64。如果不想将堆的最小空间大小和初始空间大小设置成一样,可以使用:
1. -XX:MinHeadpSize 单独设置最小空间大小;
2. -XX:InitialHeapSize 单独设置初始空间大小。
3.2.2 -Xmx<size>(最大堆大小)
默认为物理内存的 `1/4`。-Xmx 等价于 -XX:MaxHeapSize建议将 -Xms 和 -Xmx 设置成一样大小,即:初始堆大小 = 最大堆大小。这样,每次 GC 后就不需要再去调整堆大小了。
使用示例如下:

3.2.3 -Xmn<size>(新生代大小)
默认为整个堆的 3/8。因为新创建的对象一般都是存放在新生代中的,所以:
1. 如果新生代的内存太小,能够存放的对象就少,就会导致频繁的 GC;
2. 如果新生代的内存太大,就会存放过多的对象,从而导致一次 GC 的耗时较长。-Xmn 同时设置了新生代的初始空间大小和最大空间大小。也可以使用 -XX:NewSize 和 -XX:MaxNewSize 分别单独设置。
3.2.4 -XX:NewRatio(老年代与新生代的比例)
-XX:NewRatio 是设置新生代和老年代的占比关系。而 -Xmn 是设置新生代的固定内存大小。也就是说,对于 -XX:NewRatio, 新生代的内存大小取多少,是虚拟机根据比例来算的。默认值为 -XX:NewRatio=2,表示 老年代 : 新生代 = 2 : 1,即老年代占堆内存的 2/3新生代占堆内存的 1/3注意:如果 -Xms = -Xmx,且设置了 -Xmn,那么不用再设置 -XX:NewRatio。
3.2.5 -XX:SurvivorRatio(Eden 区和 Survivor 区的比例)
当 -XX:SurvivorRatio = 8 时,则 eden : from : to = 8 : 1 : 1,即:eden 区占新生代内存的 8/10;from space 占新生代内存的 1/10;to space 占新生代内存的 1/10。
3.2.6 -XX:+HeapDumpOnOutOfMemoryError(导出 OOM 时的堆信息)
导出 OOM 时的堆信息就是导出一个堆存储文件(hprof)文件。如果不通过参数 -XX:+HeapDumpPath 指定导出的 hprof 文件的路径,
则默认在程序运行时所在的当前目录下生成 hrof 文件。
3.2.7 -XX:+HeapDumpPath(OOM 时堆信息的导出文件路径)
如果不指定导出路径,默认为 `Java` 程序运行时所在的当前目录。
3.2.8 -XX:OnOutOfMemoryError(在 OOM 时,执行一个脚本)
3.2.9 示例:GC 前后对象在堆内存中的分布变化

3.2.10 示例:分配的新生代内存不能超过最大堆内存的一半
当 -Xmn 设置的新生代内存超过了 -Xmx 设置的最大堆内存的一半时,实际给新生代分配的内存可能比 -Xmn 设置的小。

3.2.11 示例:导出堆存储文件并使用 MAT 进行内存分析






3.2.12 示例:输出 GC 日志到指定文件

3.2.13 示例:-XX:NewRatio 设置老年代与新生代的比例

3.2.14 示例:-XX:SurvivorRatio 设置 Eden 区和 Survivor 区的比例

3.2.15 示例:分析内存溢出(OOM)时 GC 日志信息

3.3 Java 栈的参数
3.3.1 -Xss(栈的大小)
通常只有几百 K,决定了函数调用的深度。栈是由帧组成的,函数调用开始时创建帧,函数调用结束后销毁帧。
当函数嵌套调用时,就会同时有多个帧存在,嵌套层级越深,帧越多,
因此说 -Xss 所设置的栈大小,决定了一个栈中能同时存放下多少个帧,即确定了函数调用的嵌套深度。
3.3.2 示例:通过递归函数模拟 StackOverflowError(栈溢出异常)

3.4 元空间的参数
3.4.1 -XX:MetaspaceSize(初始空间大小)
3.4.2 -XX:MaxMetaspaceSize(最大空间大小)
默认是没有限制的
3.4.3 -XX:MaxMetaspaceFreeRatio(GC 后的最大剩余百分比)
在 GC 之后,最小的 Metaspace 剩余空间容量的百分比。
3.4.4 -XX:MinMetaspaceFreeRatio(GC 后的最小剩余百分比)
在 GC 之后,最大的 Metaspace 剩余空间容量的百分比。