垃圾收集器和内存分配策略

说明:本篇文章是在阅读《深入理解Java虚拟机》过程中的一些笔记和分析,由于本人能力有限,如果有书写错误的地方,欢迎各位大佬批评指正!我们互相交流,学习,共同进步!

该项目的地址:https://github.com/xiaoheng1/jvm-read

GC 需要考虑三件事:
(1)什么样的对象需要回收?
(2)什么时候回收?
(3)怎么回收?

我们知道:程序计数器、栈、本地方法栈这三个区域的生命周期和线程是一样的,所以这块的内存的回收可以不用考虑. 关于栈的内存分配,栈中局部
变量表的大小,一般来说在编译器就可知,所以这块的内存分配我们也可以不用考虑. 我们需要专注的是是对堆内存和方法区内存的分配和回收.

我们现在来看第一个问题,什么样的对象需要被回收?

我们的答案肯定是没有用的对象. 那如何确定这个对象是否有用了?引用计数法.

引用计数法的思路是:给对象中增加一个引用计数器,每当有一个地方引用它时,引用计数器 +1,当引用失效时,引用计数器 -1. 任务时候,引用
计数器为 0,则说明对象不可能在被使用.

引用计数器实现虽然简单,但是无法解决相互引用的问题,例如:

A a = new A();
B b = new B();

a.instanceB = b;
b.instanceA = a;

a = null;
b = null;

那我们说下第二种思路:可达性分析. 可达性分析是说通过一系列称为 “GC Roots” 的节点出发,开始向下搜索,搜索走过的路径称为引用链,任意
一个对象到引用链不可达,则说明该对象不可用.

那 Java 中可作为 GC Roots 的节点有哪些了?
(1)虚拟机栈中引用的对象
(2)方法区中类静态属性所引用的对象
(3)方法区中常量所引用的对象
(4)本地方法栈中引用的对象

无论是通过引用计数器,还是可达性分析来判断对象是存活,都和引用有关. Java 中的引用可以分为四种:强引用、软引用、弱引用、虚引用.
(1)强引用,类似这样的 A a = new A(); a 持有的就是强引用. 强引用只要存在,GC 就不会回收被引用的对象.
(2)软引用,用来描述一些有用,但是非必须的对象. 在系统将要发生内存溢出异常之前,会把这些对象进行第二次回收,如果还是没有足够内存,才会
抛出内存溢出异常.
(3)弱引用,用来描述一些非必须的对象,只能存活到下一次发生 GC 之前. 也就是说,当发生 GC 时,无论系统中内存是否足够,这类对象都将被
回收.
(4)虚引用,它是最弱的一个引用,它只是用在对象被回收的时候,收到一个系统通知.

那我们在来说下,当发现一个对象到引用链没有路径时,这个对象是否必须被回收?其实不是的. 确定一个对象确实死亡有至少有两次确认.
(1)该对象不可达
(2)在该对象不可达的基础上做一次筛选,看该对象的 finalize() 方法是否被调用,如果该对象没有重写 finalize() 方法或者 finalize()
方法被调用,则宣告该对象死亡,可以进行回收.

如果该对象被判定需要执行 finalize() 方法,那么该对象会被加入到 F-Queue 的队列中,稍后由虚拟机建立的一个低优先级的线程进行执行.
finalize() 方法是对象逃脱死亡的最后一次机会. 如果对象在 finalize() 方法中完成自我救赎,那么它将被移除 “即将回收的集合”,否则,它
将等待被回收.

值得注意的是:finalize 方法只会被调用一次,同时该方法的执行代价大,不确定性高,所以大家最好是不要使用该方法.

回收方法区

在 JDK1.8 以前,方法区在 HotSpot 虚拟机中被称为永久代. 但是这并不意味着方法区不会发生垃圾回收,只是说回收的比率比新生代低.

永久代的垃圾回收主要回收两部分:
(1)废弃常量
(2)无用的类

回收废弃常量:加入字面量 “hello world” 进入到了常量池,但是系统中没有任何一个 String 对象叫做 “hello world”, 换句话说,没有任何
一个字符串对象引用常量池中的 “hello world” 常量,也没有其他地方引用这个字面量,那么在发生 GC 时,且有必要时,这个 “hello world”
将会被清理出常量池.

看到这块是否很疑惑?什么叫做没有其他地方引用这个字面量了?比如说:System.out.println(“hello world”) 这个算其他地方引用了这个
字面量吗?还有就是什么叫有必要了?难道说发生 GC 后,还要满足一定的条件才能回收这个废弃常量吗?

