一. 可达性分析与引用
1. 可达性分析
可达性分析
是一种判定对象是否存活的方法.算法的基本思想是: 通过一些列成为GC Roots
的对象作为起点, 从这些节点开始向下搜索, 搜索走过的路径称为引用链
. 如果一个对象不能通过GC Root节点的引用链达到, 则证明此对象不可用.可作为GC Roots
的对象有以下几种:
- 虚拟机栈中引用的对象(Java方法内的局部变量)
- 方法区(永久代)中静态属性引用的对象(类的静态变量)
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
2, 引用
- 强引用
- 软引用
软引用关联的对象, 将在系统发生OOM之前进行回收 - 弱引用
弱引用比软引用更弱一点. 被弱引用关联的对象, 只要系统发生GC就会被回收 - 虚引用
最弱的一种引用, 它完全不会对对象的生存周期构成影响, 也无法通过引用获得对象实例. 其唯一目的就是在被引用的对象被GC时收到一个系统通知
常用的是强引用和软引用
二. 垃圾回收器
1. CMS回收器
-
CMS回收器的设计目标是什么
CMS(Concurrent Mark Sweep
)收集器是以最短回收停顿时间为目标的收集器. 适用于作为服务端程序的回收器选择, 因为服务端程序尤其重视响应速度, 希望系统停顿时间最短, 从而给用户带来良好体验 -
CMS回收器的回收步骤
从名字上就可以看出, CMS回收器基于"先标记, 后清除"的算法实现, 整个过程可分为4个步骤:- 初始标记(
Stop The World
)
仅仅是标记一下GC Root能直接关联到的对象, 速度很快 - 并发标记
对初始标记到的对象, 进行GC Root Tracing. - 重新标记(
Stop The World
)
修正并发标记期间因用户程序继续执行导致标记产生变动的那部分对象的标记记录
该阶段的停顿时间一般比初始标记阶段稍长一些, 但远比并发标记的tracing时间短 - 并发清除
- 初始标记(
整个过程中, 最耗时的并发标记
和并发删除
过程, GC收集线程和用户线程一起工作, 因此达到了短停顿时间的效果
- CMS远达不到完美的程度, 有3个明显缺点
-
- CMS收集器对CPU资源非常敏感:
- 并发的两个各阶段下, 虽然CMS收集器可以和用户线程并行, 因为会占用一部分线程资源获得时间片, 导致引用程序变慢, 总吞吐量降低
- CMS默认启动的线程数是
(CPU_num+3)/4
. 因此:- 当CPU个数在4个以上时, CMS并发回收时GC线程个数不少于CUP个数的25%(至少抢占了1/4的CPU资源). 随着PU数量的增加这个比例会降低
- 当CPU个数不足4个时, CMS对程序的影响就会很大. 比如CPU只有2个, CMS回收线程就会1个, 占用了50%的CPU资源. 这可能会导致并发阶段, 用户程序的执行速度突然降低50%
-
-
CMS无法处理浮动垃圾
浮动垃圾是指: 在并发清除阶段, 因为用户线程也在并发执行而产生的新垃圾, 这些新垃圾由于没有经过3个标记阶段(初始标记, 并发标记, 重新标记), 因此CMS回收器无法再本次回收它们. 由于浮动垃圾的影响, CMS回收器不能像其他收集器一样, 等老年代被填满后再触发垃圾回收, 需要预留一部分空间给浮动垃圾. jdk1.5中, 默认老年代使用68%后就触发GC; jdk1.6以后该值变为92%. 这个比例可以使用-XX:CMSInitiatingOccupancyFraction
设置. 如果CMS在运行期间的预留内存无法满足程序需求, 就会出现一次Concurrent Mode Failure
失败, 此后CMS会退化成Serial Old
收集器进行收集, 这样停顿时间就很长了
`-XX:CMSInitiatingOccupancyFraction`设置的太高很容易导致大量"Concurrent Mode Failure"失败, 性能反而会降低
-
CMS无法处理浮动垃圾
-
-
CMS会产生大量内存碎片
因为CMS属于标记清除
算法实现的, 所以在收集结束以后会产生很多的内存碎片. 碎片过多会给大对象分配带来麻烦, 往往出现老年代还有很大空间剩余但就是无法找到足够大的连续空间分配给对象. 为了解决这个问题, CMS提供了2个参数开启compact:
-
-XX:+UseCMSCompactAtFullCollection(默认开启)
: 当CMS顶不住要进行FullGC时先开启内存碎片整理
-
-
-XX:CMSFullGCsBeforeCompaction(默认为0)
: 用于设置执行多少次不压缩的Full GC后跟着来一次带压缩的. 默认为0表示每次进入full gc都要开启碎片整理
-
-
CMS会产生大量内存碎片
-
2. G1收集器
- G1的特点
- 划分Region :
G1收集器不再对堆内存分代, 而是划分为多个大小相同的region. 划分方式有有2种 :- 指定每个region的大小. 通过
-XX:G1HeapRegionSize
, 大小区间在1M和32M之间, 但要是2的幂 - 不指定参数时, 默认把堆内存等分成2048份
- 指定每个region的大小. 通过
- 为Region标记
G1对每个Region进行标记为4中类型:Eden, Survivor, Old, Hunmongous
. Humongous用于存放大小超过region大小一半的大对象. 如果1个Region存不下这个大对象, G1会找来连续的H区域装下这个对象, 为了找到连续的H区域可能会不得不切换成serialOldGC进行全堆扫描(eden, survivor, old, hunmongous) - 属于"标记-整理"算法
- 用户客户可以指定gc停顿时间
- 划分Region :
- G1的收集模式
-
YoungGC
正在分配非巨型对象(在eden去分配的对象)时, 如果发现Eden区满了则会触发YoungGC. 每次YoungGC都会回收Eden region和Survivor region, 然后一部分对象将进入old region. YoungGC过程如下 -
MixedGC
当老年代(old region)消耗殆尽时, 会触发混合gc(MixedGC), 会回收整个young region和一部分的old region.而选择哪部分old region收集是通过用户指定的参数-XX:MaxGCPauseMillis
推导而来. 改参数时用户设置的gc停顿时间, 选择那些预估gc时间和用户设置时间相近的old region进行gc
-
YoungGC
- G1垃圾回收的步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选清除
三. GC日志
-
-XX:+PrintGCDetails
该参数告诉虚拟机在进行gc时打印内存回收日志, 并在进程退出时打印各个区的分配情况. 配合该参数的2个额外功能 :-
-XX:+PrintGCDateStamps
: 打印GC日志时带上时间 -
-Xloggc:<file_name>
: 将gc日志打印到文件中
-
- GC日志格式
如下代码产生了如下的gc日志:- 2019-09-24T00:03:18.695+0800 : gc发生时间
- GC (Allocation Failure) : gc类型
- PSYoungGen: 8113K->824K(9216K) : 青年代gc后由8113K减小到824K, 共9216K
- 8113K->6976K(19456K), 0.0037173 secs : 总堆内存大小变化和gc耗时
/** GC日志 */
2019-09-24T00:03:18.695+0800: [GC (Allocation Failure) [PSYoungGen: 8113K->824K(9216K)] 8113K->6976K(19456K), 0.0037173 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-09-24T00:03:18.699+0800: [Full GC (Ergonomics) [PSYoungGen: 824K->0K(9216K)] [ParOldGen: 6152K->6764K(10240K)] 6976K->6764K(19456K), [Metaspace: 3222K->3222K(1056768K)], 0.0053386 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
/** 进程退出时打印的各个区分配情况 */
Heap
PSYoungGen total 9216K, used 2266K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 27% used [0x00000000ff600000,0x00000000ff836b50,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 6764K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 66% used [0x00000000fec00000,0x00000000ff29b0e8,0x00000000ff600000)
Metaspace used 3228K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
四. 对象的内存分配策略
Java自动内存管理分为2部分: 自动回收内存(gc)和自动分配内存(对象内存分配); 前面介绍了自动回收内存, 下面介绍自动发分配内存的几点
1. 优先在Eden区分配, minor gc时Servivor存不下的对象直接进入老年代
2. 大对象直接进入老年代 (-XX:PretenureSizeThreshold)
- 大对象是指占用大量连续的内存空间的java对象, 比如大数组类型对象. 这种大对象当超过-XX:PretenureSizeThreshold配置时会直接在老年代生成. 原因是避免minor gc时反复从eden, survivor0, survivor1之间相互拷贝
- 最影响性能的就是那些"朝生夕灭"的大对象
3. Serial中的对象何时进入老年代?
- Serial中高年龄对象进入老年代 (-XX:MaxTenuringThresholdSize)
- minor gc时Servivor存不下的对象直接进入老年代
- 当Survivor对象中大于一半的对象有相同的age时, 大于等于该age的对象直接进入老年代
4. 老年代的空间担保, 担保失败会进入full gc
每次minorGC之前, 先检查老年代剩余空间是否大于新生代所有对象之和(eden+某个survivor区)
- 是(老年代剩余空间>新生代所有对象之和):
进行一次minorGC, 此时, survivor中存不下的对象会进入老年代 - 否(老年代剩余空间<新生代所有对象之和):
此时可能发生一个问题, 如果新生代的对象在gc后全部进入老年代, 那老年代的空间不是不够了吗? 所以此时会检查一个参数-XX:-HandlePromotionFailure
是否开启(大多数情况会加上这个参数)?- 开启了: 此时老年代剩余空间和"历次gc进入老年代的对象的平均大小"作对比
- "老年代剩余空间" > "历次gc进入老年代的对象的平均大小" : 进行minorGC
- "老年代剩余空间" < "历次gc进入老年代的对象的平均大小" : 先进行full gc(老年代和青年代都gc)
- 未开启: 进行一次full gc(老年代和青年代都gc)
- 开启了: 此时老年代剩余空间和"历次gc进入老年代的对象的平均大小"作对比
上述过程产生的full gc后, 如果仍然有新生代的对象存不下, 则系统OOM
网友评论