Java虚拟机专题之垃圾回收(读书笔记)

一 如何判断对象是垃圾对象

1.1 引用计数法 (Reference Counting)

在对象中添加一个引用计数器,当有其他地方引用这个对象的时候,引用计数器就加1,当引用失效的时候就-1. 当垃圾回收器检查到引用为0,就会认为是垃圾对象,进行回收。

 

但是有一个问题,比如对象之间循环引用,诸如A,B两个对象,都有一个属性instance, 假设A.instance = B,B.instance=A。他们互相引用着对方,导致对方引用计数器都不为0。

 

所以JVM一般都不使用这种方法,因为难以解决对象之间相互循环引用问题。

 

 

1.2 可达性分析法 (ReachabilityAnalysis)

通过一系列GC Roots对象作为起点,这些节点开始向下搜索,搜索走过的路径叫做引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连接,则证明此对象是不可用的。

 

对于可达性分析,我们知道GC Roots是很重要的,那么有哪些对象可以作为GC Roots呢?

# 虚拟机栈中引用的对象

# 方法区中静态类属性引用的对象

# 方法区中常量池引用的对象

# 本地方法栈中JNI引用的对象

 

如下面2图所示:

第一幅图中,除了object4 没有到任何GC Roots的引用链相连接,其余的都有,所以可以认为object4 是不可用的

第二幅图中因为object1到GC Roots的引用链没了,所以object1下面的引用链上对象都不可用,因为都没引用链连接到对应的GC Roots


二 垃圾回收算法(回收策略)

2.1 标记-清除算法(Mark-Sweep)

标记出需要回收的对象,如何标记呢,就是通过可达性分析法,然后有一个清除线程来将标记的对象清除掉。

# 效率问题:标记清除效率都不高

# 空间问题: 内存碎片问题

标记清除之后,会产生大量不连续的没存碎片,空间碎片太多可能会导致以后程序要分配较大的对象时,无法找到足够的连续内存,而不得不提前触发一次新的垃圾收集动作。


2.2 复制算法(Copying)

虚拟机将内存容量划分为大小相等的2块,每一次只使用其中一块。当其中一块用完了,就将存活的对象复制到另一块上去,然后已使用这块内存清理。

优点:

效率高,解决了内存碎片问题

缺点:

浪费内存;对象存活率较高的时候,会进行较多的复制操作,效率将变得低下

所以其特点也决定,只适合在新生代区域使用,不适合老年代,因为老年代的对象生命周期都比较长,所以如果出现极端情况,100%的存活率,就容易发生Full GC


2.3 标记-整理算法(Mark-Compact)

我们知道,老年代不适合使用复制算法,所以根据老年代的特点,提出了标记整理算法。

# 先进行标记

# 标记完了之后,不是直接进行清理,而是让所有存活的对象都往一段移动

# 移动完了之后,直接从边界处清理掉不可用对象

优点

解决了标记-清除算法中的碎片问题;也解决了复制算法中内存浪费问题

缺点:效率不高

所以不适合频繁进行GC的场景,而老年代GC的发生次数比较少,所以适合老年代


2.4 分代收集算法(GenerationalCollection)

根据对象对象不同的存活周期,将堆内存划分成新生代和老年代。

针对不同的年龄代使用不同的垃圾收集算法。

比如新生代,经常有大量对象死去,只有少量存活可以使用复制算法,而老年代因为存活率较高,所以一般使用标记清理或者标记整理算法。

 

三 垃圾收集器

3.1 Serial收集器

JDK1.3 之前使用的垃圾收集器,它是单线程的。但是这个单线程必须暂停所有的工作线程,然后再进行垃圾回收,直到它收集结束。


很明显,这种垃圾回收方式是令人难以接受的。

开启方式:-XX:+UseSerialGC

 

3.2 ParNew收集器

ParNew除了是多线程进行垃圾回收以外,其余的特点和Serial几乎是一样的。可以认为是Serial的多线程版本。

Serial 和 ParNew比较

相同点:

# 暂停所有工作线程,然后进行垃圾回收

# 采用复制算法,对新生代进行垃圾回收

不同点:

# Serial是单线程;ParNew是多线程


开启方式:-XX:+UseParNewGC

 

3.3 ParallelScavenge 收集器

Parallel也是一个新生代采用复制算法的收集器,而且也是多线程收集器,感觉和ParNew差不多。

ParNew 和 Parallel 收集器比较

相同点:

# 采用复制算法,对新生代进行垃圾回收

# 使用多线程进行垃圾回收

不同点:

关注点不一样,CMS,ParNew等收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间;而Parallel 收集器目标是达到一个可控制的吞吐量。

什么是吞吐量?

吞吐量 = (运行用户代码时间)  / (运行用户代码时间 + 垃圾收集时间)

如果虚拟机总共运行了100分钟,而垃圾收集花了1分钟,那么吞吐量就是99%。

 

Parallel提供了2个参数用于精确控制吞吐量

 

-XX:MaxGCPauseMills: 垃圾收集器最大停顿时间(毫秒)

-XX:GCTimeRatio: 吞吐量大小(值应该大于0且小于100)

开启Parallel和Parallel Old收集器的方式:

-XX:+UseParallelGC -XX:+UseParallelOldGC

 

3.4CMS收集器

CMS是Concurrent Mark Sweep,即并发标记删除,而不是内容管理系统的缩写。在这个过程中,部分阶段所有线程暂停,部分阶段会和应用线程一起并发执行

 

工作过程

# 初始标记 (CMS initial mark)

会暂停整个虚拟机,然后标记所有的跟对象(GCRoots),但是GC Roots一般都很少,所以这个过程很快。

 

# 并发标记 (CMS concurrent mark )

虚拟机会从根节点开始,将所有引用到的对象都打上标记,而且这个阶段是和应用线程一起并发执行

 

# 重新标记 (CMS remark)

会暂停整个虚拟机。由于之前是并发执行,那么一边标记,一边可能会有一些新的更新,比如之前是有引用的对象,现在没有引用了,所以需要重新标记

 

# 并发清除 (CMS concurrent sweep)

将没有标记的对象作为垃圾回收掉,这个阶段也是和应用线程一起工作的。

 

优点

并发收集,低停顿

 

缺点

# 占用的CPU资源更多

# 无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Model Failure

 

# 由于是基于标记清除算法,所以存在内存碎片问题

 

为什么存在无法处理浮动垃圾(FloatingGarbage),可能出现ConcurrentModel Failure而导致另一次的Full GC产生的原因分析?

 

浮动垃圾:CMS并发清理阶段,用户线程还在继续运行,就有垃圾不断产生,由于出现在了重新标记之后,CMS无法再次收集处理掉,只能留到下一次GC时再来清理。这就是浮动垃圾。

 

垃圾收集阶段,用户线程还要继续运行,那也就需要预留有足够的内存 空间给用户线程使用,因此CMS不像其他的收集器,等待老年代都填满了才再进行收集。

如果老年代不是增长的很快,那么可以适当提高参数:

-XX:CMSInitiatingOccupancyFractiont,以提升触发的百分比

 

JDK1.6 中,CMS收集器启动阀值时92%,要是CMS运行期间预留的内存无法满足程序需要就会出现Concurrent Model Failure失败,这时候虚拟机启动预备方案:临时启用Serial Old对老年代进行收集,这样停顿时间就长了。

 

所以说参数-XX:CMSInitiatingOccupancyFraction设置太高容易导致Concurrent Model Failure失败,性能反而降低了。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫言静好、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值