我觉得吧,对于 System.out.println(“hello world”) 这个应该算在其他地方使用到了这个 “hello world” 常量.

new String(“hello world”) 会先检查字符串常量池中是否存在 “hello world”,如果不存在的话,先在字符串常量池中创建 “hello world”,
然后在堆中创建一个 “hello world” 的字符串对象.

判定一个类是否是"无用类"的条件相对比较苛刻. 需要满足如下条件:
(1)该类的所有实例都已经被回收
(2)加载该类的 ClassLoader 已经被回收
(3)该类对应的 java.lang.Class 对象没有在任何地方被引用.

满足上面三个条件,只是说这个类可以被回收,它不像对象那样,没有就被回收. 是否对类进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行
控制,还可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看类加载和卸载信息.

垃圾收集算法

(1)标记-清除算法:它分为两个阶段,第一阶段标记,首先标记所有需要被回收的对象,第二阶段回收被标记的对象. 虽然这个算法简单,但是它的
标记和清除效率都不高,且会产生碎片.

(2)复制算法:将内存空间分为两块,一块用于存放新生对象,另一块存放存活对象.当一块内存满了后,就将存活的对象拷贝到另一块上去,然后清空
以前那块的内存. 这个方法的好处不会产生内存碎片,缺点是内存利用率不高,每次只能使用一半的内存.

新生代中,98% 的对象都是朝生夕死,所以复制算法划分空间的时候,并不需要按照 1:1进行划分.
Eden : From Survivor : To Survivor = 8 : 1 : 1.
每次使用的时候,使用 Eden + 一个 Survivor 的空间,换句话说,新生代的可利用空间在 90%,只有 10% 的空间会被浪费. 但是是不是有
一个疑惑?10% 的空间够存放活着的对象吗?所以这有一个担保(老年代进行担保). 如果活着的对象过多,一个 Survivor 空间不够,那么这些对象
将直接通过分配担保机制进入到老年代.

现在来说下为啥不使用 0 个 Survivor ?如果使用 0 个 Survivor 的话,那么新生代满了后,触发GC,活着的对象进入到老年代(可能这些对象
在下一次GC的时候就会被回收),很快,老年代也满了,Full GC 发生的频率大大加大.

为啥不是一个 Survivor 了?如果是一个 Survivor 的话?那 Eden 和 Survivor 和比例如何划分了?假设设置为 8:1,那么 Survivor 的
空间很容易被填满,触发 Minor GC. 这样总体上没有降低 Minor GC 的频率,而且 GC 的时间间隔也不平均,如果将 Eden : Survivor 设置
为 1 : 1 的话,内存利用率不高.

使用两个 Survivor 的话,为啥就可以了?使用两个的话,在触发 GC 时,会回收 Eden 和 Survivor 区域内的对象,这样活着的对象更加少,
所以效率更高.

(3)标记-整理算法:第一阶段也是标记,第二阶段是让活着的对象都向一段移动,然后直接清理掉端边界以外的内存.

(4)分代收集算法:根据对象的特性,划分为不同的代,比如新生代和老年代. 针对不同代对象的特性,采用不同的回收算法. 例如:对新生代采用
复制算法,对老年代采用标记-清除或标记-整理算法.

Hotspot 的算法实现

上面我们只是说了可达性分析的思路,在 HopSpot 中是如何做的了?

枚举根节点.

现在很多应用的方法区中就有好几百兆,如果逐个检查这里面的引用,那么将会是非常耗时的. 在进行可达性分析的时候,系统必然在某个时间点上
进行冻结(不能我在执行可达性分析的时候,引用还是在变化,那么将无法获得准确的分析).所以这点会导致 GC 进行时必须停止所有Java执行程序
(Stop the World).

保守式 GC:如果 JVM 不记录任何这种类型的数据,那么它就无法区分内存中某个位置上的数据到底应当解读为引用类型还是整型还是其他?这种
情况下实现出来的 GC 就是保守 GC. 所以在进行 GC 的时候,就需要遍历整个内存空间,看这是不是一个指向堆中的指针,虽然这种实现方式很
简单,但是效率太低.

目前主流的 Java 虚拟机使用的都是准确式 GC,也就是说虚拟机应当有办法知道哪些地方存放着对象的引用. 在 HotSpot 的实现中,是使用一组
称为 OopMap 的数据结构来达到这个目的的,在类加载完成的时候,HotSpot 就把对象内什么偏移量上是什么类型的数据计算出来,在 JIT 编译
过程中,也会在特定位置记录下栈和寄存器中那些位置是引用. 这样,GC 在扫描时就可以直接得知这些信息了.

