1 GC垃圾回收
1.1 如何判断对象可以回收
1.1.1 引用计数法
当一个对象的引用计数为0时,则可以被判断为垃圾进行回收。
但存在有循环引用的现象(早期python使用):
1.1.2 可达性分析算法
java虚拟机 垃圾回收器所用
在垃圾回收前,会对堆内存中的所有对象进行扫描,查看对象是否被根对象直接或间接引用,如果是则不回收,反之回收
1.1.3 四种引用
1、强引用
当存在强引用时,无法垃圾回收,当对象的强引用都去除后,才可作为垃圾被回收
2、软引用
当垃圾回收后发现内存仍然不够时,软引用会被回收(此时没有强引用)
3、弱引用
当没有强引用时,不管是否内存冲突,只要垃圾回收,弱引用均会被回收
============================================
如果软引用 或 弱引用 在创建时分配了引用队列,在其引用对象被回收时,软引用(弱引用)本身的对象会进入引用队列 。当想要释放软引用、弱引用自身占用的内存,需要使用引用队列
4、虚引用
直接内存地址 所需
在虚引用 引用的对象ByteBuffer被垃圾回收后,它分配的直接内存不能由java垃圾回收管理,此时虚引用对象进入引用队列,ReferenceHandler线程定时监听引用队列是否有新的虚引用对象进入(如Cleaner),有则调用Cleaner的clean方法,调用其中的Unsafe.freeMemory方法去释放直接内存
5、终结器引用
在第一次垃圾回收时,将终结器引用对象入引用队列,由一个优先级很低的线程FinallizeHandler去查看队列中是否有终结器引用,有则根据终结器引用找到要作为垃圾回收的对象,调用其finalize( )方法,在下一次垃圾回收时释放内存
总结
1.2 垃圾回收算法
1.2.1 标记清除
两个阶段,先标记,后清除
1.2.2 标记整理
在第二阶段会进行整理
1.2.3 复制
将内存区划成大小相等的两个内存区(FROM、TO),先标记垃圾,再从FROM区中存活的对象复制到TO区,伴随着整理过程,然后清空FROM区,再将两个区域调换使TO始终保持空闲
1.3 分代回收
根据对象生命周期的不同特点,进行不同的垃圾回收策略,老年代的垃圾回收频率低,新生代的垃圾回收频率高
· 对象首选分配在伊甸园区
· 新生代空间不足时,触发 minor gc,伊甸园和from存活的对象使用copy复制到to中,存活对象年龄+1并交换from to
· minor gc会引发stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
· 当对象寿命超过阈值时,,会晋升至老年代,最大寿命是15(4bit )
· 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,则触发full gc
GC 相关vm参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringDistribution |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
!!!!
当一个子线程内 内存溢出时,不会影响主线程的执行
1.4 垃圾回收器
类别:
1、串行
· 单线程
· 堆内存较小,适合个人电脑
2、吞吐量优先
· 多线程
· 堆内存较大,多核 cpu
· 让单位时间内,stw的时间最短
3、响应时间优先
· 多线程
· 堆内存较大,多核 cpu
· 尽可能让单次stw(垃圾回收时需暂停线程)时间最短
1.4.1 串行(SerialGC)
-XX:+UseSerialGC = Serial + SerialOld
1.4.2 吞吐量优先(ParallelGC)
-XX:+UseParallelGC ~ -XX:UseParallelOldGC
-XX:+UseAdaptiveSizePolicy (采用自适应大小调整策略)
-XX:GCTimeRatio=ratio (1/ 1+ratio,)
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n (控制线程数)
1.4.3 响应时间优先(CMS)
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent (当老年代的内存占用达到percent,执行垃圾回收,为了预留空间给浮动垃圾)
-XX:+CMSScavengeBeforeRemark
1.4.4 G1
Garbage First,JDK 9 默认
适用场景:
· 同时注重吞吐量(Throughput)和低延迟(Low latency),默认暂停目标是200ms
· 超大堆内存,会将堆划分为多个大小相等的Region
· 整体上是标记+整理算法,两个区域之间是复制算法
相关JVM参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
垃圾回收阶段
Oracle工程师JVM版本:
Young Collection
· 会stw
首先G1将堆内存划分成大小相等的区域(均可作为伊甸园、幸存区、老年代),当区域被占满就会触发一次新生代的垃圾回收
新生代垃圾回收会以复制的算法将幸存对象放入幸存区
当幸存区的对象占满或存活时间超过阈值时,触发垃圾回收,幸存区的对象会晋升至老年代,存活时间未达到阈值的会再次复制到另一个幸存区,同时新生代的幸存对象也会复制到幸存区
Young Collection + CM
· 在Young GC时会进行GC Root的初始标记
· 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由JVM参数决定:
-XX:InitiatingHeapOccupancyPercent=percent(默认45%)
Mixed Collection
会对伊甸园、幸存区、老年代进行全面垃圾回收
· 最终标记(Remark)会STW
· 拷贝存活(Evacuation)会STW
-XX:MaxGCPauseMillis=ms
G1会根据最大暂停时间进行有选择的回收,优先挑选回收价值最高(垃圾最多)的老年代区域进行回收
Full GC
· SerialGC
· 新生代内存不足发生 minor gc
· 老年代内存不足发生 full gc
· ParallelGC
· 新生代内存不足发生 minor gc
· 老年代内存不足发生 full gc
· CMS
· 新生代内存不足发生 minor gc
· 老年代内存不足发生 并发标记速度赶不上新对象分配速度就可能会提前引发串行的回收阶段,即FULL GC
· G1
· 新生代内存不足发生 minor gc
· 老年代内存不足发生 并发标记速度赶不上新对象分配速度就可能会提前引发串行的回收阶段,即FULL GC
Young Collection 跨代引用
老年代引用新生代 问题
· 通过Remembered Set知道卡表中的脏卡,在脏卡中遍历GC root
· 在每次对象引用发生变更时,通过写屏障 post-write barrier +dirty card queue更新脏卡(由脏卡的队列进行异步操作)
· concurrent refinement threads 更新 Remembered Set
卡表、记忆集
记忆集是一种抽象概念,用于在实现部分垃圾收集(Partial GC)时用于记录从非收集区域指向收集区域的指针集合,卡表是记忆集的一种实现方式。
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,成这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把他们加入GC Roots中一并扫描。
写屏障
在HotSpot虚拟机里是通过写屏障(Write Barrier)技术去维护卡表状态的。这与解决并发乱序执行问题的“内存屏障”有所不同。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动手做的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值前后都在写屏障的覆盖范畴之内。在赋值前的部分写屏障叫做写前屏障(Pre-Write-Barrier),在赋值后的则叫作写后屏障(Post-Write-Barrier)。HotSpot虚拟机的许多收集器中都有使用到写屏障,但直到G1收集器出现,其他收集器都只用到了写后屏障。
Remark
CMS、G1都包含重新标记阶段
· pre-write barrier + satb_mark_queue
并发标记阶段,对象引用更改的,jvm会加入写屏障,写屏障指令会将当前这个对象加入到一个队列(satb_mark_queue)中
并发标记结束后,重新标记阶段会先STW,然后重新标记线程会从队列中将对象取出,检查,如果还有强引用存在,则标记为黑色,防止被误当成垃圾回收
有引用的最后都会变成黑色,未引用的最终会被回收
JDK 8u20 字符串去重
-XX:+UseStringDeduplication
· 将所有新分配的字符串放入一个队列
· 当新生代回收时,G1并发检查是否有字符串重复
· 如果值一样,则让它们引用同一个char[ ]
· 与String.intern()不一样
· String.intern()关注的是字符串对象
· 字符串去重关注的是char[ ]
· JVM内部使用不同的字符串表
JDK 8u40 并发标记类卸载
所有对象都经过并发标记后,就知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX:+ClassUnloadingWithConcurrrentMark 默认启用
JDK 8u60 回收巨型对象
· 一个对象大于region一半时,称为巨型对象
· G1不会对巨型对象进行拷贝
· 回收时被优先考虑
· G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
JDK9 并发标记起始时间调整
· 并发标记必须在堆空间占满前完成,否则退化为FullGC
· JDK9 之前需使用 -XX:InitiatingHeapOccupancyPercent
· JDK9 可动态调整
· -XX:InitiatingHeapOccupancyPercent 用来设置初始值
· 进行数据采样并动态调整
· 总会添加一个安全的空档空间
1.4.5 垃圾回收调优
新生代调优
· 新生代特点:
· 所有new操作的内存分配非常廉价
· TLAB thread-local allocation buffer
· 死亡对象的回收代价是0
· 大部分对象用过即死
· Minor GC的时间远远低于 Full GC
新生代能容纳所有【并发量*(请求-响应)】的数据
幸存区大到能保留【当前活跃对象+需要晋升对象】
晋升阈值配置得当,让长时间存活的对象尽快晋升
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistribution
老年代调优
举例CMS:
· CMS的老年代内存越大越好
· 先尝试不调优,如果没有Full GC,则说明老年代空间充裕,否则先尝试新生代调优
· 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3
· -XX:CMSInitiatingOccupancyFraction=percent