目录
· 多线程时在堆内的分配对象地址细节(TLAB,Thread Local Allocation Buffer)
· 垃圾回收器:Minor GC、Major GC、Full GC
运行时数据区可以分为五大部分(如下图):
- 其中方法区和堆空间是每个线程共享的
- 虚拟机栈 每个线程独有一份
- 虚拟机栈是当前线程所需要执行的方法以栈的数据结构进行存储
- 程序计数器 每个线程独有一份
- 程序计数器是存放当前线程执行的方法中的某一行指令的地址 (为了在线程之间切换后找到之前该线程执行的位置)

JVM中线程的说明
- JVM允许一个应用有多个线程并行的执行。
- 在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。
- 操作系统负责所有线程的安排调度到任何一个可用的CPU上,一旦本地线程初始化成功,就会调用Java线程中的run()方法。
-
守护线程、普通线程:
-
守护线程:如GC线程(对在JVM中不同种类的垃圾收集行为提供了支持)……
-
当虚拟机中只剩下守护线程时就可以退出了
程序计数器(PC寄存器)

反编译后的class文件,体现pc寄存器的作用:5代表的就是当前线程所执行的指令地址,执行引擎会取出pc寄存器存放的地址的指令,然后获取局部变量表中的数据,编译成机器指令让CPU执行

为什么使用PC寄存器记录当前线程的执行地址?
- 因为CPU需要不停的切换各个线程,当CPU从另一个线程切换回来以后,CPU需要知道切换回来的线程执行到了哪儿,这个时候PC寄存器就可用提供要指行的指令地址
所以从这里也可以看出,PC寄存器是线程私有的
虚拟机栈
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台的CPU架构不同,所以不能设计为基于寄存器的。
- 优点:跨平台
- 缺点:性能下降,实现同样的功能需要更多的指令
可以使用参数 -Xss [内存大小] 来设置栈的大小,比如-Xss 256k
当一个线程运行起来后,线程所执行的方法就会以栈帧的方式进入到当前线程的虚拟机栈中。如下图:

那么栈帧是一个什么样的结构呢?
· 栈帧
每个栈帧存储着五种数据:
- 局部变量表(Local Variable)
- 操作数栈(Operand Stack)
- 动态链接(Dynamic Linking)
- 方法返回地址或异常退出的定义
- 一些附加信息
如图:

·· 局部变量表
- 主要存储方法的参数、定义在方法内的局部变量,包括基本数据类型(8大),对象的引用地址,返回值地址。
- 局部变量表中存储的基本单元为变量槽(Slot),32位(4字节)以内的数据类型占一个slot,64位(long,double)的占两个slot。
- 局部变量表是一个数字数组,byte、short、char都会被转化为int,boolean类型也会被转化为int,0代表false、非0代表true。
- 局部变量表的大小是在编译期决定下来的,所以在运行时它的大小是不会变的。
- 局部变量表中含有直接或者间接指向的引用类型变量时,不会被垃圾回收处理。
将图6的class文件进行 javap -v 解析后得到图7:
可以看到局部变量表中存放着 方法参数args,类型为 Ljava/lang/String,其中L表示是该变量是引用数据类型。


也可以在jclasslib插件中查看:

图8中 第一行的起始PC代表该变量的起始作用位置在字节码 Code 的第0行(第0行开始起效,一般在声明后的下一行),作用长度为11行(表示作用范围,是在Code中)。
为了说明作用范围,看图9:
图9中第一行起始PC为0,行号为9,去图6中找到第9行,说明args参数在第9行开始生效(为方法体的开始)。

当方法是非静态方法时,this会被分配在局部变量表中index为0的位置(图11中的序号就是index)。(静态方法中的局部变量表没有存放this变量)。比如图10的fun1()方法:


当一个变量的作用失效时,下一个变量就会被分配到失去作用的变量槽上,如图12、13:
我们可以发现变量b的作用范围出了{ }就失效了,变量c就在b的变量槽上,序号(index)都为2。


