0 引入
基本概念
- 垃圾是指在程序运行中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
- 垃圾回收(GC)是Java中自动化的内存管理机制,旨在回收不再使用的对象,以释放内存。
- 堆(Heap)是垃圾回收的主要区域,存放着运行时分配的对象。堆通常被划分为多个区域:年轻代(Young Generation)、老年代(Old Generation)和持久代(Permanent Generation,已在 Java 8 后被元空间(Metaspace)替代)。
为什么需要GC?
- 简化内存管理 :在没有垃圾回收的语言中(如c或c++),程序员必须手动分配和释放内存,这样内存管理容易造成内存泄漏或使用已经释放的内存,从而导致程序崩溃或者安全漏洞。
垃圾回收
的过程是,JVM自动的去找到正在使用的对象,然后堆这些使用的对象进行标记和追溯,然后把剩下的对象判定为垃圾,进行清理,从而大大简化了内存管理的复杂性,减少了出错的可能性。- 提高开发效率:程序员可以专注于业务逻辑的实现而不是内存管理的细节。
- 优化内存使用:垃圾回收可以对内存进行优化,除了释放没有的对象,垃圾回收也可以清除内存里的记忆碎片,从而提高内存的利用率。
1 如何判断对象可以回收
1.1 引用计数法
给对象添加一个引用计数器
,当一个对象被引用时,该对象的引用计数器加1
,引用失效时计数器减1
,如果计数器为0
就标识该对象不被引用,可以被垃圾收集器回收,但是这个方法存在一个弊端,就是循环引用
时,计数器不能清零,导致两个对象都不能被释放,在java中,我们使用的并不是这个方法。
1.2 可达性分析算法
与我们设想不同的是,垃圾回收 ,并不是找到不再使用的对象,然后将这些对象清除掉,它的过程正好相反,JVM会找到正在使用的对象,对这些使用的对象进行追溯和标记,然后一股脑的把剩下的对象判定为垃圾,进行清理。
由此,我们可以得出一些衍生分析:
- GC的速度,和堆内
存活对象
的多少有关,与堆内所有对象的数量无关 - GC的速度
与堆的大小无关
,无论堆的大小是32G还是64G,只要存活的对象是一样的,垃圾回收的速度就会差不多 - 垃圾回收最重要的是不要把正在使用的对象判定为垃圾
📢
所以,我们可以看出,
如何找到这些存活对象
,也就是正在被使用的对象,是垃圾回收问题的核心,而我们找到存活对象的方法,就叫可达性算法
1.2.1 算法的基本思路
从一系列称为GC Roots
的根节点开始,根据引用关系
向下搜索,搜索过程中所走过的路径称为引用链
,如果某个对象到GC Roots之间没有任何引用链相连,即GC Roots到这个对象不可达,则说明这个对象是不会再被使用的对象。
1.2.2 GC Roots是什么?
❓ 我们要通过GC Roots去找存活对象,那么GC Roots一定是当前一定存活的对象,那这些当前一定存活的对象有哪些呢?
- 所有
活动线程
中,正在被调用的方法的局部变量表
和操作数栈
中引用的对象
,也就是与我们的活动栈帧相关的各种引用 静态变量
中引用的对象- JNI(Java Native Interface)引用的对象,即
本地方法栈中native方法
引用的对象 虚拟机内部的引用
,如常量池(Constrant Pool)中引用的对象,基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError),系统类加载器等
除了这些固定的GC Roots外,GC Roots话可以扩充。例如当针对部分区域进行收集时,其他区域对该区域的引用,也需要加入GC Roots集合中进行判断。
1.2.3 记忆集Remembered Sets简介
当对堆进行部分内存区域回收的时候,就会存在跨区域引用
的问题,在GC Root这里讲过,如果存在跨区域的引用关系,那么这种引用即便不是"固定"GC Roots范畴,那也应该作为GC Roots集合的补充,一起来进行可达性分析判断。(例如所有堆内存被划分为(A,B,C,D,E)五个区,当我们这次只对A,B进行回收时,就需要判断C,D,E中是否有引用A,B中的对象)
💡 可以理解为:某个对象可能在该次回收的区域中不存在引用关系,也本不是GC Roots但是它被另一个区域的对象引用了,所以也将这个对象加入GC Root中
为了能够找出这种跨区的引用关系 ,一种直接的方式就是,将"回收区"以外的所有内存区域扫描一遍,看看哪些是有引用回收区里面的对象的。很显然,这种全域扫描的方式性能会极差,是不可接受的。所以就有了记忆集
,记忆集列出了从外部指向本块的所有引用。这种引用记录会在引用关系创建、更改时进行维护。当需要进行这种外部引用关系分析时,直接读取记忆集内容就行。
❓ 那么问题来了,通过GC Roots能追溯到的对象,一定不会被垃圾回收吗?
1.3 四种引用
Java中,引用类型的强弱会决定对象是否能被垃圾回收,主要分为四种,强度递减分别是:强-->软-->弱-->虚
强引用
:最常见的引用类型,比如Object obj = new Object()这种new产生的引用就是强引用,一个对象如果还有强引用,那么垃圾回收器绝不会回收它。(用途:对象的一般状态)
java
Object object = new Obeject();
String str = "StrongReference";
软引用
:软引用来表示对象是有用的,但不是必须的。如果一个对象只有软引用了,那么当内存不足时,系统会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。(用途:对象缓存)
java
SoftReference<Object> softRef = new SoftReference<>(new Object());
弱引用
:弱引用就更低一级,用来描述一些非必须的对象。当一个对象只有弱引用的时候,只要发生垃圾回收gc,就会被回收,所以说弱引用对象活不过下一次gc(用途:对象缓存)
java
public static void main(String[] args) {
WeakReference<String> weakReference = new WeakReference<>(new String("hello"));
System.out.println(weakReference.get());
System.gc();//进行垃圾回收
System.out.println(weakReference.get());
}
结果:
java
hello
null
虚引用
:最弱的一种引用,形同虚设。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象实例。为一个对象设置虚引用的唯一目的只是为了这个对象被垃圾收集器回收时受到一个系统通知。虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。(用途:未知)
java
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> phantomReference = new PhantomReference<>(new String("hello"),queue);
System.out.println(phantomReference.get());
System.gc();
Thread.sleep(1000);//等待gc完成
System.out.println(queue.poll());
}
输出:
java
null
java.lang.ref.PhantomReference@1540e19d
2 如何回收垃圾对象------垃圾收集算法
2.1 标记-清除算法
- 分为
标记
和清除
阶段,首先,从每个GC Roots出发,标记有引用关系的对象,最后清除没有标记的对象 - 但是存在
执行效率
问题,清除阶段会造成停顿,如果堆包含大量对象且大部分需要回收,必须进行大量标记清除,导致效率随对象数量增长而降低 - 存在
内存碎片化
问题,会产生大量不连续的内存碎片,导致以后需要分配大对象时容易出发Full GC
(对整个堆内存进行回收)
2.2 标记-复制算法
为了解决内存碎片问题
,将内存划分成大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理,并把两块空间的名称互换(主要用于新生代)标记-复制算法
实现简单,运行效率高,解决了内存碎片问题,代价
是一次只能用到一半内存,浪费空间
2.3 标记-整理算法
- 首先找出所有对象,将存活对象进行标记,然后将存活对象整理到一端,然后把其他内存区域直接清理掉。
- 与
标记-清除算法
比较:标记-整理算法
通过整理阶段消除了碎片问题,但是它的缺点
是回收的停顿时间较长,特别是需要移动大量对象时 - 与
标记-复制算法
比较:标记-复制算法虽然避免了碎片化,但是要求堆空间时原来的两倍,标记整理算法
只使用一个堆内存区域,不需要额外的内存空间来存储活动对象,因此相较于标记-复制算法,它的内存需求较少
2.4 分代收集
现在的商业虚拟机采用分代收集算法
,它根据对象存活周期将内存划分为几块,不同的块采用适当的收集算法。
一般将堆分为新生代
和老年代
,新生代每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
- 对象首先分配在
伊甸园
区域 - 新生代空间不足时,触发
minor gc
,伊甸园
和幸存区from
存活的对象使用copy复制到to
中,存活对象
年龄+1
并且交换
from to的引用(标记-复制算法) minor gc
会引发stop the world,暂停其他用户线程,等待垃圾回收结束,用户线程才恢复运行,因为垃圾回收的过程会改变对象的地址,多线程环境下会出现问题- 当对象寿命超过阈值时,会晋升至老年代,最大寿命时
15
(4bit) - 当
老年代
没有足够的空间来分配给新对象时,会触发full gc
,即进行一次完整的垃圾回收,以释放一些老年代中不再使用的对象,从而为新对象腾出空间(标记-清除/标记-整理)
GC分析
java
private static final int _512KB = 512*1024;
private static final int _1MKB = 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 -Xmn10M -XX:+UserSerialGC -XX:+PrintGCDetails -verbose:gc
//堆内存大小 新生代 指定垃圾回收器 打印详细信息
public static void main(String[] args) {
}
java
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
伊甸园的大小为8192K,其中28%被占用,再放入7MB,肯定放不下,会触发垃圾回收
再放入512KB:
java
list.add(new byte[_512KB]);
再放入512KB,伊甸园肯定不够,又触发了垃圾回收:
如果直接往伊甸园中放入8M,伊甸园的大小为8M,本身还存了一些其他东西,肯定放不下,此时存在一种策略,直接将这8M晋升到老年代:
java
private static final int _512KB = 512*1024;
private static final int _1MKB = 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:+UserSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
- Xms20M: 设置 JVM 初始堆大小为 20MB。
- Xmx20M: 设置 JVM 最大堆大小为 20MB,即堆内存的上限为20MB。
- Xmn10M: 设置新生代大小为 10MB。
- XX:+UseSerialGC: 指定使用串行垃圾收集器。串行垃圾收集器会在一个单独的线程中执行垃圾收集操作,并且在进行垃圾收集时,会停止所有的应用线程,称为停顿时间(Stop-the-World)。
- XX:+PrintGCDetails: 打印详细的 GC 日志,包括每次 GC 的时间戳、堆大小信息、收集器类型等。
- verbose:gc: 开启 GC 日志输出,会在每次进行 GC 时输出相关的信息。
再放8M的话,新生代放不下,老年代也也放不下,再尝试垃圾回收后,还是放不下会出现OutOfMemoryError
(两次试图自救)
(还是不能幸免)
❓ 如果申请内存的过程运行在一个子线程中,它导致的内存溢出会不会导致整个主线程的运行结束呢?
java
private static final int _512KB = 512*1024;
private static final int _1MKB = 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:+UserSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep...");
Thread.sleep(1000L);
}
其实是不会的,子线程出现OutOfMemoryError
,实际上因为没有足够的空间分配所以没有给第二个8M
分配内存,子线程因为异常结束,而对于主线程来说,堆空间是有空余的,或者说子线程结束后就会进行垃圾回收,因为我在子线程结束后在主线程中尝试申请8M
内存是没有出现问题的,所以子线程结束后因该发生了垃圾回收,所以一般情况下,子线程导致的内存溢出只会导致自身的运行结束,不会导致整个主线程的运行结束。
3 用什么回收对象------垃圾收集器
以上为HotSpot中的7个垃圾收集器(或垃圾回收器),图中有连线说明是可以一起搭配使用的。
垃圾收集器分为三类:
- 串行
a. 单线程
b. 适用于堆内存较小的场景,比如说个人电脑- 吞吐量优先
a. 多线程
b. 适用于堆内存较大,多核CPU的场景
c. 让单位时间内,STW的时间最短- 响应时间优先
a. 多线程
b. 适用于堆内存较大,多核CPU的场景
c. 尽可能让单次STW的时间最短
3.1 Serial收集器
java
-XX:+UseSerialGC = Serial + SerialOld
Serial收集器在进行垃圾回收时,必须暂停其他所有工作线程(Stop The World),这样的话,会导致用户线程停止工作,对有些真实应用来说是无法接受的。
特点:
- 以串行的方式执行
- Serial是
新生代
的垃圾收集器 - 采用的算法是
复制算法
- 这是HotSpot虚拟机运行在
客户端模式
下的默认新生代收集器
3.2 ParNew收集器
ParNew收集器实质上是Serial收集器的多线程版本 ,除了同时使用多条线程进行垃圾收集之外,其余的行为包括控制参数,收集算法,Stop The World,对象分配规则,回收策略等都与Serial收集器完全一致,在实现上这两种收集器也公用了相当多的代码。
特点:
- 垃圾收集时多线程并行
- ParNew是
新生代
的垃圾收集器 - 采用的算法是
复制算法
- 是
Server
模式下的虚拟机首选新生代收集器,主要是因为除了Serial收集器,只有它能与CMS收集器(老年代)配合工作 - 使用
-XX:ParallelGCThreads
参数来设置GC线程数
3.3 Parallel Scavenge收集器
该收集器与ParNew类似,都是多线程 的垃圾收集器。其它收集器关注点可能是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标
是达到一个可控制的吞吐量 ,它被称为"吞吐量优先"收集器
。这里的吞吐量指CPU用于运行用户代码的时间占总时间的比值。
🔍 吞吐量 = 运行用户代码的时间/(运行用户代码的时间+运行垃圾收集的时间)
特点:
- Parallel Scavenge是吞吐量优先收集器
- 它是
新生代
垃圾收集器 - 采用的垃圾收集算法是
复制算法
- 有连个精准控制吞吐量的参数
- 控制最大垃圾收集停顿时间:
-XX:MaxGCPauseMills
- 直接设置吞吐量大小:
-XX:GCTimeRatil
- 控制最大垃圾收集停顿时间:
- 可以采用GC自适应调节策略:开启GC自适应调节策略开关后,就不需要指定新生代的大小(-Xmm)、Eden和Survivor区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量
XX:+UseAdaptiveSizePolicy
3.4 Serial Old收集器
是Serial收集器的老年代版本,主要意义也是供客户端模式下的HotSpot虚拟机使用。
特点:
- 它是
老年代
收集器 - 采用的垃圾收集算法是
标记-整理
算法 - gc时会暂停所有用户线程,也就是以
串行
的方式执行 - 主要作为
客户端模式
下的HotSpot虚拟机使用,另外也作为CMS收集器并发收集发生Concurrent Mode Failure
时的后被预案使用
3.5 Parallel Older收集器
Parallel Old是Parallel Scavenge收集器
的老年代版本
,目前只能与新生代的Parallel Scavenge收集器搭配使用,可以说Parallel Old就是为Parallel Scavenge而生的。在这之前Parallel Scavenge收集器就只能与老年代的Serial Older搭配,但是一个多线程,一个单线程,导致吞吐量没有充分的提升,知道Parellel Old收集器的出现。
特点:
- Parallel Old为
Parallel Scavenge
而生,只能搭配Parallel Scavenge - 采用
多线程
进行垃圾收集 - 采用的垃圾收集算法是
标记-整理
算法 - 在注重吞吐率 以及处理器资源较为稀缺 的
场合
,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合 JDK6
时才开始提供
3.6 CMS收集器
CMS
(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间 为目标
的收集器。
目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能的短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的要求。
CMS收集器是基于标记-清除算法
实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:
- 初始标记(CMS initial mark) :需要"Stop The World",且仅仅只是标记一下GC Roots能
直接关联
到的对象,速度很快 - 并发标记(CMS concurrent mark) :
从GC Roots的直接关联对象开始
遍历整个对象图的过程,这个过程耗时比较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行 - 重新标记(CMS remark):重新标记阶段是为了修正并发标记期间,因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短(这个阶段,为了保证一致性,用户线程暂停)
- 并发清除(CMS concurrent sweep) :清理删除掉标记阶段判断的已经死亡的对象,由于
不需要移动存活对象
,所以这个阶段也是可以与用户线程同时并发的。
整个过程中,耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以和用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器是HotSpot虚拟机追求低卡顿的第一次成功尝试,但是也存在一些明显的缺点:
- 吞吐率低 :CMS追求用户线程停顿时间少,而这是通过与用户线程并发执行部分阶段实现的,这样会导致整个垃圾回收需要执行的整体时间会更长,所以吞吐量会更低
- "浮动垃圾"问题 :在标记过程结束后的"并发清除"阶段,由于gc线程是与用户线程并发的,这个期间还会产生新的垃圾,但是由于这一部分垃圾对象实在标记结束后产生的,CMS无法在当时处理它们,只能等待下一次垃圾收集时再处理掉,这部分垃圾就称为
浮动垃圾
。- 同样由于在垃圾收集阶段用户线程还要持续使用,那么就需要预留足够的内存空间提供给用户线程使用 ,不能像其他收集器那样等老年代几乎完全被填满了再进行垃圾收集,否则可能导致
Concurrent Mode Failure
失败,进而导致另一次完全完全"Stop The World"的Full GC的产生,这个失败后的Full GC过程是由Serial Old
收集器来完成的。 - 可以通过
-XX:CMSInitiatingOccupancyFraction
来设置触发CMS的老年代占用百分比
- 同样由于在垃圾收集阶段用户线程还要持续使用,那么就需要预留足够的内存空间提供给用户线程使用 ,不能像其他收集器那样等老年代几乎完全被填满了再进行垃圾收集,否则可能导致
- 空间碎片 :CMS是一款基于"标记-清除"算法实现的收集器,收集结束时可能会有大量碎片空间产生,空间碎片过多时,会给大对象配带来问题,不得不提前触发一次 Full GC
- XX:+UseCMSCompactAtFullCollection开关参数,用于在CMS收集器不得不进行Full GC时,开启内存碎片的合并整理过程,由于这个内存整理必须移动存对象,所以是无法并发的,这样停顿时间又会边长
- 所以还有另一个参数-XX:CMSFullGCsBefore-Compaction,要求CMS收集器在执行过若干次不整理空间的Full GC之后,下一次进入Full GC前会先进行碎皮整理(默认为0,表示每次进入Full GC时都会进行碎片整理)
- 这两个参数都在JDK9开始废弃了
3.7 G1收集器
Garbage First(简称G1)收集器,意味垃圾优先,哪一块的垃圾最多就优先清理它。从名字就可以看出G1的特性,那就是G1能对不同内存进行回收价值和成本的排序 ,即价值越高成本越低的区块会先被回收,另外,我们还能为G1设定性能指标,例如任意1秒内暂停时间不超过10毫秒,G1会尽力去达成这个目标。
G1收集器开创了面向局部收集的设计思路
和基于Region的内存布局形式
。JDK 8 Update 40这个版本以后的G1收集器被Oracle官方称为"全功能的垃圾收集器"。
G1依然还是采用了分代设计,但是和之前的一些垃圾收集器有很大差别,不会再为新生代、老年代等分配规定大小的区域,而是将整个堆分成一个个大小固定的Region,没一个Region都可以是新生代,老年代,Eden空间,Survivor空间的角色,所以Region成为了垃圾回收的最小单元,每一次回收都会是Region的整数倍大小。
Region特性和关键问题总结:
- 所有Eden区和Survivor区合并起来就是年轻代,所有Old区拼在一起就是老年代
- G1每次收集时只会收集部分的region,每次收集时,会先估算每个小块的存货对象总数,回收时,垃圾最多的小块会被优先回收
- Region里面存在的跨Region引用对象如何解决?
- 使用记忆集 避免全堆GC Roots扫描,1.2.3 提到过记忆集,每个Region都维护有自己的记忆集,这些记忆集会记录下
别的Region指向自己的指针
- 使用记忆集 避免全堆GC Roots扫描,1.2.3 提到过记忆集,每个Region都维护有自己的记忆集,这些记忆集会记录下
G1收集器的收集分为四步:
初始标记
:仅仅只是标记一下GC Roots能直接关联到的对象。(需要停顿)并发标记
:从GC Roots开始进行可达性分析,完成对象图的扫描,判断存活对象和可回收对象。做后再处理下SATB记录的所有引用变动的对象。(无需停顿)最终标记
:对用户线程做另一个短暂的停顿,用于处理并发阶段结束后仍然遗留下来的最后那少量的SATB记录。(需要停顿)筛选回收
:统计各个Region的回收价值和成本,并进行比较,根据用户所期望的停顿时间来制定回收计划,筛选任意多个Region构成会收集,然后把决定回收的那一部分Region的存活对象复制
到空的Region中,再清理掉整个旧Region的全部空间(需要停顿)
4 内存分配与回收策略
我们前面提到过,GC是为了自动回收内存,简化内存管理,实际Java为了简化内存管理,实现了自动内存管理 ,而自动内存管理除了包含自动回收分配给对象的内存
,还包括自动给对象分配内存
。
- 对象优先在Eden分配:大多数情况下,对象在新生代Eden中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(即新生代GC)
- 大对象直接进入老年代 :大对象就是指需要大量连续内存空间的Java对象,最典型大对象便是那种很长的字符串,或者元素数量很庞大的数组。大对象会直接进入老年代,可以设想一下,如果大对象被分配在新生代,又因为新生代多采用复制算法,如果一个大对象能存活很久的话,那么复制开销将会是非常大的。(在Java虚拟机中要避免大对象的原因是,它容易导致,内存明明还有不少空间时就提前触发垃圾收集器,以获取足够的连续空间才能安置好它们,而且当复制对象时,大对象就意味着高额的内存复制开销。)
- HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配。
- 长期存活的对象将进入老年代:对象头里存储了对象的分代年龄,新生代的对象每经历一次Minor GC年龄就会增加一岁,当年龄达到一定程度(默认15,-XX:MaxTenuringThreshold用来配置这个年龄),就会晋升为老年代。
- 动态对象年龄判断:并不是永远要求对象的年龄必-XX:MaxTenuringThreshold才能晋升老年代,如果在Sruvivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无需等到-XX:MaxTenuringThreshold中要求的年龄
- 空间分配担保 :Minor GC可能会导致一大批对象从新生代进入老年代,那老年代放不下怎么办?
- 每次Minor GC前都检查老年代最大可用的连续空间是否大于新生代所有对象总空间
- 如果大于那么这一次Minor GC就是安全的
- 如果不大于,虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败
- 如果允许,那么继续检查老年代最大可用的连续空间是否大于晋升到历次晋升到老年代对象的平均大小
- 如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的
- 如果小于就进行Full GC
- 如果不允许,那这时改为执行一次Full GC
- 如果允许,那么继续检查老年代最大可用的连续空间是否大于晋升到历次晋升到老年代对象的平均大小
- 每次Minor GC前都检查老年代最大可用的连续空间是否大于新生代所有对象总空间