在 OopMap 的协助下,HotSpot 可以快速且准确地完成 GC Roots 枚举,但是如果为每条指令都生成对应的 OopMap,那将会需要大量的额外空间,
这样 GC 的空间成本将会变得很高.

下面说下 oopmap. oopmap 有一个 off 字段,我的理解是从指令开始,到 off 为止,这个 oopmap 记录的就是这个范围内的 oop(普通指针对象).

实际上,HotSpot 也确实没有这么干,它只是在特定的位置记录了这些信息,即程序执行时并非所有地方都能停顿下来执行 GC,只有达到安全点时才能
暂停. Safepoint 的选定即不能太少以至于让 GC 等待时间太长,也不能过于频繁以至于过分增大运行时的负荷.

所以安全点的选定基本上是以程序 是否具有让程序长时间执行的特征 为标准进行选定的. 因为每条指令执行的时间都非常短,程序不太可能因为指令
流长度太长这个原因而过长时间运行,‘长时间执行’ 的最显著特征是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以这些功能的指令才
会产生 Safepoint.

对于 Safepoint 另一个需要考虑的问题是,如何在发生 GC 时让所有程序都到最近的安全点上在停顿下来. 这里有两种方案可供选择:抢先式中断和
主动式中断,其中抢先试中断不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,
就恢复线程,让它跑到安全点上. 现在几乎没有虚拟机采用抢先式中断来暂停线程从而响应 GC 事件.

主动式中断的思想是当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动的去轮询这个标志,发现中断
标志为真时就自己中断挂起. 轮询标志的地方和安全点是重合的,另外在加上创建对象需要分配内存的地方.

安全区域

上面其实有一个问题,针对执行程序这个可以完美实现,但是如何针对不执行的程序了?(例如:等待获取锁),对于这种情况,就需要安全区了.

安全区域是指在一段代码片段之中,引用关系不会发生变化. 在这个区域中的任意地方开始 GC 都是安全的. 我们也可以把 Safe Region 看成是
Safepoint 的扩展.

当线程执行到 Safe Region 中的代码时,首先标识自己已经进入了 Safe Region,那样,当在这段时间里 JVM 要发起 GC 时,就不用管标识自己为
Safe Region 状态的线程了. 在线程要离开 Safe Region 时,它要检查系统是否以及完成了根节点的枚举(或者整个 GC 过程),如果完成了,则
继续执行,否则等待收到可以安全离开 Safe Region 的信号位置.

垃圾收集器

Serial + CMS
Serial + Serial Old

ParNew + CMS
ParNew + Serial Old

Parallel Scavenge + Serial Old
Parallel Scavenge + Parallel Old

G1

Serial 收集器是最基本、发展历史最悠久的收集器,这是一个单线程收集器,它在进行垃圾收集的时候,必须暂停其他所有的工作线程,直到它
收集结束. 它是新生代收集器,采用复制算法. 它是虚拟机 Client 模式下的首选.

关于虚拟机 client 模式和 server 模式.

如果主机至少含有 2 cpu 和至少 2 GB 内存的话,会以 server 模式启动,否则以 client 模式启动.
server 模式和 client 模式的区别在哪里了?

1.server 模式和 C2 编译器共同运行,更注重编译的质量,启动速度慢,但运行效率高,使用与服务器环境.
2.client 模式和 C1 编译器共同运行,更注重编译速度,启动速度快,更适合在客户端的版本下,针对 GUI 进行优化.

Serial Old 是老年代收集器,采用标记-整理算法. 主要意义在于给 Client 模式下的虚拟机使用. 如果在 Server 模式下,一种用途是在
JDK1.5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备预案,在并发收集发生
Concurrent Mode Failure 时使用.

ParNew 其实是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和 Serial 一样. 它是虚拟机 Server 端首选的新生
代收集器.默认情况下,它开启的收集线程数与 CPU 的数量相同,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数.

并行:指多条垃圾收集线程并行工作,但是此时用户线程任然处于等待状态
并发:指用户线程和垃圾收集线程同时执行(但不一定是并行,可能交替执行)

Parallel Scavenge 是新生代收集器,它与其他收集器的不同点在于,其他收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿状态,而
Parallel Scavenge 的关注点是达到一个可控制的吞吐量. 所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即:
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和’标记-整理’算法.

CMS 是一种以获得最短回收停顿时间为目标的收集器. 它是基于标记-清除算法实现的. 它的运作过程相对于前面几种收集器更加复杂.
(1)初始标记
(2)并发标记
(3)重新标记
(4)并发清除

