Garbage Collection

如果判断对象已死

引用计数算法

给对象添加一个引用计数器,每当有个地方引用他时计数器就加1,引用失效时,计数器就减1,当计数器为0时那么该对象则已经死亡。不过该算法不能解决对象之间循环引用的问题。但是也有优点那就是因为实现简单因此判定效率很高。

1
2
3
4
5
6
7
8
ObjectA A = new ObjectA();
ObjectB B = new ObjectB();
A.instance = B;
B.instance = A;

//如果使用引用计数,即使赋值为null也无法回收,因为互相引用,计数器不会为0
A = null;
B = null;

可达性分析算法

基本思想是通过一系列称为GC Roots的对象作为起始点,从这些节点往下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的时候,那么这个对象就是不可用的。

GC Roots最大的特点就是它一定不会被回收,以虚拟机栈举例,如果栈中有个对象A引用了对象B,如果B没有引用其他对象,那么以A为起始点的可达性分析就结束了,并且A和B都不会回收。

可作为GC Roots的对象包括以下:

  • 虚拟机栈中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI引用的对象。

对象死亡过程

要真正宣告一个对象死亡,至少需要两次标记过程:

  1. 如果对象经过可达性分析之后没有发现任何和GC Roots相连接的引用链,那么会进行第一次标记并进行一次筛选,筛选条件为是否需要执行对象的finalize方法,当对象没有覆盖或者已经执行过finalize,就不会再执行finalize。
  2. 如果确实需要执行finalize方法,这个对象会被放在F-Queue队列中,之后被一个由虚拟机自动建立的Finalizer线程去执行。如果finalize方法中仍然没有和引用链上的对象建立连接,GC会对F-Queue进行第二次标记,然后彻底进行回收。

Warning:由于finalize方法运行代价高昂,不确定性大,因此不建议复写该方法,如果有收尾的处理放在try-finally中更好。

垃圾收集算法

标记-清除算法

顾名思义,一共分为两个阶段,标记出所有需要回收的对象(@对象死亡过程),完成标记之后统一回收所有的对象。

该算法有两个缺点:1.效率不高 2.标记清除之后会产生大量不连续的内存碎片,碎片太多会导致以后需要分配较大对象时,由于连续内存不够需要提前触发一次GC。

复制算法

基本思想是将可用内存等分为两个部分,每次分配内存都只从其中一个分配,当A内存用完了,将A内存中还存活的对象全部复制到B内存上去,再把A内存一次清理掉。这样能解决内存碎片的问题,但是代价则是将内存缩小为原来的一半。但是没有必要严格按照1:1的比例来切割,因为新生代中的对象98%都是很快就回收了的。

标记-整理算法

标记整理算法和标记清除算法的标记过程是一样的。但在清理之前多了一步,就是将所有存活的对象向一端移动。

由于复制算法在对象存活率较高的老年代要复制很多对象,该算法适用于老年代。

分代收集算法

根据对象存活周期的不同将内存划分为几块,一般为新生代和老年代。新生代由于大批对象死亡,因此使用复制算法,而老年代由于存活率高,就需要用标记清除或者标记整理的算法。

Stop The World:GC进行时必须暂停Java的所有线程。

垃圾收集器

垃圾收集器是收集算法的具体实现

Serial收集器

该收集器是单线程收集器,只使用一个CPU或者线程区完成GC。因此优点在于简单高效,然而缺点也很明显它在进行垃圾收集时,必须暂停其他所有的工作线程。

ParNew收集器

该收集器是Serial收集器的多线程版本,除了使用多线程之外,其余的例如收集算法、Stop The World、回收策略等等都和Serial收集器完全一样。

它作为Server模式下的虚拟机中首选的新生代收集器原因之一是只有它能和CMS收集器配合工作。

Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器。Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。可以通过设置收集器的参数来控制吞吐量。

吞吐量:CPU用于运行代码的时间占比,如果虚拟机一共运行了100分钟,GC了10分钟,那么吞吐量就是90%。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本。同样单线程,采用标记整理算法。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和标记整理算法。

CMS收集器

Concurrent Mark Sweep从名字可以看出这是一个并发的收集器,以获取最短回收停顿时间为目标的收集器。并且基于标记清除算法。不过有以下三个缺点

  • 由于是并发,会在一定程度上占用CPU。
  • 无法处理浮动垃圾,因为在GC过程中没有停顿,因次会有新的对象生成。
  • 由于是标记清除算法,因此会有大量内存碎片生成。

内存分配

  • 新生代
    • Edan区
    • 两个Survivor区
  • 老年代
  • 永久代

  • Minor GC: 在新生代发生的垃圾收集动作,较频繁,时间短

  • Major GC/Full GC: 在老年代发生的GC,速度较慢。

大多数情况,新建的对象将分配到Edan区,当Edan区空间不足时,就会触发一次Minor GC,也就是将Edan区清空,然后将存活的对象拷贝到Survivor区,并将Survivor区内对象的年龄默认设置为1,之后每经过一次GC就会增长1,并且会在两个Survivor区之间互相切换。当成长到15的时候就会被转移到老年代。当然这个阀值可以通过参数设置。

至于为什么需要两个Survivor区,假设只有一个Survivor区,那么第一次执行GC,将存活的对象拷贝到Survivor区,暂时没毛病,程序运行了一段时间又满了,这时候再执行GC,但是在这种情况下对Survivor区执行GC的话就会在Survivor区内造成大量的内存碎片,因此需要两个Survivor区的主要原因是为了清除生成的碎片,能保证有一块Survivor永远处于干净状态。另一个原因是可以继续对Survivor区进行复制的垃圾收集算法,也是为了提升GC的性能。

对于某些大对象将直接进入老年代,大对象就是指需要大量连续内存的对象,例如长字符串或者数组。

图解:http://www.tothenew.com/blog/java-garbage-collection-an-overview/