垃圾收集 Garbage Collection GC
优化的一个非常重要的地方
如何判定对象为垃圾对象?
- 引用计数法
- 可达性分析法
如何回收?
- 回收的策略
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法
- 常见的回收器
- Serial
- Parnew
- Cms
- G1
何时回收?
对象已死吗
引用计数法
Reference Counting
在
对象中添加一个
引用计数器,当有地址引用这个对象的时候,引用计数器的值就+1,当引用失效的时候,计数器的值-1。
缺点:很难解决对象之间相互循环引用的问题
-verbose:gc 打印垃圾回收的简单信息
-xx:+PrintGCDetail 打印垃圾回收的详细信息
可达性分析法
Reachability Analysis
引用链
作为GCRoot的对象
- 虚拟机栈,栈帧中的局部变量表引用的对象
- 方法区的类属性所引用的对象
- 方法区中常量所引用的对象
- 本地方法栈中(Native方法)引用的对象
引用
强引用
软引用:还有用但并非必需的对象,在系统将要发生内存溢出异常之前回收
弱引用:非必需对象,只生存到下一次垃圾收集发生之前
虚引用:相当于没有,唯一目的是在对象被回收时收到一个系统通知
要真正宣告一个对象死亡,至少要经历两次标记过程。
finalize()方法,尽量避免使用,因为运行代价高,不确定性大。
任何一个对象的finalize()方法只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。
回收方法区
方法区(HotSpot虚拟机中的永久代)垃圾收集主要回收两部分内容:
废弃常量和
无用的类。
回收废弃常量与回收Java堆中的对象非常类似。
类需要
同时满足下面3个条件才能算是“
无用的类”:
- 该类所有的实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
3.3 垃圾收集算法
3.3.1 标记-清除算法
mark-sweep
两个不足:
- 效率问题:标记和清除两个过程效率都不高
- 空间问题:内存碎片化,导致需要较大空间时空间不够而触发另一次垃圾收集动作
3.3.2 复制算法
为了解决标记-清除算法的效率问题,我感觉也解决了碎片问题。
代价:内存缩小为原来的一半
现在用来回收新生代
因为新生代中对象98%都是朝生暮死,所以不需要1:1划分。
划分为一块Eden,两块Survivor。
每次将Eden和其中一块Survivor中的存活的对象复制到另一块Survivor空间。
Eden:Survivor = 8:1
当Survivor空间不够时,需要依赖其他内存(老年代)进行分配担保Handle Promotion
堆
- 新生代
- Eden 伊甸园 ,区域非常大
- Survivor 存活区 有两块
- Tenured Gen
- 老年代
存活率较高的老年代不能使用这种算法。
标记-整理算法
Mark-Compact
标记-整理-清除
针对
老年代的特点提出。
分代收集算法
标记-整理和复制算法结合
只是根据对象存活周期的不同将内存划分为几块。
根据各个年代的特点采用最适当的收集算法。
内存回收率高的新生代用复制算法
回收率低的老年代用标记-整理算法或标记-清除算法
HotSpot算法实现
虚拟机通过OopMap的数据结构来直接得知哪些地方存放着对象的引用。
安全点 Safepoint
安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。
“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生
Safepoint。
如何在GC发生时让所有线程都跑到最近的安全点上再停顿下来?
抢先式中断:几乎没有采用
主动式中断:设置标志,线程主动轮询这个标志,发现为真时挂起。
安全区域:是指在一段代码片段中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。
垃圾收集器
不同收集器适用的场景不同
直到现在为止还没有最好的收集器出现,更加没有万能的收集器。
serial收集器
最基本,发展最悠久的
单线程的垃圾收集器:垃圾收集的时候,其他线程都要暂停
用在
Client
应用的默认
新生代收集器,简单而高效,在单个cpu环境下高效。
ParNew收集器
serial收集器的多线程版本
是许多运行在
Server模式下的首选
新生代收集器。
能与CMS收集器配合工作的只有:serial 和 ParNew
Parallel Scavenge 收集器
复制算法(
新生代收集器)
多线程收集器
关注点是达到可控制的
吞吐量
吞吐量=(运行用户代码时间)/(运行用户代码时间+垃圾收集时间)
高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,适合
后台而不需要太多交互的任务。
-XX:MaxGCPauseMillis 最大停顿时间
-XX:GCTimeRatio 设置吞吐量大小
-XX:+UseAdaptiveSizePolicy 可以设置自适应调节策略
Serial Old 收集器
Serial的老年代版本
标记-整理
Parallel Old 收集器
Parallel Scavenge收集器的老年代版本
标记-整理
CMS收集器
concurrent mark sweep
标记-清除算法
用在老年代
以获取
最短回收停顿时间为目标的收集器。
服务器端重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
工作过程:
- 初始标记:仅仅标记一下GC Roots能直接关联到的对象
- 并发标记:GC Roots Tracing
- 重新标记:修正并发标记期间因用户程序继续运行而导致标记变化的那一部分标记记录
- 并发清除:
初始标记和重新标记需要stop the world
优点:
并发收集
低停顿
缺点:
- 对CPU资源非常敏感,但cpu数量少时占用大量的CPU资源
- 无法处理浮动垃圾,需要预留一部分空间提供并发收集时的程序运行使用,不够时会出现ConcurrentModeFailure
- 空间碎片
G1收集器
Garbage-First
当今收集器技术发展的最前沿成果之一。
面向服务器端
优势:
- 并行与并发
- 分代收集:和传统分代有区别
- 空间整合:从整体上看是基于“标记-整理”,从局部(两个region之间)上看基于“复制”
- 可预测的停顿:能让使用者明确指出在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
使用Region划分内存空间
,
有优先级的区域回收方式
,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
如果存在Region间的引用,虚拟机使用
Remembered Set来避免全堆扫描。
当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试的选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。
理解GC日志
垃圾收集器参数总结
内存分配与回收策略
java自动内存管理,归结为自动化地解决了两个问题:
给对象分配内存
回收分配的内存
几种最普遍的内存分配规则
优先分配到Eden
当eden区没有祖国空间进行分配时,虚拟机发起一次
Minor GC。
打印内存回收日志:-XX:+PrintGCDetails
Minor GC 和 Full GC?
新生代GC Minor GC:发生在新生代的垃圾收集动作,因为Java对象大多数具备朝生暮死的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC Major GC/ Full GC:发生在老年代的GC,出现了Full GC, 一般会伴随至少一次的Minor GC(但非绝对)。Full GC的速度一般会比Minor GC 慢10倍以上。
大对象直接分配到老年代
-XX:PretenureSizeThreshold 大于这个值的直接在老年代中分配。
避免在Eden区及两个Survivor区之间发生大量的内存复制。
长期存活的对象分配到老年代
虚拟机给每个对象定义一个对象年龄Age计数器,每经过一次Minor GC就加1
-XX:MaxTenuringThreshold
动态对象年龄判断
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold 才能到老年代。如果在survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
空间分配担保
-XX:+HandlePromotionFailure +开启 -禁用
JDK6 Update24后,HandlePromotionFailure 没有用了。
规则变为:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
逃逸分析与栈上分配
逃逸分析:分析对象的作用域