初始标记和重新标记这两步仍然需要 STD, 初始标记只是标记 GC Root 能够直接关联到的对象,速度很快,并发标记阶段就进行 GC Roots Tracing
的过程,而重写标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录.

CMS 收集器对 CPU 资源很敏感,在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分 CPU 资源而导致应用程序变慢,总吞吐量会降低.
CMS 默认的启动的回收线程数是(CPU 数量+3)/4,也就是当 CPU 在 4 个以上时,并发回收时垃圾收集线程不少于 25%的CPU资源. 但是当 CPU 资源
不足 4 个时,CMS 对用户程序的影响很大,为了应对这种情况,虚拟机提供了一种 i-CMS,就是在并发标记和清理阶段,让 GC 线程和用户线程交替
运行.
CMS 收集器无法处理浮动垃圾,可能出现 Concurrent Mode Failure 失败而导致另一次 Full GC 的产生. 由于 CMS 并发清除阶段程序运行
自然还会产生新的垃圾,这部分垃圾出现在标记过程后,CMS 无法在本次收集中处理掉它们,只好留待下一次处理. 这部分垃圾就被称为"浮动垃圾".
也是由于垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此 CMS 收集器不能像其他收集器那样等到老年代
几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运行使用. CMS 收集器当老年代使用了 68% 的空间后就会被激活. 也可以
通过设置 -XX:CMSInitiatingOccupancyFraction 的值来提高触发百分比.如果 CMS 运行期间预留的内存无法满足程序需要,就会出现一次
“Concurrent Mode Failure” 失败,这时虚拟机将启动后背预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集.
CMS 是基于标记-清除算法的收集器,收集结束后会产生大量空间碎片,为了解决这个问题,CMS 收集器提供了 -XX:+UseCompactArFullCollection
开关参数 用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片合并整理过程.
现在虽然解决了碎片化的问题,但是停顿时间不能太长,虚拟机提供了另一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少
次不带压缩的 Full GC 后,跟着来一次带压缩的(默认值为 0,表示酶促进入 Full GC 时都要进行碎片整理)

GC 收集器
G1 是一款面向服务端应用的垃圾收集器,G1 收集器的特点
1.并行与并发:G1能重复利用多 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 STW 停顿时间,部分其他收集器原本需要停顿 Java 线程来执行
GC 动作,G1 收集器任然可以通过并发的方式让 Java 程序继续执行.
2.分代收集:虽然 G1 可以不需要其他收集器配合就能管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过
多次 GC 的旧对象以获取更好的收集效果.
3.空间整合: 与 CMS 标记-清除 算法不同,G1 整体来看是基于 标记-整理 算法实现的. 从局部(两个 Region 之间)上来看,是基于赋值算法实现
的,但无论如何,这两种算法都意味着 G1 在运行期间不会产生内存碎片.
4.可预测的停顿:这是 G1 相对于 CMS 的另一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求停顿外,还能建立可预测的停顿
时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片内,消耗在垃圾收集上的时间不得超过 N 毫秒,这几乎已经是实时 Java 的垃圾收集器
的特征了.

G1 之前的其他收集器进行收集的范围都是整个新生代或老年代,而 G1 不是这样,使用 G1 收集器时,Java 堆的内存布局就与其他收集器有很大差别,
它将整个Java 堆划分为多个大小相等的独立区域,虽然还保留有新生代和到年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分
Region 的集合.

G1 收集器之所以能简历可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集. G1 跟踪各个 Region 里面的
垃圾堆积的价值大小(回收所得空间以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region
(这也就是 Garbage-First 名称的来由). 这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以
获取尽可能高的收集效率.

在 G1 收集器中,Region 之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,虚拟机都是使用 Remembered Set 来避免全堆扫描
的,G1 中每个 Region 都有一个与之对应的 Remembered Set,虚拟机发现程序在堆 Reference 类型的数据进行写操作时,会产生一个 Write Barrier
暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),
如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 中. 当进行内存回收时,在 GC 根节点的枚举
范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏.

初始标记
并发标记
最终标记
筛选回收

初始阶段仅仅只是标记一下 GC Roots 能直接关联到的对象,并修改 TAMS(Next Top Ar Mark Start) 的值,让下一阶段用户程序并发运行时,
能在正确可用的 Region 中创建新对象,这一阶段需要停顿线程,但耗时很短. 并发标记阶段是从 GC Root 开始对堆中对象进行可达性分析,找出
存活的对象,这阶段耗时较长,但可与用户程序并发执行. 最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一
部分记录,虚拟机将这段时间内对象变化记录在线程 Remembered Set Logs 里,最终标记阶段需要把 Remembered Set Logs 的数据合并到
Remembered Set 中,这阶段需要停顿线程,但是可并发执行. 最后在筛选回收阶段首先对各个 Region 的回收价值和成本进行排序,根据用户所
期望的 GC 停顿时间来指定回收计划.

