社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
摘要:本文讲解讲解常见的垃圾收集算法,首先思考三个问题:1、哪些内存需要回收?2、什么时候回收?3、如何回收? 然后讲解内存回收的具体实现-垃圾收集器 ,最后对Java中 对象引用类型及具体使用场景 做了探讨,jvm的自动垃圾回收策略使得程序员摆脱了编程中繁杂的内存管理,可以把精力专注于系统业务。
讲讲你对垃圾回收机制的理解(问题问的很宽泛,就看你怎么回答和理解)
什么是垃圾,为什么要回收,不回收有什么问题,jvm有哪些区域,分别采用哪些回收方案,每个方案有哪些优缺点,为什么适合这个区域 讲讲你对垃圾回收机制的理解(问题问的很宽泛,就看你怎么回答和理解)
面试官: 为什么年轻代e,s1,s2是8:1:1
我:xxxx,内存利用率能方面讲
面试官: 工作中有解决过gc问题吗?什么场景下出现的,你如何去排查和解决
1)判断对象是否死亡
1、引用计数算法 (废弃)
对象被引用就+1,难以解决循环引用问题
2、可达性算法(栈、方法区的引用对象)
1)概念:通过一系列称为 “GC roots” 的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC roots没有任何引用链相连时,证明此对象不可用。
2)GC roots的对象包括:
GC roots是啥:堆外指向堆内的引用
1、虚拟机栈中引用的对象(堆中)
2、本地方法栈 native引用的对象
3、方法区中 类静态属性 引用的对象
4、方法区中常量引用的对象
3)引用的分类:
4)当一个对象不可达GC Roots时,这个对象并不会立马被回收,被真正的回收需要经历两次标记:
2)Java垃圾回收时间
3) Stop-the-world 以及安全点
1)标记-清除算法 直接回收不存活的(老年代)
2)复制算法 (新生代)
3)标记 - 整理算法(老年代)
和标记-清除算法类似,在清除对象的时候先将可回收对象移动到一端,然后清除掉端边界以外的对象
优点:1、解决大量内存碎片问题;2、当对象存活率较高时,效率也很好
缺点:压缩算法的性能开销大
4)分代收集算法 根据对象存活周期的不同,将内存空间划分为几块
讲一下新生代、老年代、永久代的区别
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。而新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
新生代中一般保存新出现的对象,所以每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集;
老年代中一般保存存活了很久的对象,他们存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。
永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收
默认情况下,Java 虚拟机采取的是一种动态分配的策略(对应 Java 虚拟机参数 -
),根据生成对象的速率,以及 Survivor 区的使用情况动态调整 Eden 区和 Survivor 区的比例
XX:+UsePSAdaptiveSurvivorSizePolicy
-XX:SurvivorRatio
来固定这个比例Action1: 为什么新生代要分Eden和两个 Survivor 区域?
Action2:Java堆老年代( Old ) 和新生代 ( Young ) 的默认比例?
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio
来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。
其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,Eden 和俩个Survivor 区域比例是 = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio
来设定 ),
但是JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
Action3:JVM的 stop-the-world 机制非常不友好,有哪些解决之道?原理是什么?
Action4:压测时出现频繁的gc容易理解,但是有时出现毛刺是因为什么呢?
Action5:fullgc有卡顿,对性能很不利,怎么避免呢?
Action6:TLAB是什么?有什么作用?
-XX:+UseTLAB
,默认开启)。每个线程可以向 Java 虚拟机申请一段连续的内存,比如 2048 字节,作为线程私有的 TLAB内存分配与回收策略 对象的内存分配
1、大多数情况下,对象在新生代eden区分配,eden区没有足够的空间进行分配时,虚拟机将发起一次minor gc,如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存;
2、大对象(需要大量连续内存空间的java对象,超出eden区大小)直接进入老年代
3、长期存活的对象将进入老年代:(存活轮次最多:15)
-XX:+MaxTenuringThreshold
)时,升到老年代。4、动态对象年龄判断(防止survivor满)
-XX:TargetSurvivorRatio
),年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到maxTenutingThreshold
中要求的年龄。5、空间分配担保机制
1)垃圾收集器就是内存回收的具体实现
2)HotSpot内存回收算法
准确式GC 快速完成GC Roots引用链枚举
引用链枚举时,应用OopMap数据结构,快速完成GC Roots引用链枚举,
OopMap:在类加载完成时,存储 :寄存器和栈的 <偏移量,数据及数据类型>;<k,v>
安全点检测:程序有长时间执行特征 — 方法调用、循环跳转、异常跳转时
1、仅需在安全点记录OopMap信息;若每条指令生成OopMap,则空间成本太大;
2、程序执行时仅在安全点停下来GC
多线程的主动式中断,使得各个线程都跑到安全点再停顿
1、在安全点、创建的对象分配内存时,设置一个标志
2、各个线程执行时主动轮询该标志,若为真,则中断挂起
安全区域检测:代码中,引用关系不发生变化
1、线程没有分配CPU时间,无法跑到安全点
2、GC时可忽略该标识自己处于安全区域的线程
3、要离开安全区域,需要收到系统已完成引用链枚举的信号
HotSpot虚拟机的垃圾回收器
年轻代:serial收集器 parNew parallel scavenge G1
老年代:parallel old serial oid CMS G1
为什么对垃圾收集器分类?
垃圾收集器可分为3类
不同收集器之间的连线表示它们可以搭配使用
新生代的垃圾回收器共有三个:Serial,Parallel Scavenge 和 Parallel New
1、serial收集器 (新生代默认收集器)
2、parNew收集器: 新生代
-XX:ParallelGCThreads
参数来设置线程数3、ParNew Scanvenge收集器 新生代
针对老年代的垃圾回收器也有三个
4个步骤 | 特点 |
---|---|
初始标记 | gc-roots能直接关联的对象 stop the world |
并发标记 | 进行gc tracing的过程 |
重新标记 | 修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录 stop the world |
并发清除 | 基于标定结果,直接清除对象 |
缺点 | 详情及解决方案 |
---|---|
对cpu资源非常敏感 | |
吞吐量低 | 低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不够高。 |
cms收集器无法处理浮动垃圾 | 可能出现concurrent mode failure 而导致另一次full gc的产生,解决方法:虚拟机启动后备预案,临时启用serial old收集器来重新进行老年代的垃圾回收 |
cms基于“标记-清除”,会产生大量内存碎片 | 解决方案:开启内存碎片的合并整理过程 |
G1的特点 | 详情 |
---|---|
1、并发与并行 | G1能充分利用cpu、多核的硬件优势,使用多个cpu (CPU 或者CPU 核心) 来缩短stop-the-world 停顿的时间,部分其他收集器原本需要停顿 java 线程执行的GC动作,G1收集器仍然可以通过并发的方式让 java 程序继续执行。 |
2、空间整合 | cms基于“标记-清除”,会产生大量内存碎片,G1是基于标记-整理的算法(两个region的数据是基于复制的),不会产生内存空间碎片 |
3、可预测的停顿 | g1能建立可预测的停顿时间模型,让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集的时间不超过N毫秒 |
4、分代收集 | 虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆, 但是还是保留了分代的概念 |
实现思路: |
使用G1收集器时,将堆划分为相等的region,并且能和整个堆中任意的对象发生引用关系,优先回收价值大的region。每个Region都有一个Remembered Set,用来记录该Region对象的引用对象所在的Region。通过使用Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
整体上看是“标记-整理”,局部看是“复制”,不会产生内存碎片
总结:
需求 | 使用的收集器 |
---|---|
1.吞吐量优先 | Parallel Scavenge 新生代 复制算法 / parallel Old 老年代 标记-整理 |
2、重视服务响应速度,最短回收停顿时间 | Parallel Scavenge 新生代 复制算法 / CMS 老年代 并发的标记-清除 |
3、面向服务器端应用 | G1收集器 |
初始标记 initial mark
并发标记(Concurrent Mark)
并发预处理(Concurrent Preclean)
可中断的并发预处理(Concurrent Abortable Preclean)
CMSMaxAbortablePrecleanLoops
,默认是0,也就是没有循环次数的限制。CMSMaxAbortablePrecleanTime
,默认是5秒。CMSScheduleRemarkEdenPenetration
,默认为50%。CMSScheduleRemarkEdenSizeThreshold
,默认是2M。最终标记/重新标记(Final Remark)
并发清理(Concurrent Sweep)
并发重置(Concurrent Reset)
使用的标记-清除算法,可能存在大量空间碎片。
// 开启CMS压缩,在FGC时执行压缩,默认为true
-XX:+UseCMSCompactAtFullCollection
// 执行几次FGC才执行压缩,默认为0
-XX:CMSFullGCsBeforeCompaction=0
并发清理可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生
// CMS触发GC的比例
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+CMSInitiatingOccupancyFraction=80 (我们项目为80)
对CPU资源非常敏感。在并发阶段,会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4。
// CMS并发线程数
-XX:ConcGCThreads=X
这是因为 CMS 的写屏障(write barrier)并不是对所有会导致引用变化的字节码生效,例如不支持 astore_X(把栈顶的值存到本地变量表)。
至于为什么不为 astore_X 添加写屏障,R 大认为是栈和年轻代属于数据快速变化的区域,对于这些区域使用写屏障的收益比较差。
不是的。
三色标记算法由 Edsger W. Dijkstra 等人在1978年提出,是一种增量式垃圾回收算法,增量式的意思是慢慢发生变化的意思,也就是 GC 和 mutator(应用程序)一点点交替运行的手法。
三色标记算法顾名思义就是将 GC 中的对象分为三种颜色,这三种颜色和所包含的意思如下:
我们以 GC 标记-清除算法为例简单的说明一下。
GC 开始运行前所有的对象都是白色。GC 一开始运行,所有从根能到达的对象都会被标记为灰色,然后被放到栈里。GC 只是发现了这样的对象,但还没有搜索完它们,所以这些对象就成了灰色对象。
灰色对象会被依次从栈中取出,其子对象也会被涂成灰色。当其所有的子对象都被涂成灰色时,该对象就会被涂成黑色。当 GC 结束时已经不存在灰色对象了,活动对象全部为黑色,垃圾则为白色。
下面是一个三色标记算法的示例动图,大家参考着理解。
明白了三色标记算法后,再回过头去看5.2.3,是不是顿时就明白了。
三色标记算法是增量式垃圾回收算法,mutator可能会随时改变对象引用关系,因此在并发下会存在漏标和错标(多标)。
直接通过一个简单的例子来看:
假设当GC线程执行到时刻1时,此时应用线程先执行了步骤1和2,也就是到了时刻3的场景,GC线程继续执行。
此时对象Z只被黑色对象X所引用,而黑色对象是不会被继续扫描的,因此扫描结束后Z仍然是白色对象,也就是时刻4,此时白色对象Z则会被当做垃圾而回收。
直接通过一个简单的例子来看:
假设当GC线程执行到时刻1时,此时应用线程先执行了步骤1,也就是到了时刻2的场景,GC线程继续执行。
此时对象Z是灰色对象,GC线程对其进行搜索,搜索结束后将其标记为黑色,也就是时刻3,此时对象Z其实没有到GC Roots的引用,理应被回收,但是因为被错误的标记为黑色,而在本次GC中存活了下来。
错标和漏标都是三色标记算法存在的问题,但是两者带来的后果有本质的不同。
通过实验追踪,Wilson 发现,只有当以下两个条件同时满足时才会出现漏标问题:
解决方案:5.2.7 所示的增量更新和起始快照
为了解决三色标记算法的漏标问题,产生了两种比较著名的解决方案:增量更新和起始快照,CMS 和 G1 就是采用了这两种解决方案,
漏标问题的出现必须同时满足上述的两个条件,因此解决办法只需破坏两个条件之一即可。
增量更新(Incremental update)
起始快照(SATB,snapshot at the begin)
使用写屏障拦截所有删除的引用关系,将其记录下来,然后将被删除的引用关系所指向的对象会被当作存活对象(非白色),重新扫描该对象。
SATB 抽象的说就是在一次GC开始时刻是存活的对象,则在本次GC中都会被当做存活对象,此时的对象形成一个逻辑“快照”,这也是起始快照名字的由来。
起始快照破坏的是条件2,当到白色对象的引用断开时,写屏障会记录下该引用,将该对象当作存活对象,后续继续扫描该对象的引用。
以上面的漏标为例,就是拦截步骤2:Y.a=null,将Z作为存活对象,然后重新扫描对象Z。
CMS 的整个垃圾回收过程中只有2个阶段是 stop the world,一个是初始标记,一个是重新标记,初始标记只标记GC Roots直达的对象,因此一般不会耗时太久,而重新标记出现耗时久的现象则比较多见,通常如果CMS GC较慢,大多都是重新标记阶段较慢导致的。
Final Remark 阶段比较慢,比较常见的原因是在并发处理阶段引用关系变化很频繁,导致 dirty card 很多、年轻代对象很多。
比较常见的做法可以在 Final Remark 阶段前进行一次 YGC,这样年轻代的剩余待标记对象会下降很多,被视为GC Root 的对象数量骤减, Final Remark 的工作量就少了很多。
// 在remark之前尝试进行清理,默认值为false
-XX:+CMSScavengeBeforeRemark
通常增加 -XX:+CMSScavengeBeforeRemark
都能解决问题,但是如果优化后还是耗时严重,则需要进一步看具体是哪个小阶段耗时严重。
Final Remark 具体包含了若干个小阶段:weak refs processing、class unloading、scrub string table等,从日志里可以看出来每个小阶段的耗时,根据耗时的阶段再进行针对性的分析,可以查阅源码或者查阅相关资料来帮助分析。
以比较常见的 weak refs processing 为例:
这边的 weak refs 不是单指 WeakReference,而是包括了:SoftReference、WeakReference、FinalReference、PhantomReference、JNI Weak Reference,这边应该是除了强引用外的所有引用都被归类为 weak 了。
因此,我们首先添加以下配置,打印出GC期间引用相关更详细的日志。
// 打印GC的详细信息
-XX:+PrintGCDetails
// 打印在GC期间处理引用对象的时间(仅在PrintGCDetails时启用)
-XX:+PrintReferenceGC
然后根据每个引用的耗时,定位出耗时严重的引用类型,然后查看项目中是否存在对该引用类型不合理的使用。
另外一种比较简单粗暴的办法是可以通过增加引用的并行处理来尝试解决,通常会有不错的效果。
// 启用并行引用处理,默认值为false
-XX:+ParallelRefProcEnabled
而如果是 scrub string table 阶段耗时,则可以分析项目中是否存在不合理的使用 interned string,其他的也类似。
定义:G1(Garbage First),是一种服务端应用使用的垃圾收集器,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量。
G1产生的背景: 大堆JVM问题
特点:
传统垃圾回收器内存模型如下图所示
G1内存模型如下图所示
GC过程
GC前
GC后
扫描过程
普通Young GC
GC步骤
GC LOG
2020-12-02T15:24:13.238-0800: Total time for which application threads were stopped: 0.0043412 seconds, Stopping threads took: 0.0000095 seconds
{Heap before GC invocations=8 (full 0):
garbage-first heap total 102400K, used 74304K [0x00000007b9c00000, 0x00000007b9d00320, 0x00000007c0000000)
region size 1024K, 60 young (61440K), 5 survivors (5120K)
Metaspace used 3166K, capacity 4568K, committed 4864K, reserved 1056768K
class space used 338K, capacity 392K, committed 512K, reserved 1048576K
2020-12-02T15:24:13.864-0800: [GC pause (G1 Evacuation Pause) (young)
Desired survivor size 4194304 bytes, new threshold 15 (max 15)
- age 1: 2048080 bytes, 2048080 total
- age 2: 2048080 bytes, 4096160 total
4.865: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 256, predicted base time: 1.83 ms, remaining time: 198.17 ms, target pause time: 200.00 ms]
4.865: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 55 regions, survivors: 5 regions, predicted young region time: 46.69 ms]
4.865: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 55 regions, survivors: 5 regions, old: 0 regions, predicted pause time: 48.52 ms, target pause time: 200.00 ms]
, 0.0045289 secs]
[Parallel Time: 4.2 ms, GC Workers: 2]
[GC Worker Start (ms): Min: 4865.2, Avg: 4865.2, Max: 4865.2, Diff: 0.0]
// 扫描gc Roots对象
[Ext Root Scanning (ms): Min: 0.2, Avg: 0.2, Max: 0.2, Diff: 0.0, Sum: 0.3]
// 更新RSet,Rset是通过写屏障和缓冲区实现的,更新Rset,确保此时Rset是最新的。更新完后将缓冲区处理掉
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
[Processed Buffers: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 2]
// 扫描Rset
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
// 扫描局部对象引用
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
// 赋值对象
[Object Copy (ms): Min: 3.4, Avg: 3.6, Max: 3.8, Diff: 0.3, Sum: 7.2]
// 线程窃取算法,每个线程完成任务后会尝试帮其他线程完成剩余的任务
[Termination (ms): Min: 0.1, Avg: 0.3, Max: 0.5, Diff: 0.4, Sum: 0.6]
[Termination Attempts: Min: 77
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_28959087/article/details/86665793
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!