下篇地址 |
---|
jvm内存模型与垃圾回收(下) |
0. JVM 整体结构
1. 类的加载过程
通过编译器编译后生成class文件,然后类加载子系统进行加载链接初始化
1. 加载
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类各种数据的访问入口
2. 链接
- 验证
- 确保 Class 文件字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
- 主要包括四种验证,
文件格式验证,元数据验证,字节码验证,符号引用验证
- 准备
- 为类变量分配内存并且设置该类变量的默认初始值
这里不包含 final 修饰的 static,因为final在编译的时候就会分配了,准备阶段会显式初始化
这里不会为实例变量分配初始化
,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中
- 解析
- 将常量池内的符号引用转换为直接引用的过程
- 符号引用:一组符号来描述所引用的目标
- 直接引用:直接指向目标的指针、相对偏移量或一个简介定位到目标的句柄
- 解析动作
主要针对类或接口、字段、类方法、接口方法、方法类型等
- 将常量池内的符号引用转换为直接引用的过程
3. 初始化
- 初始化阶段就是执行类构造器方法< clinit>()的过程
(不重要)
该方法是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
2. 加载器分类:
- 系统类加载器
- 扩展类加载器
- 引导类加载器:加载Java的核心库(java. javax.等开头的类)
1. 程序计数器(寄存器)
- 作用:是记录下一条 jvm 指令的执行地址行号。
- 特点
- 是线程私有的
- 不会存在内存溢出
- 工作流程:
- 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
- 多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。
2 虚拟机栈
0. 栈中存储了什么
1. 栈帧
栈中存在栈帧(调用一个方法就会产生一个当前方法的栈帧)
2. 栈帧中存储了什么
局部变量
操作数栈(或表达式栈)
- 指向运行时常量池的方法引用
- 方法返回地址 - 方法正常退出或者异常退出的定义
- 一些附加信息
1. 局部变量表
一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
数据类型包括类基本数据类型,对象引用,以及返回地址类型
2. 操作数栈
3.
4.
1. 定义:
- 每个线程运行需要的内存空间,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法
- 问题辨析:
- 垃圾回收是否涉及栈内存?
不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。 - 栈内存分配越大越好吗?
不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。 - 方法呢的局部变量是否线程安全
如果方法内部的变量没有逃离方法的作用访问,它是线程安全的
如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。
- 垃圾回收是否涉及栈内存?
2. 栈内存溢出
- 栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小!
3. 线程运行诊断
案例一:cpu 占用过多
解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程
- top 命令,查看是哪个进程占用 CPU 过高
- ps H -eo pid, tid(线程id), %cpu | grep 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高
- jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。
3 本地方法栈
一些带有 native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。
4 堆
1. 定义
Heap堆
- 通过new关键字创建的对象都会被放在堆内存
特点 - 它是线程共享,堆内存中的对象都需要考虑线程安全问题
- 有垃圾回收机制
ps: jdk1.8后,字符串常量池被放在了 堆中
堆在逻辑上被分为新生区+养老区+元空间,元空间是属于本地内存
2. 堆内存溢出
java.lang.OutOfMemoryError: Java heap space
可以使用 -Xmx8m 来指定堆内存大小。
3. 堆内存诊断
- jps 工具
查看当前系统中有哪些 java 进程 - jmap 工具
查看堆内存占用情况 jmap - heap 进程id
jhsdb jmap --heap --pid
- jconsole 工具
jconsole
图形界面的,多功能的监测工具,可以连续监测
4. jvisualvm 工具
5 方法区
1. 基本理解及组成
- 方法区与堆一样,是各个线程共享的内存区域
- 方法区在JVM启动的时候创建,物理内存可以不连续
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法去溢出,虚拟机会抛出异常,OutOfMemoryError:Metaspace,元空间导致的内存溢出
1.1 方法区中存储了什么
-
用于存放
类的元数据信息、常量池、静态变量、即时编译器编译后的代码等
(ps:类的元数据信息包括 本类、父类的全类名等) -
方法区的内部结构包括:
-
运行时常量池
:存放编译器生成的常量,如字符串常量、整型常量等,以及通过类加载器加载的类、接口、方法和字段的符号引用。 -
类信息
:存放类的元数据信息,包括类的访问标志、父类、接口、字段、方法等信息。 -
静态变量
:存放类的静态变量,也就是在类加载时就被分配的变量。 -
即时编译器编译后的代码
:当某个方法被多次执行时,即时编译器会将该方法编译成本地机器码,并将其存放在方法区中,以提高执行效率。 -
动态生成的代理类和动态生成的字节码
:当使用Java反射、动态代理等技术时,会动态生成类的字节码,并将其存放在方法区中。
-
1.2 方法区内存结构图
Hotspot 虚拟机 jdk1.6 1.7 1.8 内存结构
1.4 永久代为什么要被元空间替换
- 永久代设置空间的大小是很难确定的,某些场景下如果动态加载类过多,容易产生OOM,而元空间使用的是本地内存,默认情况下元空间的大小仅受本地内存限制
- 对永久代进行调优比较困难
3. 方法区内存溢出
- 1.8 之前会导致永久代内存溢出
使用 -XX:MaxPermSize=8m 指定永久代内存大小 - 1.8 之后会导致元空间内存溢出
使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
3.1 常量池与运行时常量池
常量池
:
就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息运行时常量池
:
常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
3.2 为什么要有常量池
它的主要作用是为了提高程序的执行效率和节省内存空间。
常量池可以存储类名、方法名、参数类型等信息,这些信息在程序运行过程中会被频繁使用。如果每次使用这些信息都需要重新创建,那么会浪费大量的内存空间和时间。
3.2 StringTable 特性
- 常量池中的字符串仅是符号,第一次用到时才变成对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder(jdk1.8)
- 字符串常量拼接的原理是编译器优化
- 可以使用 intern() 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
3.3 StringTable位置
jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。
3.4 StringTable垃圾回收
- -Xmx10m 指定堆内存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次数,耗费时间等信息
3.5 StringTabel性能调优
-
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放热串池所需要的时间
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
-
考虑是否需要将字符串对象入池
可以通过 intern 方法减少重复入池
4. 方法区是否存在垃圾回收
方法区的垃圾收集主要回收两部分:常量池中废弃的常量和不再使用的类型,只要常量池中的常量没有被任何地方引用,就可以被回收
- 存放的常量主要是:
- 字面量:如被声明 final 的常量值等
- 符号引用:类和接口的全限定名、字段名称和描述符、方法名称和描述符
补录 5. 对象的实例化
1. 对象实例化的几种方式
2. 对象创建的6个步骤
简记:1. 加载类元信息 - 2.为对象分配内存 (3.处理并发问题)- 3.属性的默认初始化(成员变量) - 4.设置对象头信息 - 5.属性显示初始化(init方法,代码块中、构造器中初始化)
成员变量的赋值过程:默认初始化 - 显示初始化 - 构造器中初始化
3. 对象的内存布局
图示:
6 直接内存
6.1 定义:Direct Memory
- 常见于NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
6.2 直接内存原理
-
文件读写流程
因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。 -
使用了 DirectBuffer 文件读取流程
直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。
6.3 代码示例
public class Code_06_DirectMemoryTest {
public static int _1GB = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
// method();
method1();
}
// 演示 直接内存 是被 unsafe 创建与回收
private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe)field.get(Unsafe.class);
long base = unsafe.allocateMemory(_1GB);
unsafe.setMemory(base,_1GB, (byte)0);
System.in.read();
unsafe.freeMemory(base);
System.in.read();
}
// 演示 直接内存被 释放
private static void method() throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完毕");
System.in.read();
System.out.println("开始释放");
byteBuffer = null;
System.gc(); // 手动 gc
System.in.read();
}
}
6.3.1 直接内存回收原理
public class Code_06_DirectMemoryTest {
public static int _1GB = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
// method();
method1();
}
// 演示 直接内存 是被 unsafe 创建与回收
private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe)field.get(Unsafe.class);
// 分配内存
long base = unsafe.allocateMemory(_1GB);
unsafe.setMemory(base,_1GB, (byte)0);
System.in.read();
// 释放内存
unsafe.freeMemory(base);
System.in.read();
}
// 演示 直接内存被 释放
private static void method() throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完毕");
System.in.read();
System.out.println("开始释放");
byteBuffer = null;
System.gc(); // 手动 gc
System.in.read();
}
}
直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。
第一步:allocateDirect 的实现
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
底层是创建了一个 DirectByteBuffer 对象。
第二步:DirectByteBuffer 类
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size); // 申请内存
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); // 通过虚引用,来实现直接内存的释放,this为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。
att = null;
}
这里调用了一个 Cleaner 的 create 方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。
public void clean() {
if (remove(this)) {
try {
// 都用函数的 run 方法, 释放内存
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
}
}
可以看到关键的一行代码, this.thunk.run(),thunk 是 Runnable 对象。run 方法就是回调 Deallocator 中的 run 方法,
public void run() {
if (address == 0) {
// Paranoia
return;
}
// 释放内存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
6.3.2直接内存的回收机制总结
- 使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用freeMemory 方法
- ByteBuffer 的实现内部使用了 Cleaner(虚引用)来检测 ByteBuffer 。一旦ByteBuffer 被垃圾回收,那么会由 ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法调用 freeMemory 来释放内存
注意:
/**
* -XX:+DisableExplicitGC 显示的
*/
private static void method() throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完毕");
System.in.read();
System.out.println("开始释放");
byteBuffer = null;
System.gc(); // 手动 gc 失效
System.in.read();
}
一般用 jvm 调优时,会加上下面的参数:
-XX:+DisableExplicitGC // 禁止显示的 GC
意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。
7 垃圾回收
什么是垃圾
垃圾是指在运行程序中没有任何指针指向的对象
什么是GC,为什么需要GC
7.1 如何判断对象可以回收
7.1.1 引用计数法
当一个对象被引用时,就当引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。
这个引用计数法听起来不错,但是有一个弊端,如下图所示,循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。
7.1.2. 可达性分析算法
- JVM中的垃圾回收器通过可达性分析来探索所有存活的对象
- 扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收
- 可以作为 GC Root 的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 本地方法栈中 JNI(即一般说的Native方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁 synchronized 持有的对象
7.1.3 四种引用
- 强引用
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收 - 软引用
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
可以配合引用队列来释放软引用自身 - 弱引用
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
可以配合引用队列来释放弱引用自身 - 虚引用
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,
由 Reference Handler 线程调用虚引用相关方法释放直接内存 - 终结器引用
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象。
7.1.4 演示软引用
/**
* 演示 软引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Code_08_SoftReferenceTest {
public static int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
method2();
}
// 设置 -Xmx20m , 演示堆内存不足,
public static void method1() throws IOException {
ArrayList<byte[]> list = new ArrayList<>();
for(int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
}
// 演示 软引用
public static void method2() throws IOException {
ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
for(int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for(SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
- method1 方法解析:
首先会设置一个堆内存的大小为 20m,然后运行 mehtod1 方法,会抛异常,堆内存不足,因为 mehtod1 中的 list 都是强引用。 - method2 方法解析:
在 list 集合中存放了 软引用对象,当内存不足时,会触发 full gc,将软引用的对象回收。细节如图:
- 上面的代码中,当软引用引用的对象被回收了,但是软引用还存在,所以,一般软引用需要搭配一个引用队列一起使用。
修改 method2 如下:// 演示 软引用 搭配引用队列 public static void method3() throws IOException { ArrayList<SoftReference<byte[]>> list = new ArrayList<>(); // 引用队列 ReferenceQueue<byte[]> queue = new ReferenceQueue<>(); for(int i = 0; i < 5; i++) { // 关联了引用队列,当软引用所关联的 byte[] 被回收时,软引用自己会加入到 queue 中去 SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue); System.out.println(ref.get()); list.add(ref); System.out.println(list.size()); } // 从队列中获取无用的 软引用对象,并移除 Reference<? extends byte[]> poll = queue.poll(); while(poll != null) { list.remove(poll); poll = queue.poll(); } System.out.println("====================="); for(SoftReference<byte[]> ref : list) { System.out.println(ref.get()); } }
7.1.5 弱引用演示
public class Code_09_WeakReferenceTest {
public static void main(String[] args) {
// method1();
method2();
}
public static int _4MB = 4 * 1024 *1024;
// 演示 弱引用
public static void method1() {
List<WeakReference<byte[]>> list = new ArrayList<>();
for(int i = 0; i < 10; i++) {
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB]);
list.add(weakReference);
for(WeakReference<byte[]> wake : list) {
System.out.print(wake.get() + ",");
}
System.out.println();
}
}
// 演示 弱引用搭配 引用队列
public static void method2() {
List<WeakReference<byte[]>> list = new ArrayList<>();
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for(int i = 0; i < 9; i++) {
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB], queue);
list.add(weakReference);
for(WeakReference<byte[]> wake : list) {
System.out.print(wake.get() + ",");
}
System.out.println();
}
System.out.println("===========================================");
Reference<? extends byte[]> poll = queue.poll();
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}
for(WeakReference<byte[]> wake : list) {
System.out.print(wake.get() + ",");
}
}
}
7.2 垃圾回收算法
7.2.1 标记清除
定义:Mark Sweep
- 优点:速度较快
- 缺点:会产生内存碎片
7.2.2 标记整理 Mark Compact
- 速度慢
- 没有内存碎片
7.2.3 复制 Copy
- 不会有内存碎片
- 需要占用两倍内存空间
7.3 补录
7.3.1 补录 Minor GC、Major GC与Full GC
-
部分收集:不是完整收集整个Java堆的垃圾收集,其中又分为:
- 新生代收集 (Minor GC / Young GC):针对新生代(Eden、from、to)的垃圾收集
- 老年代收集 (Major GC / Old GC):针对老年代的垃圾收集
- 目前,只有 CMS GC会有单独收集老年代的行为
注意,很多时候 Major GC 会和Full GC 混迹使用,需要具体分辨是老年代回收还是整堆回收
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
目前,只有G1 GC会有这种行为
-
整堆收集(Full GC):
收集整个 Java 堆和方法区的垃圾收集
7.3.2 补录 垃圾回收触发机制
1. 年轻代GC(Minor GC)触发机制
- 当年轻代空间不足时,就会触发 Minor GC,这里的年轻代满指的是 Eden区满,survivor幸存区满不会引发GC。(每次 Minor GC会清理年轻代的内存)
- 因为 Java对象
大多具备朝生夕灭
的特性,所以 Minor GC 非常频繁,一般回收速度也比较快 - Minor GC 会引发 STW,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
2. 老年代垃GC(Major GC/ Full GC)触发机制:
- 出现了一次Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge收集器的收集策略里就有直接进行 Major GC的策略选择过程)
- Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长
- 如果Major GC后,内存还不足,就报 OOM 了
7.3.3 补 为什么有TLAB(Thread Local Allocation Buffer本地线程分配缓冲区)
- 堆区是线程共享的区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
7.3.4补 堆是分配对象存储的唯一选择吗
不是,如果经过逃逸分析后发现一个对象没有逃逸出方法的话,那么可能被优化成栈上分配
- 逃逸分析就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸,然后使用栈上分配
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中
7.3 分代垃圾回收
7.3.0 分代垃圾回收流程
- 新创建的对象首先分配在 Eden(伊甸园)区
- 新生代空间不足时,触发 minor gc,Eden 区 和 from 区存活的对象使用 -copy 复制到 to 中,存活的对象年龄加一,然后交换 from to
- minor gc 会引发 stop the world,暂停其它线程,等垃圾回收结束后,回复用户线程运行
- 当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(4bit)
- 当老年代空间不足时,会先触发 minor gc,如果空间仍然不足,那么就触发 full gc,停止的时间更长!
7.3.1 相关 jvm 参数:
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
新生代与老年代比例 | -XX:NewRatio=2 (老年代2份,新生代1份) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
通过下的代码,给 list 分配内存,来观察 新生代和老年代的情况,什么时候触发 minor gc,什么时候触发 full gc 等情况,使用前需要设置 jvm 参数。
7.3.2 GC分析
public class Code_10_GCTest {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
// -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_6MB]);
list.add(new byte[_512KB]);
list.add(new byte[_6MB]);
list.add(new byte[_512KB]);
list.add(new byte[_6MB]);
}
}
7.4 垃圾回收器
- 串行
- 单线程
- 堆内存较小,适合个人电脑
- 吞吐量优先
- 多线程
- 堆内存较大,多核 CPU
- 让单位时间内,STW 的时间最短
- 响应时间优先
- 多线程
- 堆内存较大,多核 CPU
- 尽可能让单次 STW 的时间最短
相关概念:
- 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
- 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个 CPU 上
- 吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。
7.4.1 串行
特点
- 单线程
- 堆内存较少,适合个人电脑
-XX:+UseSerialGC=serial + serialOld
安全点
让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象
因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态
Serial 收集器
Serial 收集器是最基本的、发展历史最悠久的收集器
特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!
ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本
特点:多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本
特点:同样是单线程收集器,采用标记-整理算法
7.4.2 吞吐量优先
- 多线程
- 堆内存较大,多核 cpu
- 让单位时间内,STW(stop the world) 的时间最短 0.2 0.2 = 0.4
-XX:+UseParallelGC ~ -XX:+UsePrallerOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio // 1/(1+radio)
-XX:MaxGCPauseMillis=ms // 200ms
-XX:ParallelGCThreads=n
Parallel Scavenge 收集器
与吞吐量关系密切,故也称为吞吐量优先收集器
**特点:**属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与 ParNew 收集器最重要的一个区别)
GC自适应调节策略:
Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。
当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、
晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。
Parallel Scavenge 收集器使用两个参数控制吞吐量:
- XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms)
- XX:GCTimeRatio=rario 直接设置吞吐量的大小
Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本
特点:多线程,采用标记-整理算法(老年代没有幸存区)
7.4.3 响应时间优先
- 多线程
- 堆内存较大,多核 cpu
- 尽可能让 STW 的单次时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
CMS 收集器
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务
CMS 收集器的运行过程分为下列4步:
初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
并发清除:对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!
CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。
7.4.4 G1收集器
定义: Garbage First
适用场景:
- 同时注重吞吐量和低延迟(响应时间)
- 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域
- 整体上是标记-整理算法,两个区域之间是复制算法
相关参数:
JDK8 并不是默认开启的,所需要参数开启
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
G1 垃圾回收阶段
Young Collection:对新生代垃圾收集
Young Collection + Concurrent Mark:如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。
Mixed Collection:会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。
Full GC
- SerialGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- ParallelGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- CMS
- 新生代代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足
- G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足
G1 在老年代内存不足时(老年代所占内存超过阈值)
如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理
如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 serial Old 收集器串行的收集,就会导致停顿的时候长。