一、如何判断对象可以回收
1.引用计数法(reference-counting)
每个对象有一个引用计数器,当对象被引用一次则计数器加1,当对象引用失效一次则计数器减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。
缺点:若两个对象相互引用,他们的count都不会是0,所以一直不能被垃圾回收。
2.可达性分析算法(GC Roots Tracing)
扫描堆中的对象,从GC Roots作为起点开始搜索,那么整个连通图中的对象便都是活对象,对于GC Roots无法到达的对象便成了垃圾回收的对象,随时可被GC回收。
可以作为GC Roots的对象:
- 虚拟机栈的栈帧的局部变量表所引用的对象;
- 本地方法栈的JNI所引用的对象;
- 方法区的静态变量和常量所引用的对象;
3.四种引用
强引用
- 只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。
软引用(SoftReference)
- 仅有软引用引用该对象时,在垃圾回收后,内存不足时会再次出发垃圾回收,回收软引用对象
- 可以配合引用对象来释放软引用自身
弱引用(WeakReference)
- 仅有弱引用引用该对象时,在垃圾回收时,无论空间是否足够,都会回收弱引用对
- 可以配合引用队列来释放弱引用自身
虚引用(PhantomReference)
- 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存。
终结器引用(FinalReference)
- 无需手动编码,其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalier线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC 时才能被回收引用的对象。
二、垃圾回收算法
1.标记-清除算法
算法分为“标记”和清除阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
- 速度快
- 会造成内存碎片
2.复制算法
将内存分为大小相同的两块,每次使用其中的一块,当这一块的内存使用完毕后,就将还存活的对象复制到另一块中去,然后把使用的空间一次清理掉。
- 不会有内存碎片
- 需要占用双倍内存空间
3.标记-整理算法
根据老年代的特点特别出的一种标记算法,标记过程和“标记-清除”算法一样,但是后续步骤不是直接对可回收的对象进行垃圾回收,而是让所有存活的对象向另一端移动,然后直接清理掉端边界以外的内存。
- 速度慢
- 没有内存碎片
三、分代垃圾回收
- 对象首先在Eden区分配
- 新生代空间不足时,触发Minor GC,Eden区和Survivor From区存活的对象使用复制算法到Survivor To区,存活的对象年龄加1,并且交换From和To。
- Minor GC会引发Stop The World机制,暂停其他用户线程,等垃圾回收结束以后,用户线程才恢复运行。
- 当对象寿命超过阀值时,会移动到老年代,最大寿命是15次(4bit)。
- 当老年代空间不足,会先尝试触发Minor GC,如果之后空间仍不足,那么触发Full GC,STW的时间更长。
相关JVM参数
含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size)
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阀值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
Full GC 前Minor GC -XX:+ScavengeBeforeFullGC
-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
四、垃圾回收器
1.串行
- 单线程
- 堆内存较小,适合个人电脑
-XX:+UseSerialGC=Serial+SerialOld (Serial:复制算法,SerialOld:标记-整理算法)
2.吞吐量优先
- 多线程
- 堆内存较大,多核CPU
- 让单位时间内,STW的时间最短
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC (复制 标记-整理)
-XX:UseAdaptiveSizePolicy (自适应的调整策略,调整新生代大小)
-XX:GCTimeRatio=ratio 计算公式:1 / (1+ratio)
-XX:MaxGCPauseMillis=ms 最大暂停时间默认200ms
-XX:ParallelGCThreads=n (并发线程数)
3.响应时间优先
- 多线程
- 堆内存较大,多核CPU
- 尽可能让单次STW的时间最短
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n(初始标记的并发线程数) ~ -XX:ConcGCThreads=threads(并发标记的线程数)
-XX:CMSInitiatingOccupancyFraction=percent(何时进行CMS垃圾回收的时机)
-XX:+CMSScavengeBeforeRemark(在重新标记之前做一次垃圾回收)
在内存碎片比较多的情况下,(标记-清除算法会产生内存碎片),可能造成将来,分配对象时,MinorGC不足,老年代由于碎片过多也不足,这样会造成并发失败,这时候CMS就不能正常工作了,会退化为SerialOld,做一次单线程串行的垃圾回收,这样垃圾回收的时间就会变的很长。
G1(Garbage First)垃圾回收器
适应场景
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的Region
- 整体上是标记-整理算法,两个区域之间是复制算法
相关JVM参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
G1垃圾回收阶段
Young Collection
- 会STW
Young Collection +CM
- 在Young GC时会进行GC Root的初始标记
- 老年代占用堆空间比例达到阀值时,进行并发标记(不会STW),由下面的JVM参数决定
- -XX:InitiatingHeapOccupancyPercent=percent(默认是45%)
Mixed Collection
会对E、S、O进行全面垃圾回收
- 最终拷贝(Remark)会STW
- 拷贝存活(Evacuation)会STW
-XX:MaxGCPauseMillis=ms
JDK 8u60回收巨型对象
- 一个对象大于region的一半时,称之为巨型对象
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所以incoming引用,这样老年代incoming引用为0的巨型对象可以在新生代垃圾回收时处理掉。
五、垃圾回收调优
查看虚拟机运行参数
"C:\Program Files\Java\jdk1.8.0_271\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"
1.调优领域
- 内存
- 锁竞争
- CPU占用
- IO
2.确定目标
- 【低延迟】还是【高吞吐量】,选择合适的回收器
- 【低延迟】CMS,G1,ZGC
- 【高吞吐量】ParallelGC
3.最快的GC是不发生GC
- 查看FullGC前后的内存占用,考虑下面几个问题
- 数据量是不是太多了?
- resultSet = statement.executeQuery("select * from 大表 limit n");
- 数据表示是否臃肿?
- 对象图
- 对象大小 16bit Integer24bit int 4bit
- 是否存在内存泄漏?
- static Map map =
- 软
- 弱
- 第三方缓存实现
- 数据量是不是太多了?
4.新生代调优
- 新生代的特点
- 所有的new操作的内存分配非常廉价
- TLAB (thread-local allocation buffer):让每个线程用自己私有的内存进行对象分配
- 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC的时间远远低于Full GC
- 所有的new操作的内存分配非常廉价
- 越大越好吗?
- -Xmn 设置新生代初始和最大值
- 当新生代内存越大时,老年代内存就会越小,会频繁发生Full GC,会占用更长的时间
- 建议新生代内存占整个堆内存:大于25%小于50%
- 复制算法中:标记耗费时间短,复制耗费时间长
- 新生代能容纳所以【并发量*(请求-响应)】的数据
- 1000个用户,请求-响应的数据是512K,则总共需要512M的内存
- 幸存区大到能够保留【当前活跃对象+需要晋升对象】,若到老年代,在FullGC发生时间需要再老年代内存满了才发生,所以尽可能在Minor GC时就回收。
- 晋升阀值配置得当,让长时间存放对象加快晋升
- -XX:MaxTenuringThreshold=threshold
- -XX:+PrintTenuingDistribution
5.老年代代调优
以CMS为例
- CMS的老年代内存越大越好
- 先尝试不做调优,如果没有Full GC那么已经...,否则先尝试调优新生代
- 观察发生Full GC时老年代内存占用,将老年代的内存预设调大1/4 ~ 1/3
- -XX:CMSInitiatingOccupancyFraction=precent (内存占用老年代总内存多少的是否使用CMS进行垃圾回收)
6.案例
案例1:Full GC和Minor GC频繁
- 先增大新生代内存,增大幸存区空间
- 让生命周期较短的对象尽可能的留在新生代,不要晋升到老年代,这样Full GC就不会频繁出现
案例2:请求高峰期发生Full GC,单次暂停时间特别长(CMS)
- 查看CMS日志,看哪个阶段耗费时间比较长,通过日志可以发现重新标记耗费时间长,会扫描整个堆内存,若新生代对象比较多,则耗费时间比较长;
- 在重新标记之前做一次垃圾回收,-XX:+CMSScavengeBeforeRemark(在重新标记之前做一次垃圾回收),可以将2s左右降低到20ms左右
案例3:老年代充裕情况下,发生Full GC(环境:CMS jdk1.7)
- 1.8有一个元空间作为方法区的实现,1.7使用的是永久代,而永久代的空间不足也会导致Full GC
- 1.8以后,使用的操作系统的内存,空间比较充裕
- 结果:方法区的空间不足,调大方法区的内存。