理解 GC 日志

33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]

100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

33.125 和 100.667 代表了 GC 发生的时间,这个数字的含义是从 Java 虚拟机启动以来经过的毫秒数
[GC 和 [Full GC 说明了这次垃圾收集的停顿类型,而不是用来区分新生代 GC 还是老年代 GC 的. 如果有 Full,则说明此次 GC 是发生了 STW.
[DefNew、[Tenured、[Prem 表示 GC 发生的区域.
在 Serial 收集器中,新生代的名称为 Default New Generation
如果是 ParNew 收集器,新生代的名称为 ParNew -> Parallel New Generation
如果是 Parallel Scavenge,则新生代名称为 PSYoungGen

3324K->152K(3712K) 含义是 GC 前该内存区域已使用容量 -> GC 后该内存区域已使用容量(该内存区域总容量)
3324K->152K(11904K) 表示 GC 前 Java 堆已使用容量 -> GC 后 Java 堆已使用容量(Java 堆总容量).
0.0031680 表示该内存区域 GC 所占用的时间,单位是秒.
user, sys, real 分别代表用户态消耗的 CPU 时间、内核态消耗的 CPU 时间和操作空开始到结束所经过的墙钟时间.
墙钟时间包括各种非运算的等待耗时,例如等待磁盘 I/O、等待线程阻塞,而 CPU 时间不包括这些耗时,但是当系统有多核 CPU或者多核的话,多线程
会叠加这些时间,所以读到的 user 或 sys 时间超过 real 是正常的.

内存分配与回收策略

Java 技术体系锁提倡的自动内存管理最终解决两个问题:给对象分配内存以及回收分配给对象的内存.

对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配. 少数情况下也可能会直接分配在老年代中.
分配规则并不是百分百固定的,其细节取决于当前使用的哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置.

1.对象优先在 Eden 区上分配.
在大多数据情况下,对象在新生代 Eden 区中分配,当 Eden 区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC.
虚拟机提供了 -XX:PrintGcDetails 这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在退出的时候输出当前的内存
各个区域分配情况.

新生代 GC(Minor GC) 发生在新生代的垃圾收集动作,因为 Java 对象大多是朝生夕死,所以 Minor GC 非常频繁,一般回收速度较快.
老年代 GC(Major GC / Full GC) 发生在老年代的 GC,出现了 Major GC, 经常会伴随至少一次 Minor GC(但非绝对,在 Parallel Scavenge
收集器的收集策略里就有直接进行 Major GC 的策略选择过程). Major GC 事务速度一般比 Minor GC 慢 10 倍以上.

2.大对象直接进入老年代

所谓大对象,指的是需哟啊大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组, 经常出现大对象容易导致内存还有不少
空间时就提前触发垃圾收集以获取足够连续的空间来 “安置” 他们.
虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配. 这样做的目的是避免在 Eden 区以及在两个
Survivor 区之间发生大量的内存复制.

PretenureSizeThreshold 参数只对 Serial 和 ParNew 两款收集器有效,Parallel Scavenge 收集器不认识这个参数.

3.长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄(Age) 计数器, 如果对象在 Eden 出生并经过第一次 Minor GC 后任然存活,并且能被 Survivor 容纳的话,
将被移动到 Survivor 空间中,并且对象年龄设为 1. 对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定
程度(默认为 15 岁),就将会被晋升到老年代中. 对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置.

4.动态对象年龄判定

如果在 Survivor 空间中相同年龄所有对象的总和大于 Survivor 空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到
MaxTenuringThreshold 中要求的年龄.

5.空间分配担保

在发送 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 是安全的,
如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许,那么会继续检查老年代最大可用连续空间是否大于
历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者
HandlePromotionFailure 设置不允许冒险,那么这时也好改为进行一次 Full GC.

新生代采用了复制收集算法,但是为了内存利用率,只使用其中一个 Survivor 空间来作为轮换备份,因此当出现大量对象在 Minor GC 后任然存活
的情况(最极端的情况在 Minor GC 后新生代所有对象都存活),就需要老年代进行分配担保,把 Survivor 无法容纳的对象直接进入老年代.

在 JDK 6 Update 24 之后,HandlePromotionFailure 这个参数不会再影响到虚拟机的空间分配担保策略,代码中已不再使用该参数.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值