Java垃圾回收杂谈

对象是否存活

  既然要进行垃圾回收,那么必然要知道回收谁,什么样的对象需要回收,没有其他地方引用该对象,那么该对象就是垃圾,就需要回收。

  如何知道该对象是否还被引用呢?在早期提出了引用计数法来进行对象是否还被引用的判断,该算法的主要思想就是,如果一个对象被引用了,那么给该对象计数加1,如果取消了一个该对象的引用,那么该对象的计数减一,最终如果对象的计数为0,那么证明没有任何地方还在引用该对象,该对象就会被当做垃圾回收掉。

  这种算法简单高效,实现起来非常容易,但是却存在一个问题而被遗弃,该问题便是循环引用,两个对象的实例变量都相互引用对方,即使两个对象都置为null,但是他们的实例变量仍然引用着对方,两个对象的计数仍然为1,所以不会被当做垃圾,这就是循环引用的问题。

  既然循环引用存在问题,那么必然有另外一种判断对象是否存活的方法,该方法称为可达性分析算法,该算法的主要思想就是以一系列的GC Roots为出发点,然后引用的对象为相关联的可以到达的对象,从图的角度出发,不可到达的对象就是垃圾对象,需要被回收的对象。现在使用的就是这种算法。而可以作为GC Roots的有常量池中的常量引用的对象,静态变量引用的对象,线程栈帧中的本地变量表也就是线程的本地变量,native方法中引用的对象

垃圾回收算法

  垃圾回收算法就是对于垃圾进行回收的时候的整体思路,有很多种垃圾回收算法,也就是说没有谁好谁坏,只是在于使用场景不一样而已

标记清除算法

  标记清除算法就是指当垃圾对象被标记完成后,直接清除这些对象,这种算法比较简单高效,但是存在几个问题,首先,原地清除这些对象会存在很多的空间碎片,也就是说当分配大的对象的时候,有可能因为空间碎片的问题不得不提前触发一次gc。标记和清除操作效率都不是很高

复制算法

  复制算法是先将内存划分为两块,然后只使用其中的一块,当需要进行gc时,将其中一块的存活的对象直接复制到另一块内存中,复制过去的时候直接就按照顺序进行摆放,这样就不会存在内存碎片的问题了。但是解决了内存碎片的问题,确又引起了新的问题,根据我刚才的描述,将内存分成了两部分,每次只能使用其中的一部分,这样对于内存的利用率是非常低的,如果每次需要复制的对象特别少的话,也就是存活的对象特别少,那么就可以调整两个的比例,其实真正使用过程中也就是这么做的。
  利用分类的思想将堆分为了新生代,老年代,新生代的对象特点就是存活时间短,其实在所有的新生代垃圾收集器中都采用的是这种算法。真实生产环境中,将新生代划分为一个Eden和两个Surivivor区,比例为8:1:1,每次都是使用一个Eden和一个Survivor区,需要gc时,将这两个区都复制进入另外一个Survivor,如此循环往复,这样内存的利用率达到了90%。那么这样做,会不会有可能gc时Survivor放不下,其实这种问题是考虑过的,jvm为这种算法提供了内存担保机制,也就是说,当放不下时,是可以把多出的对象放到老年代来进行担保

标记整理算法

  标记整理算法后续步骤与标记清除算法有些不太一样,标记整理算法是标记完成后,让所有存活的对象都往一端移动,然后直接清除掉界限以外的内存空间,这种算法不但没有内存担保,也解决了内存碎片的问题

分代回收算法

  根据不同的区域应用不同的垃圾回收算法,其实在实际过程中,就是这么去做的