·· 操作数栈
- 操作数栈,主要用于存储临时数据,作为计算过程中变量的临时储存空间和保存计算的中间结果。
- 和局部变量表一样:32位(4字节)以内的数据类型占一个深度,64位(long,double)的占两个深度。
- 说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
举一个简单案例:
图15为图14代码的字节码,
Code中:
- bipush 9:将9压入操作数栈中
- istore_1:出栈并将9存放到局部变量表中index为1的变量槽中
- bipush 29、istore_2也同理
- iload_1、iload_2表示将局部变量表中index为1、2的变量槽中的数据压入操作数栈
- iadd将操作数栈中的数相加,9+29=38
- istore_3:出栈并将32存放到局部变量表中index为3的变量槽中


·· 动态链接
指向运行时常量池中该栈帧中所用到的引用(如:方法,常量,类……)
·· 方法返回地址
一个方法结束有两种情况:
- 正常结束(调用者的pc计数器的下一条指令的地址)
- 出现未处理的异常(通过异常表确定)
·· 本地方法栈
和虚拟机栈类似(管理本地方法)。如何定义并使用本地方法
堆
- 一个JVM实例只有一个堆内存
- 其空间大小在JVM启动的时候确定
- 堆可以在物理内存上不连续,但在逻辑上时连续的
- 堆内存可以通过JVM启动参数来调节大小 -Xms10m -Xmx10m (表示初始内存大小和最大内存大小,一般在生产环境中设置成一样的值)
· 查看堆的大小
通过设置JVM启动参数-Xms10m -Xmx10m启动HeapDemo类:
-XX:-UseAdaptiveSizePolicy 先关闭内存自适应
public class HeapDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("start...");
Thread.sleep(1000000);
System.out.println("end...");
}
}
并打开JAVA_HOME/bin/jvisualvm.exe程序(jdk8),查看Visual GC,可以发现下图16中红框的四个部分加起来(即堆)的内存大小是10M:

当将参数改为-Xms20m -Xmx20m时,可以看到图17,那四个区域内存大小和 约为20M:

堆空间的细分结构:图16中的四个框 [Eden Space]、[Surviver 0]、[Surviver 1]为新生区,[Old Gen]为养老区,如图18:

在win10控制台中也可以使用命令查看:S0C+S1C+EC+OC=(512+512+2048+7168)/1024=10M

