目录
1. 什么时候会触发Young GC, 什么时候会触发Old GC
2. 触发新生代GC,如果存活对象总量大于survivor区容量,咋办
8. CMS哪几个阶段需要STW, G1哪几个阶段需要STW
9. GC Roots有哪些,实例变量可以作为RC Roots吗
JVM管理的内存中,虚拟机栈、本地方法栈和程序计数器的生命周期和线程的生命周期一致,随着栈帧的出栈实现自动的内存清理,因此JVM中需要GC的内存区域主要在Java堆和方法区。
程序所使用的对象和数组填充满Java堆之后,会抛出out of memory异常,程序挂掉,GC机制将不再使用的对象和数组删除,整理内存以便后续使用。
对象已死?
需要进行回收的对象的已经不再存活的对象(不再使用),判断对象是否存活的方法有:
(1)引用计数(缺点:可能存在循环引用的问题,导致GC无法回收,内存泄漏);
(2)对GC Roots对象可达(Java GC使用的方法);
GC Roots对象
(1)方法区内 类静态属性/常量 引用的对象;
(2)Java虚拟机栈(栈帧中的本地变量表)中引用的对象,比如线程方法栈中使用的参数、局部变量、临时变量等;
(3)本地方法栈中JNI引用的对象;
(4)同步锁持有的对象;
引用类型
(1)强引用,通常说的引用就是指定强引用,只要强引用还在,GC就不会回收该引用引用的对象;
(2)软引用(SoftReference),这类引用引用的对象在内存溢出之前会被GC先回收掉,如果内存仍不够实例分配,则抛出OOM异常;
(3)弱引用(WeakReference),这类引用引用的对象只要发生垃圾收集都给被GC回收掉;
(4)虚引用(PhantomReference),这类引用的唯一目的就是当它引用的对象被回收时收到系统通知。
垃圾收集算法
(1)标记清理 (Mark-Sweep),标记存活的对象,将未标记的对象删除,缺点:a.造成内存碎片,导致内存足够但是大的内存分配失败;b.执行效率不稳定,随着对象数量的增加而降低;
(2)标记整理,删除垃圾后,将存活的对象前移整理,缺点:代价大,需要移动存活对象,必须STW才能进行;
(3)复制算法,将内存一分为二,GC将存活的对象复制到另一半内存中,然后前该半内存中的垃圾删除,缺点:可用空间只有原来的一半;
(4)分代收集算法,现在的JVM大多是采用这种方式,根据对象的生存周期。将堆分为新生代和老年代,新生代有分为一个Edan区和两个Survivor 区(E区和S区内存大小一般8:1),因此GC又分为Young GC和Old GC。在新生代中,由于对象周期短(存活对象少),因此Young GC采用复制算法,老年代里对象存活率高,因此Old GC使用标记清理或标记整理算法。对象分配会在Edan区和S from区上进行,当发生依次YoungGC,存活的对象会复制到S to区,如果S to区放不下,则直接存放到老年代Old区。
堆内存分配参数
-Xms20M 表示初始堆内存20M;
-Xmx30M 表示堆可扩展,最大30M;
-Xmn10M 表示新生代10M,剩下的10M为老年代的大小;
-XX:SurvivorRatio=8 表示E:S=8:1
如何触发GC
(1)通过System.gc();手动触发;
(2)JVM自己管理,当内存不足时,自动触发;
1)当新生代的E区满时,触发Young GC,采用复制算法将存活对象复制到S区,并将失活的对象回收;
2)当老年代空间不足 / 方法区空间不足
GC收集器
古早期的GC:
CMS(Concurrent Mark Sweep)收集器
极大的降低StopTheWorld时间,是服务端常用的垃圾收集器,配合PawNew使用。但是由于采用的是标记清理的算法,存在内存碎片。CMS收集器工作过程:
1 初次标记,需要STW,标记GC roots直接关联的对象,耗时短;
2 并发标记,GC Root Tracing,耗时长,和其他线程一起工作,从GC roots的直接关联对象开始遍历整个对象图的过程;
3 重新标记,需要STW,修正并发标记中的错误(基于增量更新),耗时短;
4 并发清除,清理内存,不移动存活对象,所以可以并发进行,和其他线程一起工作。
CMS容忍内存碎片的存在,当内存碎片多到影响对象分配时,再采用一次标记-整理算法收集一次(因此常结合Serial Old收集器(使用的是标记-整理算法))。
CMS收集器的三个缺点
1. 并发设计,对处理器资源敏感,和工作线程抢夺处理器资源;
2. 无法处理浮动垃圾;
3. 采用标记-清除算法,产生内存碎片。
下面这张图将这种 串行GC,并行GC 以及CMS的多次标记并发清理表达的很清楚。
注:图片截取自https://www.bilibili.com/video/av89885794?p=2
现代的GC
G1 (Garbage First)收集器
JDK9之后主流的GC收集器
G1的内存结构
G1首先在内存结构上采用了region化的方法,将堆内存划分成2000块左右的小块,每块大小1-32M(2的幂次),每块region都可以作为E、S、O以及H区(本质还是O区,存储较大对象)
关于Region的一个关键:Remember Set(Rset), 记录region的对象被其他region对象的引用,目的时缩小标记时所需遍历范围。
YGC采用 标记-复制算法
OGC采用MixGC,优先回收存活对象最少的区,主要工作过程:
(1)初次标记:标记 GCRoots 直接引的对象和所在Region,但是与CMS不同的是,G1不止标记O区。初次标记一般和YGC同时发生。
(2)RootRegion扫描 : 标记 GCRoots所在的region到O区的引用的对象,这个过程会将S区的存活对象存到O区;
(3)并发标记:不需要STW, 并发标记整个堆,这个时候会计算每个区的存活率,如果发现某个Region中所有对象失活,将其标记为X区;
(4)重新标记:短暂STW,收集 并发标记阶段 工作线程产生新的垃圾,并直接删除并发标记过程中标记的X区;G1中采用了比CMS更快的原始快照算法:snapshot-at-the-beginning (SATB);
(5)筛选回收:采用复制-清理算法,清除失活对象。G1将回收区域的存活对象拷贝到新区域 并 清除Remember Sets,清空回收区域并把它返回到空闲区域链表中。由于涉及存活对象的移动,所以需要STW.
总结G1 收集器和其他收集器(主要是CWS)区别
优势
1. 内存结构不同。G1 内存结构将JVM管理的内存分成很多的小块,更灵活应用内存;
2. 增加Root Region Scanning阶段,借助Remember Set这一数据结构记录Region中对象在别的Region的引用,缩小标记遍历范围;
3. 重新标记过程中采用比CMS(增量更新算法)更快的SATB算法,且直接删除X区,提高效率;
4. MixGC优先回收对象存活率低的区,而不是全部清除,达到垃圾清理的要求且最大限度的降低STW时间,提高效率;
5. YGC和OGC均使用的是复制-清理的算法,不会造成垃圾碎片;
劣势
1. 从为了收集垃圾而产生的内存占用上来看,G1和CMS都使用卡表(记忆集)来处理跨代引用,但G1的记忆集的本质是一种哈希表,Key是引用别的Region的起始地址,Value是一个集合,存储卡表的索引号,这种双向的卡表记录了“我指向谁”和“谁指向我”,比CMS的卡表更加复杂,并且G1的Region数据多,CMS只需要老年代到新生代的唯一卡表,因此G1收集器要比CMS有着更高的内存占用负担;
2. 在重新标记阶段,相比CMS的增量更新算法,G1使用原始快照算法能够减少并发阶段和重新标记阶段,减少重新标记阶段的STW时间,但是在工作线程运行过程中确实会产生由跟踪引用变化带来的额外负担。
内存分配策略
1. 对象优先在E区分配;
2. 大对象直接进入老年代,使用-XX:PretenureSizeThreshold=x参数设置大于x的对象直接分配到老年代(PretenureSizeThreshold单位为byte,比如1M要写-XX:PretenureSizeThreshold=1048576);
3. 长期存活的对象进入老年代,对象头中有4个字节用来记录对象经过GC后存活的次数(年龄),默认超过15岁(1111)将晋升到老年代,可以使用-XX:MaxTenuringThreshold=x设置年龄的阈值;
面试相关问题
1. 什么时候会触发Young GC, 什么时候会触发Old GC
1) 当对象在Edan区无法完成空间分配,则会触发Young GC(标记-复制),将存活的对象保存到Survivor或者通过分配担保机制直接保存到Old区;
2) Young GC执行之前,会先判断Old区连续可用空间是否小于年轻代之前平均保存过来的对象大小,如果小于,则认为Young GC存活到Old区的对象可能在Old区也放不下,则先触发Old GC收集垃圾腾出空间,再执行Young GC;
3) Young GC发生,存活到Old区的对象如果在Old区仍放不下,则触发Old GC继续腾空间。
2. 触发新生代GC,如果存活对象总量大于survivor区容量,咋办
HotSpot虚拟机规定,Survivor区中相同年龄age所有对象大小总和大于Survivor区空间的一半时,年龄大于或等于age的对象就可以直接进入老年代,无须等待-XX:MaxTenuringThreshold中要求的年龄。
3. Java虚拟机对标记-复制算法的优化/分代收集理论
早期的标记-复制算法也被称为“半区复制”,将内存按容量划分为大小相等的两块,每次只使用其中一块。这种1:1的内存空间划分使得内存使用率只有50%,极大的浪费。
之后出现新的内存布局,将内存划分为新生代和老年代,新生代中分为一块较大的Edan区和2块较小的Survivor区,也被称为分代收集。
分代收集理论建立在两个假说上:
1)大多数对象“朝生夕死”;
2)少量多次存活的对象难以消亡。
每次GC只需要标记少量存活的对象,以较低的代价回收大量空间,难以消亡的对象集中存储,GC不需要高频回收此区域,这样同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
4. 分代收集理论->跨代引用假说->记忆集
假如进行一次Young GC,由于新生代中的某些对象可能被老年代引用(跨代引用),因此除了扫描GC Roots之外,还是需要扫描老年代,但是因为这些对象被老年代引用,那么它们会因为老年代引用它们的对象存在而不会被回收,因此将这些对象一起存入老年代,消除跨代引用。
记忆集就起到了记录跨代引用的作用,在新生代上建立一个全局的数据结构-记忆集(Remembered Set, Rset),这个结构将老年代内存划分为很多小块,标识出老年代的哪一块内存存在对新生代对象的跨代引用,之后发生Yunger GC时,只需要扫描Rset记录的内存,避免将整个老年代加进GC Roots扫描。
5. 记忆集的精度,卡表本质
记忆集作为一种记录非收集区(比如上述的老年代)对收集区(比如上述的新生代)的引用集合的抽象数据结构,主要存在以下三种记录精度:
1)字长精度,记忆集中一个记录精确到一个机器字长,这个字中存储了到新生代对象的引用;
2)对象精度,记忆集中一个记录精确到一个对象,这个对象里有字段存储了到新生代对象的引用;
3)卡精度,记忆集中一个记录精确到一块内存区域,该区域内存储了到新生代对象的引用;
目前最常用的卡精度,也被称为卡表(Card Table)的方式实现记忆集。(不要搞混这个概念噢,记忆集是一种抽象的结构,卡表是记忆集的一种具体实现)
HotSpot中卡表有字节数组实现,每个元素对应非收集区的一块特定的内存区域,如果该区域中存在对收集区的引用,则将该元素置1,没有则置0。对收集区垃圾收回时,只需要扫描1对应的内存块。
6. 什么情况下新生代的对象会进入老年代
1) 默认经过15次回收仍存活的对象,可以使用-XX:MaxTenuringThreshold设置次数(年龄阈值);
2)Survivor区中相同年龄age所有对象大小总和大于Survivor区空间的一半时,年龄大于或等于age的对象就可以直接进入老年代(分配担保);
3) 大对象直接在老年区分配,可以通过-XX:PretenureSizeThreshold参数指定大对象内存阈值。
7. 标记-清理算法会产生内存碎片,虚拟机是怎么优化的
早期的垃圾收集器(Serial Old, Parallel Old)采用标记-整理算法,将存活对象前移,这样就不会存在内存碎片。
由于对象移动必须STW,后来CMS都采用标记-清理算法,不需要移动对象,这样清除工作和用户线程并发进行,直到没有足够的连续空间可以保存对象时,才进行一次标记-整理。(比如CMS结合Serial Old)
8. CMS哪几个阶段需要STW, G1哪几个阶段需要STW
CMS的初次标记阶段、重新标记阶段STW
G1的初次标记阶段、重新标记阶段、筛选回收阶段STW
9. GC Roots有哪些,实例变量可以作为RC Roots吗
GC Roots有
1) 方法区中静态变量/常量引用的对象;
2)Java虚拟机栈中,本地变量表引用的对象;
3)本地方法栈中JNI引用的对象;
4)同步锁持有的对象等。
实例变量可以存储在堆上,不能作为GC Roots。但是被同步锁持有的实例对象可以作为GC Roots。