垃圾回收器

  在谈垃圾回收器之前,必须得说一说stop the wrold,该单词的意思就是暂停线程的执行状态,为什么呢,比如清洁工人在打扫垃圾的过程中,肯定是不能允许在打扫过程中乱扔垃圾的。垃圾回收也一样,如果正在进行垃圾回收,由于线程的执行,导致某些已经是垃圾的对象确又重新引用了,很明显会出问题,所以在垃圾回收的过程中就需要暂停所有线程。

  说到暂停线程,线程能随便暂停吗,肯定是不行的,这就提出了安全点的概念,线程暂停也分为主动和被动,主动就是让线程主动去轮询看是否到达了安全点,到了最近的检查点就停下来,而被动则是全部中断所有线程,让没有到达安全点的线程继续执行起来,到达安全点为止。在实际的使用中采用了主动的方式。

  还需要知道的是jvm在找GC Roots的时候,并不是盲目的遍历,jvm为GC Roots维护了一个Oop Map的数据结构,通过该数据结构,jvm可以快速的找到gc root

了解完了这些以后就来说一说垃圾收集器吧

Serial收集器

  该收集器应用于客户端模式下的新生代,使用的是复制算法,该收集器是单线程的,也就是说在进行垃圾回收的时候,会暂停用户线程,这种收集器简单高效,唯一的缺点就是单线程,但是平常的使用过程中回收几十或者几百只会停顿几十毫秒,所以客户端模式下还是可以接受的。

ParNew收集器

  该收集器应用于服务端新生代,使用的是复制算法,该收集器是多线程的,其余特点和Serial相似,在多CPU下会比Serial收集器有更大的效用。

Parallel Scavenge收集器

  应用于服务端新生代,采用复制算法,是服务端默认的收集器,该收集器专注于吞吐量,而吞吐量就是垃圾收集时间对于cpu执行时间的占比,也就是说其他的收集器关注的是并发,stop the world,而该收集器关注的吞吐量,控制了吞吐量也就是提高了相应时间,所以该收集器适用于后台的运行处理,被设置为服务端默认的垃圾收集器。但是比较尴尬的一点是前期居然没有可以和Parallel Scavenge搭配的老年代收集器,主要就是由于使用的GC框架不同导致的。

Serail Old收集器

  应用于客户端老年代收集器,使用标记整理算法单线程,是Serial的老年代版本,可以与Serail或者Par New搭配使用

CMS收集器

  CMS应用于服务端老年代收集器,使用了标记清除算法,该收集器几乎实现了不用stop the world,只是几乎,也即是时间非常短,但是还是需要暂停用户线程的。CMS是真正意义上的第一款老年代并发收集器,该收集器的特点就是几乎不需要暂停用户线程,收集过程分为下面几个过程。首先是初次标记,初次标记快速找到GCRoot,然后标记与GCRoot直接相连的对象,直接怎么理解,就是只有一步,而不是递归深入,下来是并发标记,该过程可以与用户线程同时进行,这个过程是递归深入,标记所有相连的对象,接着是重复标记,重复标记就是为了标记在并发标记阶段由于用户线程的执行所导致的对象的变动,最后是并发清除,清除垃圾对象。既然是用的是标记清除算法,就会存在内存碎片,这是CMS的缺点,而且CMS会存在浮动垃圾无法回收的问题。

Parallel Scavenge Old收集器

  该收集器是为了配合Paralle Scavenge的新生代收集器而出现的,使用标记整理算法,由于Serail是单线程的,与Parallel Scavnege配合使用不能发挥性能,而CMS使用的GC框架与Parallel Scavnege不同,所以以前该Parallel Scavnege处于比较尴尬的局面,在后面Parallel Scavenge Old出现后,这两个搭配使用,会提高用户的吞吐量,性能得到提升。

G1收集器

  上面讲的垃圾收集器都只局限于一个区域,而G1则是可以包括新生代和老年代,G1使用的是复制算法+标记整理算法,G1将新生代和老年代揉到一块,把堆分为Eden Region和Survivor Region,分成很多个区域Region,区域之间使用复制算法,区域内部使用标记整理算法。G1不但可以并行处理,减少了停顿时间,原理和CMS相同,最大的一个特点就是可以控制GC的时间,设置GC时间为M,则GC不会超过M,实现是通过维护了一个优先级队列,每次回收都回收价值最大的区域。

最后附上一张垃圾回收器之间的搭配使用图:
在这里插入图片描述

以上是我结合《深入理解Java虚拟机》所做的一些思路回顾,写的比较乱,有理解错误欢迎大家指出

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值