深入理解Java垃圾回收机制(GC机制)及其实现原理

深入理解Java垃圾回收机制(GC机制)及其实现原理

前言

说起垃圾收集,大部分人都把这项技术当作Java语言的伴生产物。实际上,GC的历史比Java久远,1960年诞生与MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。

Java语言的所有指令都是建立在JVM基础之上的,JVM是Java Virtual Machine(Java虚拟机)的缩写,作为Java语言最重要的特性之一的自动垃圾回收机制也是基于JVM实现的。

垃圾

什么是垃圾?

对于Java来说,垃圾就是指那些在堆中存在的,已经“死亡”的对象。而对于“死亡”的定义,就是该对象不再被任何内存引用。

两种垃圾判断算法

引用计数法(Reference Counting)

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,这就是引用计数算法的核心。

  • 优点:实现比较简单。
  • 缺点:需要额外的空间来存储计数器,并且在对象之间相互引用的时候会无法对被回收。

可达性分析法(Reachability Analysis)

这是Java虚拟机采用的判定对象是否存活的算法。通过一系列的称为“GC Roots"的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。可作为GC Roots的对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象。也就是说,堆内存中的对象如果没有被方法区或虚拟机栈中的GCRoots所引用的话,那么这个对象就被看作是可回收的垃圾。

  • 优点:不需要额外的空间来存储计数器,并且避免了对象之间相互引用而无法被回收的情况
  • 缺点:GCRoots在方法区和虚拟机栈中,属于线程共享,会被多线程更新。

一般来说,强引用是判断是否是GCRoots的标志。

  • **强引用(Strong Reference):**强引用是使用最普遍的引用,是作为GCRoots的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。
Object strongReference = new Object();
  • 软引用(Soft Reference):内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
//强引用
String string = new String("abc");
//软引用引用了一个强引用
SoftReference<String> softReference = new SoftReference<String>(string);
  • 弱引用(Weak Reference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

//强引用
String string = new String("abc");
//弱引用引用了一个空的强引用
SoftReference<String> weakReference = new SoftReference<String>(string);
string = null;
  • 虚引用(Phantom Reference):虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

垃圾回收

垃圾回收算法

标记-清除算法

标记-清除算法是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。

  • 缺点:有内存碎片。

复制算法

复制算法是为了克服标记清楚算法的缺陷,将内存划分为两个部分,每次只使用一部分。在标记清除的时候,先将不被清除的引用复制到另一块内存,然后整体清除之前的内存。

标记-整理算法

为了解决复制算法的缺陷,充分利用内存空间,提出了标记整理算法。该算法标记阶段和标记清除一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法(压缩法)

垃圾回收器

Serial收集器

新生代,标记-复制算法,单线程。进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束

ParNew收集器

ParNew本质上是Serial收集器的多线程并行版本

Parallel Scavenge收集器

新生代,标记复制算法,多线程,主要关注吞吐量
吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)

Serial Old收集器

老年代,标记-整理算法,单线程,是Serial收集器的老年代版本

Parallel Old收集器

老年代,标记-整理算法,多线程,是Parallel Scavenge收集器的老年代版本。

CMS收集器

老年代,标记-清除算法,多线程,主要关注延迟

Garbage First收集器(G1)(新生代 + 老年代)

它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。

Garbage First收集器(G1)(新生代 + 老年代)

它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。

使用复制 + 标记 - 整理算法收集新生代和老年代垃圾。


版权声明:本文为rookie77原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。