垃圾收集器与内存分配策略
一、概述
垃圾收集(Garbage Collection,简称GC),这项技术并不是Java语言的伴生产物,早在java语言出来之前,就已经有语言开始使用动态分配内存和垃圾回收了,例如Lisp的语言就是第一门使用内存动态分配和垃圾收集技术的语言
垃圾收集主要考虑考虑以下三个问题
- 哪些内存需要回收
- 何时回收
- 怎么回收
对于java虚拟机中的五大区域来说,程序计数器、虚拟机栈、本地方法栈三个区域是线程私有的,随线程而生随线程而灭。而且每一个栈帧基本上在类的结构被确定下来后其大小也确定下来,所以这三个区域的分配和回收都具有确定性,不需要过多的考虑
而方法区和堆内存分配是不确定的,所以垃圾收集器主要关注的正是这部分·
二、对象已死?
即讨论哪些内存需要回收
2.1、引用计数算法
HotSpot并没有采用这种算法
原理:在对象中添加一个引用计数器,当对象被引用时,计数器+1,当引用失效时,计数器-1.任何时刻计数器为0的对象就是不能被使用的对象
优点:判定效率高
缺点:简单的引用技术法存在缺陷,譬如对象之间循环引用的问题
循环引用:
package com.perfume;
/**
* @author 沈洋 邮箱:[email protected]
* @create 26-05-2021-13:10
**/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024*1024;
/**
* 这个成员属性的唯一意义就是占一点内存,以便能在Gc日志中清除的看到是否被回收。
*/
private byte[] bigSize = new byte[2*_1MB];
public static void testGC(){
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance=objB;
objB.instance=objA;
objA=null;
objB=null;
//此时发生GC,A,B对象是否会被回收
System.gc();
}
}
对于这种情况,如果使用引用计数器算法,A,B对象将不会被回收。因为A,B对象分别被对方引用,所以两个对象的引用计数为2.
当objA=null和objB=null执行时,两个对象的引用技数变为1。但实际上这个时候程序中已经没有办法使用AB两个对象了。
2.2、可达性分析算法
原理:通过一系列称为GC Roots的根对象作为起始节点集,从这些结点开始,根据引用关系向下搜索。搜索过程所走的路径称为**”引用链“**如果某个对象到GC Roots之间没有任何引用链相连,即图论中结点不可达。证没对象不可被使用。
GC ROOTS
在Java技术体系中,固定可作为GC Roots的对象包括以下几种
- 在虚拟机栈(栈中的本地变量表)中引用的对象,譬如当前正在运行的方法所使用到的参数、局部变量、临时变量
- 方法区中类静态属性引用的对象,譬如java类的引用类型静态变量,即类中static修饰的引用变量
- 方法区中常量引用的对象,譬如字符串常量里的引用。
- 本地方法栈中JNI(Native方法)引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象、常驻的异常类型对象(比如NullPointeException等)、系统类加载器
- 所有被同步锁持有的对象(synchronized关键字)持有的对象
- 反应Java虚拟机内部情况的JMXBean、jvmti中注册的回调、本地代码缓存
除了这些固定的GC Roots集合以外。根据垃圾收集器的不同,回收区域不同有些对象还可以临时加入集合中。
比如分代收集和局部回收都只是对堆中一个区域进行垃圾收集。因为堆中的区域不是孤立封闭的,这个时候就需要考虑当前回收的区域是否被堆中其他区域引用。这个时候就要考虑将关联区域中的对象加入GC Roots集合中,进行可达性的分析。
2.3、引用分类
无论是通过可达性分析算法判断对象是否引用链可达,还是引用计数器判断对象的引用数量。对象在这种定义下只有两种状态,”被引用“、”未被引用“。
但当我们需要描述一类对象:在内存足够时,保留该对象;内存不足时可以抛弃该对象。例如缓存功能
在JDK1.2后,对引用的概念进行了补充。分为四种强度不一的引用:强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,四种引用强度依次递减
- **强引用:**传统的引用定义,即Object object = new Object(); 这种引用关系,无论任何情况,只要引用关系还存在就不会被回收
- 软引用:用来描述一些还有用,但非必须的对象。只被软引用关联的对象在系统发生内存溢出时,垃圾收集器会将这部分对象列入二次收集范围内,并进行二次收集。如果二次收集后内存仍不足,才抛出内存溢出异常。JDK1.2提供了SoftReference对象来实现软引用
- 弱引用:也是用来描述非必须对象,但这种引用比软引用对象强度更低。只被软引用的对象仅能生存到下一次垃圾收集发生。垃圾回收发生后,不论内存是否足够,这种对象都会被回收。JDK1.2后提供了WeakReference类来实现弱引用。
- 虚引用:虚引用完全不影响对象生存时间,甚至不能通过虚引用获取实例。唯一的作用就是虚引用的对象在被收集时,收到一个系统通知。JDK1.2之后提供了Phantomreference类来实现虚引用
2.4、生存还是死亡
被可达性分析算法判断为不可达的对象也并不是一定会被回收。对象可以在finalize方法中与GCRoots集合中的结点建立引用链,从而逃脱回收。但这种方式并不推荐使用,而且finalize方法只有一次执行机会,也就是说一个对象最多只有一次逃逸机会。
下面是垃圾收集器在进行可达性分析后,一个对象被标注为不可达后的过程
-
在可达性分析中,判断为不可达的对象会被标记第一次,随后对这些对象进行筛选。
-
如果对象没有覆盖finalize()方法或者此前以及在JVM中执行过该方法。则判断为没有必要执行finalize方法。这种对象将会被执行回收。
-
如果对象被判定为需要执行finalize方法,则会将该对象放入F-queue队列中(稍后由虚拟机自动创建的一个低调度优先级的线程Finalizer执行它们的finalize方法)
-
执行完finalize方法的对象如果在执行方法后于GCRoots中的结点建立了连接。将会被移除即将回收的集合,从而逃脱回收。
package com.perfume;
/**
* 此代码演示两点
* 1、对象可以在被GC时自我拯救
* 2. 这种拯救机会只有一次,因为一个对象的finalize方法最多被系统调用一次
* @author 沈洋 邮箱:[email protected]
* @create 26-05-2021-15:24
**/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK =null;
public void isAlive(){
System.out.println("yes i am still alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed");
//方法区中的静态变量引用了这个对象,从而使对象在GC ROOTS中可达。
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
//断开连接,使对象被GC
SAVE_HOOK = null;
//提醒执行GC
System.gc();
//因为Finalizer方法优先级很低,暂停0.5秒,以等待它执行
Thread.sleep(500);
if(SAVE_HOOK!=null){
SAVE_HOOK.isAlive();
}else System.out.println("no,i am dead");
//下面代码与上面完全一致,但这次却不能逃脱GC,因为一个对象的finalize方法只执行一次
SAVE_HOOK = null;
//提醒执行GC
System.gc();
//因为Finalizer方法优先级很低,暂停0.5秒,以等待它执行
Thread.sleep(500);
if(SAVE_HOOK!=null){
SAVE_HOOK.isAlive();
}else System.out.println("no,i am dead");
}
}
**总结:**这种方式实际上没有任何必要,不建议使用
2.5、回收方法区
方法区也存在内存回收,但这个区域一般回收性价比不高。并且《Java虚拟机规范》中没有要求必须实现方法区的内存回收。
-
废弃常量
回收常量和回收堆中的对象相似,就是判断系统中是否还有地方引用这个常量。(需要垃圾收集器判断是否有必要收集,并不是一个常量没有被引用就会被回收)
-
不再使用的类型:
判断一个类型是否被回收的条件比较苛刻,主要满足以下三点。
不过这种收集在大量使用反射、动态代理、cglib等字节码框架中使用。避免方法区造成过大的内存压力。
- 该类的所有实例都已经被回收。
- 加载该类的类加载器已经被回收(这个条件一般很难实现)
- 该类对应的Class对象没有在任何地方被引用,即无法通过反射访问该类。
三、垃圾收集算法
垃圾收集算法主要有两大类:引用技术式垃圾收集(Reference Counting GC)、追踪式垃圾收集(Tracing GC)
主流JVM未采用引用技术式垃圾收集算法,所以不做过多讨论
3.1、分代收集理论
分代收集理论是建立在两个假说上面的
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过多次垃圾收集的对象就越难消灭
这奠定了垃圾收集器的设计原则:将Java堆分区域,将回收对象根据年龄分配到不同区域
基于分代收集理论,一般虚拟机都将堆分为**新生代(Young Generation)和老年代(Old Generation)**两个区域。
分代回收类型
跨代引用问题
分代收集时并不是简单的将两个区域分开,对象不是孤立的,对象之间存在跨代引用的问题。
比如在收集新生代区域的对象时就需要考虑,哪些对象是被老年代引用了的对象。
所有涉及到部分收集的收集都需要考虑跨代引用的问题,并不仅限于新生代区域的收集
解决办法:
-
这个时候最简单的解决办法是,在收集新生代对象时,再额外遍历老年代的对象以保证可达性分析的正确性。但这样做无疑为内存带来很大的性能负担。
-
**跨代引用假说(Intergenerational Reference Hypothesis):**跨代引用相对于同代引用来说占少数
**原因:**因为老年代中的对象一般难以消灭,一个新生代的对象被引用后,随着时间推移新生代的对象也将变成老年代对象,从而消除跨代引用。
所以:我们只需要为新生代区域建立一个全局的数据结构,用来记录老年代中哪些对象存在跨代引用(记忆集,详见四节4.4)。这样在我们对新生代进行回收时,只需要将这一小块区域的对象加入GC Roots中,而 不需要扫描整个老年代。
注意:需要注意的是,如果老年代中某个对象改变了引用关系,这张表也需要对数据进行修改。
3.2、标记-清除算法
最基础的回收算法,后续算法都是这种算法的改良。
实现过程:根据判定算法判定哪些对象需要回收(或者判定哪些对象需要保留),统一回收所有被标记的对象(或者未被标记的对象)
在进行标记和清除时需要停顿用户线程,只是停顿时间相较后面的标记-整理算法要短。
缺点:
- 执行效率不稳定,如果堆中大量对象需要回收时,需要执行大量的标记清除动作,执行效率随之降低。
- 内存空间碎片化,标记清除后产生的空闲空间大部分是不连续的,下一次需要分配较大对象空间时,可能会找不到连续空间来分配。无法分配时就会触发Full GC
3.3、标记-复制算法
大部分商用虚拟机都是采用的这种算法(升级版)
普通版
实现过程:将内存按容量一分为二,每一次只使用一块区域。当发生垃圾收后,将存活对象全部复制到另一半区域上继续使用。
缺点:
- 如果大量对象都是存活的话,就需要执行大量对象的复制,产生内存复制的额外开销
- 可用内存只有原来的一半
升级版(Apple式回收)
实现过程:将内存分为三部分:一块较大的Eden空间,两块较小的Survivor空间(HotSpot中默认为8:1:1)。每次分配内存只使用1个Eden+1个Survivor空间,每次回收后将Eden和正在使用的Survivor空间清空,将存活对象复制到空闲的Survivor空间上。当存活对象超过Survivor空间容量时,就会将对象分配到老年代中。
缺点
- 对象存活率高时,会进行较多的复制操作,效率降低。
3.4、标记-整理算法
主要使用在老年代中
HotSpot中的Parallel Old收集器主要使用此种收集算法
与标记-清除法相似,区别是标记后不会立刻进行清除,而是将存活对象进行移动整理到一端。
缺点:在老年代中每次回收都会有大量对象需要移动,造成负担。而且与标记-清除算法一样,对象的移动操作都需要全程暂停用户线程(ZGC和Shenandoah收集器使用读屏障来实现清除时暂停用户线程)
对比标记-整理和标记清除算法,两者各有优势。两者的区别主要就是是否移动对象
- 移动对象
- 优点:有连续完整的空间分配给大的对象
- 缺点:移动对象时停顿时间长
- 不移动对象
- 优点:减少了移动对象的停顿
- 缺点:空间碎片化,遇到分配大对象时可能没有连续完整的空间分配
CMS垃圾收集器采用的是两种方式的结合,一般情况使用标记-清除算法.当内存碎片化影响到内存分配时,使用标记-整理算法收集一次.
四、HotSpot的算法实现细节
4.1、根节点枚举
在进行可达性分析时需要在一瞬间暂停用户线程并生成当前时刻的快照然后进行可达性分析
(枚举所有根节点)。
实际上即使生成了快照,如果每一次都需要虚拟机从所有信息中去检查引用,也会造成很大的内存负担。
解决办法:使用一组称为OopMap的数据结构来记录来保存这些GC Roots的引用信息。
4.2、安全点
在任何时刻生成的快照OopMap中的引用都有可能不相同,如果为每一条指令都生成对应的OopMap,将会耗费大量的额外存储空间。
解决办法
在代码中设置一些点为安全点,只有执行到安全点时,才会生成对应的OopMap。这也决定了代码不能再任意位置停下来进行垃圾回收,只能在安全点位置才能进行垃圾回收。
安全点的选取:一般选择那种指令复用的位置作为安全点例如:方法调用、循环跳转、异常跳转等。
如何在发生垃圾收集的时候让所有线程都跑到最近的安全点上?
- **抢先式中断:**系统发生垃圾收集时,先将所有线程都停下,再检查线程是否在安全点上,如果不在就启动线程继续执行到最近的安全点上。
- 主动式中断
4.3、安全区域
问题
如果在停顿用户线程生成OopMap时,有线程没有获得cpu,程序长时间得不到执行而无法进入安全区应该如何解决?例如线程Sleep或者Blocked状态
解决办法
这种情况下就需要引入安全区域的概念
安全区可以看做拉伸的安全点,是指在某一段代码片段中,引用关系不会发生变化,在这片区域的任何地方都可以开始垃圾收集。
-
线程执行到安全区域,标识自己进入安全区域,
这样当线程在安全区域的时候,虚拟机发起了垃圾收集时就不需要去管这个线程。
-
线程离开安全区域
检查虚拟机是否执行完了根节点的枚举 ,如果执行完了就离开安全区域。否则一直等待,直到收到信号。
4.4、记忆集与卡表
记忆集的主要作用就是缩减GC ROOTS的扫描范围
在分代收集理论中提到了,使用记忆集的数据结构来解决跨代引用的问题。但其实不只是新生代在收集时需要考虑跨代引用的问题,所有部分区域收集都需要考虑。
卡表其实就是记忆集的一种实现方式
记忆集的实现原理和实现方式
实现原理
因为我们不需要在记忆集中保留对象全部的数据,我们只需要通过记忆集能判断出某一块区域中是否存在跨代引用即可。所以一般采取粗粒度的记录方式。以下三种(卡精度是最常用的)
卡精度主要是用一种卡表的方式去实现,一个卡表可以用一个字节数组来实现。
数组中的一个位置就表示一片内存区域,称为卡页。HotSpot中默认卡页大小为512字节,即一个卡表中,每个卡页之间内存地址相差512个字节。
实现方式
4.5、写屏障
前面讨论了如何 使用记忆集来减少GC ROOTS的扫描时间,这一节将如何维护记忆集(即决定那些数据何时变脏)
因为Java中即时编译的存在,一些赋值指令经过即时编译之后会直接变成机器指令来执行。所以需要在机器指令的层面来切入发生引用修改的语句。
可以看作是在虚拟机层面对“引用类型字段修改”的AOP切面,大多数收集器采用写后屏障
面临的问题
卡表在高并发的情况下会出现“伪共享”问题
在将卡表变脏之前判断卡页是否 已经变脏了,不过这样就又会增加一次判断的开销。
HotSpot虚拟机需要手动开启对卡表更新的判断。两者各有性能损耗
五、经典垃圾收集器
大部分新生代收集器都是基于 标记-复制算法实现的
大部分老年代收集器都是基于 标记-整理算法实现的
5.1、Serial收集器
5.2、ParNew收集器
- 新生代收集器
- 收集时暂停所有线程
- 使用**标记-复制算法**进行可达性分析
- 多线程版本的Serial收集器,但实际上如果是单核的处理机环境上Serial收集器的效率更高,因为ParNew收集器还需要面临线程切换的开销
- JDK1.7之前和CMS收集器(老年代)组合作为服务端模式下的解决方案。但随着G1的出现(面向全堆的垃圾收集器),ParNew和CMS将不再是服务端模式下的推荐解决方案。
- 现在ParNew只能和CMS搭配使用
5.3、Parall Scavenge 收集器
-
新生代收集器
-
使用标记-复制算法进行回收
-
收集时暂停所有线程
-
多线程收集
-
提供了两个参数用于精确控制吞吐量
5.4、Serial Old收集器
- Serial收集器的老年代版
- 单线程收集器
- 收集时暂停所有线程
- 使用标记-整理算法进行回收
- 主要用途
- 客户端模式下的HotSpot虚拟机老年代收集器
- JDK5及之前与Parallel Scavenge收集器搭配使用
- 作为CMS收集器发生失败后的后备预案
5.5、Parall Old收集器
5.6、CMS收集器
- 目标是缩短系统停顿时间
- 用于老年代
- 使用标记-清除算法
工作步骤:
存在的缺点
-
无法处理浮动垃圾
-
由于CMS收集器采用的是标记-清除算法,会带来内存空间碎片化问题从而导致Full GC的提前。
5.7、G1收集器(Garbage First)
- 使用于服务端的垃圾收集器
- 整体使用 标记-整理算法,局部为标记-复制算法
- 全功能的收集器,替代了之前服务器端最常见的组合(ParNew+CMS收集器)
- 建立可停顿预测模型,在N毫秒的时间内使得垃圾回收时间不超过M毫秒
- 用户可指定期望的停顿时间,期望时间可以通过参数 -XX:MaxGCPauseMillis设置,默认值为200毫秒,推荐值在100-300毫秒之间,设置过低可能会导致回收空间不足提前触发Full GC。
- 从G1开始,收集器每次收集不追求将空间中的垃圾全部清除,而是追求能匹配上分配器速度保持应用正常运行。
- 与其他收集器不同,G1的收集对象不再是独立的新生代或者老年代。它可以面向堆中任何部分发起收集。其发起收集的衡量标准为哪个垃圾的数量和回收收益
- Region内存布局是G1可以回收任何部分内存空间的关键
G1垃圾收集器面临的问题
-
Region之间的跨代引用
- 同样使用记忆集(哈希表-卡表)来实现
- 与普通卡表不同,G1中的卡表是双向记录的
- 缺点:内存占比增加,相当于堆内存的10%~20%
-
- 采用原始快照算法实现
-
如何建立可靠的可停顿预测模型
- 为每个Region记录脏卡表数量等数据,计算衰减平均值–衰减平均值与平均值相比更反应最近的平均状态
- 根据衰减平均值来判断期望时间内的收益
G1收集器运行流程(与CMS相似)
-
初始标记
-
并发标记
从GC Root开始并发分析可达性,标记需要回收的对象
最后通过原始快照方式处理并发并发标记期间对象变更
-
最终标记
-
筛选回收
与CMS的并发清除不同,筛选回收阶段也需要暂停用户线程,G1设计初衷也不是去追求低延迟
统计各个Region的回收价值并排序,根据用户期望时间指定选择任意多个Region组成回收集进行回收
回收过程:暂停用户线程,多个回收线程并行执行,将Region中存活对象复制到空的Region中,并清理整个旧Region
缺点
- 记忆集(卡表)维护占内存,每一个Region都需要维护一张复杂的卡表
- 内存分配不足时,会促发Full GC
六、低延迟垃圾收集器
与CMS、G1不同,低延迟处理器在整个回收过程中只有初始标记+最终标记阶段需要短暂暂停用户线程以外,其余阶段都是并发执行
追求更短的用户程序停顿时间,在低延迟下尽可能的不影响吞吐量
两种收集器,都可以将停顿时间限制在十毫秒以内
6.1、Shenandoah收集器
- 这款收集器并不是Oracle主导研发的,在OracleJDK12中仍拒绝加入Shenandoah,这款收集器暂时只能在OpenJDK中使用
- 与G1一样,也是将内存区域分为多个Region(包括存储大对象的Humongous Region)
- 与G1不同的是
Shenandoah工作流程
三个重要阶段,并发标记-并发回收-并发引用更新
-
初始标记
-
并发标记
-
最终标记
与G1一样,需要暂停用户线程(时间极短),处理剩余的原始快照扫描,计算出Region回收价值并组成回收集
-
并发清理
与用户线程并发执行,只清理Region中无任何存活对象的Region。
-
并发回收
这个阶段是Shenandoah与G1区别最大的地方,在回收阶段Shenandoah需要将Region中存活对象复制到新的Region中,但如何在与用户线程并发情况下移动对象也是最大的问题。
-
初始引用更新
有短暂的用户线程停顿,该阶段无具体操作,仅做回收线程的集合点,主要是确保所有回收线程都已经完成Region存活对象的复制。
-
并发引用更新
-
最终引用更新
更新GC Roots中的引用,短暂停顿用户线程
-
并发清理
经过并发回收—引用更新后,回收集中的Region已经没有存活对象了,再一次调用并发清理即可完成回收。
并发回收实现细节
-
在原有对象布局上增加一个转发指针,正常情况下转发指针指向自己,其作用类似句柄
Shenandoah中同时使用了读屏障、写屏障而这两种操作都是极为麻烦的,尤其是读屏障其使用率极高,也就带来了性能影响。(优化:JDK13后Shenandoah将读屏障改为基于引用访问的屏障,这种屏障只拦截引用数据类型的访问,不会影响原生类型数据的访问)
-
对于并发读操作来说没有问题,但如果是并发写操作需要确保写操作能正常落到复制后对象上。Shenandoah使用CAS来实现
缺点
- 高运行负担下吞吐量下降
优点
- 低延迟
6.2、ZGC收集器
一款处于实验状态的收集器,还没有商业化。
-
同样也是基于Region布局的(没有设置分代,从优先顺序考虑,并不是分代不好)
-
使用了读屏障、染色指针、内存多重映射来实现并发标记-整理算法
-
与G1的Region不同,ZGC中的Region是动态创建和销毁的,其Region大小也是不同的
-
小型Region:容量为2MB,每个对象大小小于256KB
-
中型Region:容量为32MB,每个对象大小在256KB~4MB之间
-
大型Region:容量为2MB*N,且其中只存放一个对象。
大型Region总体容量最小可以只有4MB,所以大型Region不一定大于中型Region
-
-
因为ZGC中没有设置分代,优点是运行负担小,缺点是分配速率会降低
如何实现并发整理算法
在Shenandoah中主要使用了转发指针+读屏障
而ZGC主要采用了一种名为 染色指针的技术
染色指针
ZGC将对象的一些标记状态直接放到对象的引用指针上,例如在64位的电脑中,取高四位用来存储四个标志信息
染色指针缺点:
-
染色指针不支持32位平台
-
由于直接使用了指针中的四位,这也导致了染色指针内存收到了限制。
染色指针优点:
- 当Region中的存活对象被移走后,可以直接开始回收该Region,而不用等到将所有引用更新后才开始,其原因主要是染色指针具有自愈的特性
- 染色指针可以大幅减少内存屏障的使用,提升效率
染色指针需要解决的问题
由于所有的程序最终都会转变为机器码由cpu执行,但是cpu并不能识别
需要解决虚拟机到操作系统之间重定义内存地址
解决:使用多重映射
工作流程
- 并发标记
- 并发预备重分配
- 根据条件统计出本次收集需要清理哪些Region,并组成重分配集。
- ZGC中没有记忆集,而是通过每次全局扫描Region来代替维护记忆集的开销
- 并发重分配(核心阶段)
- 并发重映射
七、选择合适的收集器
- 如果是数据分析、科学计算类任务,吞吐量为主要关注的
- 如果是SLA应用,停顿时间是主要关注点
- 客户端应用、嵌入式应用,内存占用是主要关注点
总结
Serial收集器+Serial Old收集器用于客户端模式
ParNew收集器+CMS收集器用于服务端模式
Parall Scavenge收集器+Parall Old收集器用于服务端模式
现在推荐使用的收集器 G1收集器
未来趋势 Shenandoah收集器、ZGC收集器
垃圾收集器相关参数
参数 | 描述 |
---|---|
UserSerialGC | 虚拟机运行在Client模式下的默认值,开启后使用Serial+Serial Old的收集器组合进行内存回收 |
UseConcmarkSweepGC | 使用ParNew+CMS+Serial Old组合进行回收,Serial Old作为CMS收集器回收失败的后备收集器 |
UseParallelGC | JDK9之前虚拟机在Server模式下的默认值,使用Parallel收集器组合回收 |
SurvivorRatio | 设置新生代中Eden区与Survivor区比值,默认8:1 |
PretenureSizePolicy | 设置多大的对象直接在老年代中分配 |
MaxTenuringThreshold | 新生代中的对象年龄超过这个值时进入老年代(默认值为15,新生代中的对象每过一次GC年龄+1) |
HandlePromotionFailure | 是否允许分配担保失败(新生代回收时,存活对象超过老年代的容量,即担保失败) |
ParallelGCThreads | 并行GC线程数 |
实战:内存分配与回收策略
对象的内存分配主要是在堆中(主要在新生代中,少部分大对象直接在老年代中分配),还有一部分对象经过即时编译后作为标量直接在栈上分配。
-
对象优先在Eden分配
大多数情况下对象都在新生代的Eden区分配,当Eden区空间不足时触发一次MinorGC。
-
大对象直接进入老年代
-
长期存活的对象将进入老年代(放在对象头里面的对象年龄计数器),每经历一次Minor GC计数器+1,当年龄达到阈值时进入老年代(默认为15)
-
动态对象年龄判断:当SUrvivor空间中低于某年龄的对象超过一半,则将年龄大于该值的对象转到老年代中
-
活对象放到另外的Survivor区中,如果Survivor区容量不足以装存活对象则会将对象放到老年代中
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。