也可以获取Runtime运行实例,来获取堆内存的大小:
public class HeapDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("start...");
//获取JVM运行实例(单例)
Runtime runtime = Runtime.getRuntime();
//获取堆的内存总量(单位为byte)
long initialMemory = runtime.totalMemory();
//获取堆的最大内存存量(单位为byte)
long maxMemory = runtime.maxMemory();
System.out.println("Xms:"+initialMemory/1024/1024.0+"m");
System.out.println("Xmx:"+maxMemory/1024/1024.0+"m");
}
}
输出结果为:
start...
Xms:9.5m
Xmx:9.5m
为什么相差了0.5M呢?图19中去掉一个生存区1后:S0C+EC+OC=(512+2048+7168)/1024=9.5M,原因是生存区0、1只有一个可以存放对象,另一个作为gc的复制区。
加上JVM参数:-XX:+PrintGCDetails会在控制台打印信息:
Heap
PSYoungGen total 2560K, used 1767K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 86% used [0x00000000ffd00000,0x00000000ffeb9e10,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
Metaspace used 3252K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 351K, capacity 388K, committed 512K, reserved 1048576K
在字节码文件中,new表示在堆中创建对象,如图18:

· 堆的默认大小
可以发现在不设置堆的大小时,堆的初始内存默认为电脑内存的 64分之1,堆的最大内存容量为电脑内存的4分之1:
public class HeapSpaceDemo {
public static void main(String[] args) {
//获取JVM运行实例(单例)
Runtime runtime = Runtime.getRuntime();
//获取堆的内存总量(单位为byte)
long initialMemory = runtime.totalMemory();
//获取堆的最大内存存量(单位为byte)
long maxMemory = runtime.maxMemory();
System.out.println("Xms:"+initialMemory+"byte");
System.out.println("Xmx:"+maxMemory+"byte");
System.out.println("Xms:"+initialMemory/1024/1024+"m");
System.out.println("Xmx:"+maxMemory/1024/1024+"m");
System.out.println("系统内存大小:" + initialMemory * 64.0 /1024/1024/1024+"G");
System.out.println("系统内存大小:" + maxMemory * 4.0 /1024/1024/1024+"G");
}
}
输出结果:
Xms:257425408byte
Xmx:3791650816byte
Xms:245m
Xmx:3616m
系统内存大小:15.34375G
系统内存大小:14.125G
· 新生代与老年代(新生区与老年区)

配置新生代与老年代的比例(开发中一般不配置):
- -XX:NewRatio=2,新生代 : 老年代 = 1 : 2 (默认)。
可以使用 jinfo -flag NewRatio [进程号] 来查看新生代与老年代的比例:

配置Eden与Survivor的比例(开发中一般不配置):
- -XX:SurvivorRatio=8,Eden : Survivor0 : Survivor1 = 8 : 1 : 1(默认)。

· 对象在堆区的产生和消亡(内存分配策略)
- ①对象的产生首先在Eden区,当Eden区满时,②进行垃圾回收,由YGC/Minor GC垃圾回收器回收(为年轻代的垃圾回收器),③没有被回收的对象进入Surviver0区,对象年龄为1。
- ①当Eden区再次满时,进行 YGC/Minor GC 垃圾回收,②这时没有回收的对象进入空的Surviver1区(空的Surviver也称 to区),③之前进入Surviver0区中的对象也进行垃圾回收,没有被回收的对象转移到Surviver1区,并且年龄增大1,Surviver0区清空(变为to区),Surviver1变为from区,下次Eden区来的对象就往to区放。
- 当Surviver的from区中的年龄为15时,晋升到Old区。
- (15也可以修改,通过 -XX:MaxTenuringThreshold=<N> 设置)。
- 当Surviver的from区中相同年龄对象的大小总和超过Surviver区的一半时,也晋升到Old区。
内存分配的特殊情况:
- 当对象的大小比Eden区要大时,直接进入Old区。
- 当对象从Eden区到to区时,to区放不下,直接进入Old区。
- 如果老年代Old区放不下对象,先执行 FGC/Major GC垃圾回收器,然后再放入对象,如果放不下,报OOM异常(前提:关闭了JVM自动调整堆的大小)。

为什么Surviver要有两个区(from区和to区)?
· 多线程时在堆内的分配对象地址细节(TLAB,Thread Local Allocation Buffer)
堆是多个线程共享的区域,一个线程在堆中的某个地址时,可能有其他线程也使用了堆中的同一地址,多个线程在堆中同一个地址创建对象,在多个线程划分内存空间是不安全的。
为了避免多个线程同时操作同一个地址,进行加锁(影响分配速率)。
设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,会申请一块指定大小的内存的区域 TLAB(线程分配缓存区)。每个线程划分好区域,让线程在创建对象时用的不是同一个地址,这样就能解决划分内存空间安全的问题,也不用进行加索。

TLAB中有start、end、top指针,start和end表示范围,而 top 就是里面的分配指针,一开始指向跟 start 同样的位置,然后逐渐分配,直到再要分配下一个对象就会撞上 end 的时候就会触发一次 TLAB refill(重新申请一个TLAB)或者TLAB有一个参数 _refill_waste_limit(最大浪费空间),当剩余空间大于最大浪费空间时,也会重新申请一个TLAB,TLAB也有缺点:TLAB允许浪费空间,导致Eden区空间不连续,积少成多。
如果 对象所需空间 > TLAB空间 时,直接在TLAB外进行加锁创建。
· 逃逸分析(对象在栈上分配?)
逃逸分析:
- 当一个对象在方法内定义后只在方法内部使用,则认为没有发生逃逸。
- 当一个对象在方法内定义后被其他方法的变量引用,认为发生了逃逸。
--栈上分配:
所有的对象都会在堆中分配吗?
随着JIT编译器的发展和逃逸分析的技术的成熟,对象在特殊情况也会分配在栈上。
- 特殊情况:经过逃逸分析后发现,一个对象并没有逃逸出方法时,就会被优化到栈上分配,就无需在堆中分配了,也无需进行垃圾回收。
逃逸分析不仅对代码进行 栈上分配 的优化,还有同步省略、标量替换。
--同步省略:经过逃逸分析之后,发现同步锁对象未发生逃逸,只对一个线程起作用,那么该同步就会被省略(执行时不加锁)。
--标量替换:经过逃逸分析之后,发现对象未逃逸,就可以将该对象打散成局部变量分配在栈上,比如。(有点 栈上分配 的意思)。
但是在Hotspot虚拟机中,栈上分配并未被采用,而标量替换被默认采用。
参数设置:
在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析。如果使用的是较早的版本,开发人员则可以通过:-XX:+DoEscapeAnalysis 开启逃逸分析(-XX:-DoEscapeAnalysis 关闭逃逸分析)
-XX:+PrintEscapeAnalysis 查看逃逸分析的筛选结果。
-XX:+EliminateAllocations 开启标量替换(默认开启)
· 查看GC的执行情况
使用 -XX:+PrintGCDetails 参数可查看GC的执行情况:
public class HeapDemo3 {
public static void main(String[] args) {
List<Byte[]> list = new LinkedList<>();
while (true) {
Byte[] b = new Byte[1024*1024];//1M
list.add(b);
}
}
}
设置堆大小为6M -Xms6m -Xmx6m,运行结果:
[GC (Allocation Failure) [PSYoungGen: 1024K->504K(1536K)] 1024K->568K(5632K), 0.0030333 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1138K->504K(1536K)] 1202K->680K(5632K), 0.0009270 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 504K->488K(1536K)] 680K->704K(5632K), 0.0005688 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 488K->0K(1536K)] [ParOldGen: 216K->622K(4096K)] 704K->622K(5632K), [Metaspace: 3226K->3226K(1056768K)], 0.0057793 secs] [Times: user=0.08 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 622K->622K(5632K), 0.0003648 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 622K->603K(4096K)] 622K->603K(5632K), [Metaspace: 3226K->3226K(1056768K)], 0.0063481 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 1536K, used 64K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 6% used [0x00000000ffe00000,0x00000000ffe10098,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 4096K, used 603K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
object space 4096K, 14% used [0x00000000ffa00000,0x00000000ffa96fd8,0x00000000ffe00000)
Metaspace used 3257K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 353K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.self.java.HeapDemo3.main(HeapDemo3.java:17)
分析:

堆空间的分代思想:
分代的唯一理由:优化GC的性能,减少大量对象的回收判断。
GC线程在执行时,引发STW,用户线程是执行不了的,如果判断大量的对象是否为垃圾,会降低JVM性能。
· 垃圾回收器:Minor GC、Major GC、Full GC
部分收集:
Minor GC / Young GC:负责年轻代的垃圾回收(Eden,S0,S1)。
- 触发机制:在Eden区满时触发,对Eden区和Surviver区进行垃圾回收。
Major GC / Old GC:负责老年代的垃圾回收。
- 触发机制:在老年代满时触发,出现了Major GC通常会伴随一次Minor GC(但也不是绝对),且先触发Minor GC。执行速度比Minor GC慢10倍以上
整堆收集:
Full GC:收集整个java堆和方法区的垃圾收集。
- 触发机制:调用 System.gc(),系统建议执行Full GC;老年代空间不足时;方法区空间不足时。
· 堆空间常用的JVM参数:
JVM有哪些参数可以在官网中查看 https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
堆空间常用的jvm参数:
-XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
-XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
具体查看某个参数的指令: jps:查看当前运行中的进程
jinfo -flag SurvivorRatio 进程id
-Xms:初始堆空间内存 (默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的GC处理日志
① -XX:+PrintGC ② -verbose:gc 打印gc简要信息:
-XX:HandlePromotionFailure:是否设置空间分配担保
对 -XX:HandlePromotionFailure 参数的说明:
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
·如果大于,则此次Minor GC是安全的
·如果小于,则虚拟机会查看-xX:HandlePromotionFailure设置值是否允许担保失败。
-如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
--如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
--如果小于,则改为进行一次Full GC。
-如果HandlePromotionFailure=false,则改为进行一次Full GC。
在JDK6 Update24之后(JDK7) ,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。(相当于HandlePromotionFailure=true)
方法区
- Hotspot虚拟机中,方法区是独立于堆的内存空间。
- 方法区是线程共享的。
- jdk7及以前,把方法区称为永久代,jdk8开始,称为元空间。
- jdk7及以前,可以通过 -XX:PermSize=[size]来设置初始空间(默认大小为20.75M),-XX:MaxPermSize来设置最大空间(默认大小:64M/82M(32/64位))。
- jdk8开始,方法区的大小可以通过 -XX:MetaspaceSize来设置初始空间(windows下默认大小为≈21M),-XX:MaxMetaspaceSize来设置最大空间(值为-1,表示无限制)。
方法区的内部结构(对于每个加载的类,方法区中储存以下信息):
- 类型信息:全类名,直接父类的全类名,直接接口的有序列表,类的修饰符(public、private、abstract、final)的子集。
- 域信息(类成员信息):类成员的类型,名称,修饰符(static、public、private、final、volatile、transient)的子集。
- 方法的信息:方法名称,方法的返回类型(或void),方法的参数数量和类型,方法的修饰符(public、private、protected、static、final、synchronized、native、abstract)的子集。方法的字节码(bytecodes即JIT代码缓存)、操作数栈、局部变量表(abstract和native方法除外),异常表。
- 运行时常量池:字节码文件(ClassFile)中包含常量池,运行时会加载该常量池,加载后称为运行时常量池。可以看成一张表,指令需要根据这张表找到要执行的类,方法名,参数,字面量等。
虚拟机通过加载Class File,将下列信息加载进方法区中,虽然看不到方法区的内容,但是可以通过字节码文件来学习方法区加载了哪些内容。
常量池(为ClassFile的一部分):
- 包含各种字面量、类型、域、方法的符号引用。
运行时常量池:
- 当常量池被加载后,符号引用就变为了真实地址了。通过索引来访问。
- 具备动态性,如 strObj.intern() 方法,如果strObj没有在运行时常量池中,就会将strObj的字面量添加到运行时常量池中。
以上是Java虚拟机规范中说明的,但是不同的Java虚拟机的实现还是有些不同,在Hotspot虚拟机中不同jdk版本的变化:
jdk1.6及以前 | 有永久代,字符串常量池、静态变量(引用)在永久代中,new对象还是在堆中 |
jdk1.7 | 有永久代,字符串常量池、静态变量(引用)从永久代转移到了堆空间中 |
jdk1.8及以后 | 无永久代,为元空间,类型信息、方法、常量保存在元空间中,字符串常量池、静态变量在堆空间中 |
为什么字符串常量池被移到了堆空间?
- 因为永久代的回收垃圾效率低,使用full gc进行回收,full gc的触发条件是老年代不足,永久代不足时才触发。
- 当有大量的字符串在创建时,回收效率低会导致永久代空间不足。