一、如何判断对象可以可以被回收
1.1 引用计数法
定义:只要一个对象被变量所引用,则该对象计数就+1,若被引用了两次,则它的引用计数就变为2,如果某一个变量不再引用它了,则它的引用计数就减一,当该对象的引用变为0的时候就表示没有变量引用它了,该对象就可以被当作垃圾回收了。
弊端:当两个对象循环引用时候,但是又没有被别的变量引用,并且这两个对象不在有实用价值,这时,垃圾回收并不能够回收这两个对象,可能导致內存溢出。
1.2 可达性分析算法
根对象:那些肯定不能被当成垃圾回收的对象
定义:在垃圾回收之前,先对堆中的所有对象进行一次扫描,判断每一个对象是否被根对象直接或者间接的引用,如果是,则不能被回收,反之则可以被当成垃圾回收
Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
扫描堆中的对象,看是否能够沿着GC Root对象(一系列对象)为起点的引用链找到该对象,找不到,表示可以回收
哪些对象可以作为GC Root ?
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。.
方法区中常量引用的对象。
本地方法栈中JNI(即-般说的Native方法)引用的对象。
1.3 四种引用
1.强引用
只有所有GC Roots对象都不通过[强引用]引用该对象,该对象才能被垃圾回收
2.软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
可以配合引用队列来释放软引用自身
3.弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
可以配合引用队列来释放弱引用自身
4.虚引用(PhantomReference)
必须配合引用队列使用,主要配合ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存
5.终结器引用(FinalReference)
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收), 再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时
才能回收被引用对象
二、垃圾回收算法
2.1 标记清除
特点:速度较快、会造成內存碎片
2.2 标记整理
特点:速度慢、没有內存碎片
2.3 复制
特点:没有內存碎片、需要占用双倍内存空间
三、分代垃圾回收
3.1 分代垃圾回收机制
对象首先分配在伊甸园区域
新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄加1并且交换from to
minor gc会引发stop the world, 暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
当对象寿命超过阈值时,会晋升至老年代,最大寿命是15 (4bit)
当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc, stw的时间更长
3.2 相关VM参数
四、垃圾回收器
4.1串行
单线程
堆内存较小,适合个人电脑
4.2吞吐量优先
多线程
堆内存较大,多核cpu
让单位时间内,STW的时间最短0.2 0.2=0.4
4.3响应时间优先
多线程
堆内存较大,多核cpu
尽可能让单次STW的时间最短0.1 0.10.1 0.10.1=0.5
4.4G1
适用场景
同时注重吞吐量(Throughput) 和低延迟(Low latency) ,默认的暂停目标是200 ms
超大堆内存,会将堆划分为多个大小相等的Region
整体上是标记+整理算法,两个区域之间是复制算法
相关JVM参数
-XX:UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
G1垃圾回收阶段
1)Young Collection
2)Young Collection + CM
在Young GC时会进行GC Root的初始标记
老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
3)Mixed Collection
会对E、S、O进行全面垃圾回收
最终标记(Remark) 会STW
拷贝存活(Evacuation) 会STW
- XX:MaxGCPauseMillis=ms
FullGC
■SerialGC
新生代内存不足发生的垃圾收集- minor gc
老年代内存不足发生的垃圾收集- full gc
■ParallelGC
新生代内存不足发生的垃圾收集- minor gc
老年代内存不足发生的垃圾收集- full gc
■CMS
新生代内存不足发生的垃圾收集- minor gc
老年代内存不足:并发失败以后,才叫Full GC,否则不会触发Full GC
■G1
新生代内存不足发生的垃圾收集- minor gc
老年代内存不足:当老年代內存跟堆內存占比达到45%以上,会触发并发标记的阶段,以及后续混合收集的阶段,如果垃圾回收的速度比新产生的垃圾的速度要快,来的及打扫,这是还不叫Full GC,还是并发垃圾回收的阶段(也会有暂停,但是时间很短), 当垃圾回收的速度跟不上垃圾产生的速度,并发收集就会失败,转化为Full GC(并发进行),stw时间也会更长。
Young Collection 跨代引用
卡表与Remembered Set
在引用变更时通过post-write barrier + dirty card queue
concurrent refinement threads更新Remembered Set
Remark
JDK 8u20 字符串去重
优点:节省大量内存
缺点:略微多占用了cpu时间,新生代回收时间略微增加
-XX: +UseStringDeduplication 默认开启
将所有新分配的字符串放入一个队列
当新生代回收时, G1并发检查是否有字符串重复
如果它们值一样,让它们引用同-一个char[]
注意,与String. intern()不- -样
String. intern()关注的是字符串对象
而字符串去重关注的是char[]
在JVM内部,使用了不同的字符串表
JDK 8u40 并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它
所加载的所有类
-XX:+ClassUnloadingWi thConcurrentMark默认启用
JDK 8u60 回收巨型对象
一个对象大于region的一半时,称之为巨型对象
G1不会对巨型对象进行拷贝
回收时被优先考虑
G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
JDK 9 并发标记起始时间的调整
并发标记必须在堆空间占满前完成,否则退化为FullGC
JDK9之前需要使用-XX:InitiatingHeap0ccupancyPercent
JDK 9可以动态调整
-XX: Initiat ingHeapOccupancyPercent用来设置初始值
进行数据采样并动态调整
总会添加一个安全的空档空间
JDK 9 更高效的回收
250+增强
180+bug修复
五、垃圾回收调优
预备知识
掌握GC相关的VM参数,会基本的空间调整
掌握相关工具
明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则
5.1 调优领域
■内存
■锁竞争
■cpu占用
■io
5.2 确定目标
[低延迟]还是[高吞吐量],选择合适的回收器
CMS,G1(JDK9,在更大的內存下工作的比CMS要好),ZGC (低延迟)
ParallelGC (高吞吐量)
Zing(stw 0停顿、可管理超大的內存)
互联网项目主要是针对 低延迟
5.3 最快的GC是不发生GC
查看FullGC前后的内存占用,考虑下面几个问题
数据是不是太多?
resultSet = statement.executeQuery(“select * from大表limit n”)
数据表示是否太臃肿?
对象图
对象大小16 Integer 24 int 4
是否存在内存泄漏?
static Map map =
软
弱
第三方缓存实现
5.4 新生代调优
新生代的特点
所有的new操作的内存分配非常廉价
TLABlthread-local allocation buffer
死亡对象的回收代价是零
大部分对象用过即死
Minor GC的时间远远低于Full GC
新生代能容纳所有[并发量* (请求-响应)]的数据
幸存区大到能保留[当前活跃对象+需要晋升对象]
升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistribution
5.5 老年代调优
以CMS为例
■CMS的老年代内存越大越好
■先尝试不做调优,如果没有Full GC那么已经…否则先尝试调优新生代
■观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~ 1/3
■-XX: CMSInitiatingOccupancyFraction=percent
5.6 调优案例
案例1 Full GC和Minor GC频繁
分析:GC频繁说明空间紧张,究竟是哪一空间紧张呢,如果是新生代,当业务高峰期来了,大量的对象被创建,新生代的空间很快就满了,幸存区的空间紧张,那么它的最大晋升阈值就会降低,导致很多本来生存周期很短的对象,也会晋升到老年代去,这样情况就进一步恶化,这样就导致老年代的Full GC频繁发生。
解决:通过检测工具去观察堆空间的大小,发现新生代的內存设置的太小了,先试着增大新生代的內存,新生代的內存充裕了之后,新生代的垃圾回收就变得不那么频繁了,同时增大了幸存区的空间,以及晋升阈值,这样就能够使得生命周期较短的对象尽可能的留在新生代里,这样就可以让老年代的FullGC也不那么频繁了。
案例2 请求高峰期发生Full GC,单次暂停时间特别长(CMS)
分析:已确定垃圾回收器是CMS,先去查看GC日志,看看CMS哪个阶段耗时比较长,CMS在重新标记的时候要扫描整个堆内存,如果业务高峰期的时候,新生代的对象较多标记时间会变的很长。
解决:在重新标记之前对新生代先做一次垃圾回收,减少新生代对象的数量,这样就可以减少在重新标记时所耗费的时间。 – XX:+CMSScavengeBeforeRemark
案例3 老年代充裕情况下发生Full GC(JDK1.7 CMS)
分析:之前介绍过CMS可能由于空间不足,导致并发失败,或者是空间碎片比较多,会产生Full GC;若日志排查后,在GC日志里没有并发失败,碎片过多的错入提示,说明老年代的空间是充裕的,不是老年代空间不足产生的Full GC。从案例中可知项目所用的JDK为1.7,JDK1.8是元空间作为方法区的实现,JDK1.7及以前是永久代作为方法区的实现,JDK1.7以前的永久代空间不足也会导致Full GC,JDK1.8以后,元空间不再由JAVA控制,元空间的默认情况下他的內存空间是使用了操作系统的內存空间,空间的容量一般是比较充裕的。而JDK1.7以前永久代的空间如果设小了,就会导致触发整个堆的Full GC。
解决:增大永久代的最大值和初始值。