JVM垃圾回收机制详解

JVM垃圾回收机制详解

什么是垃圾回收

在JAVA中,垃圾回收(GC)是指一种自动的内存管理机制。当堆上的对象不再被引用时,JVM垃圾收集器就会回收这些对象,以此释放内存空间。

JVM内存模型

JVM将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区;

在上面这些区域中,程序计数器、虚拟机栈和本地方法栈都是线程私有的;

而堆和方法区是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,所以这块区域经常发生垃圾回收;方法区则用来存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据,而一般我们称之为持久代。

image

由于JVM采用的是分代回收的算法,也就是根据对象的生命周期进行分代存储和回收,所以JVM将堆内存分为年轻代、老年代和永久代;而年轻代中又分为伊甸区和幸存区。

如何判断一个对象是否存活

在了解垃圾回收的算法之前,我们先来看看JVM是如何判断一个对象是否存活的;

引用计数法

引用计数法会给每个对象设置一个引用计数器,当有一个地方引用到了这个对象时,计数器就会+1,而引用失效时,计数器会-1;当引用计数器变为0的时候,就说明这个对象没有被引用,也就是垃圾对象,它就会等待回收。

这种方式虽然简单明了,但是也存在缺点,如果当两个对象循环依赖的时候,这两个对象的引用计数器永远不可能为0,也就无法回收,所以引用计数器只出现在早期的JVM中,现在基本不再使用了。

可达性分析法

可达性分析法的核心思想是标记一些根对象(GC Roots),然后从根对象出发,一步步遍历和根对象有引用关系的对象,从而形成引用链。而不在这些引用链上的就称之为不可达对象。

而在JAVA中能称之为根对象(GC Roots)的一般有一下几种:

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

而满足以上条件的对象不一定马上被回收,还会进行两次标记,这边我们继续往下看。

垃圾回收算法

现在我们已经了知道了如何判断对象是否存活,接下来我们来看下JAVA中的垃圾回收算法。

标记清除法

标记清除法,顾名思义先会通过可达性分析法去遍历内存,把存活对象和垃圾对象进行标记;然后在此遍历内存并标记对象,将垃圾对象回收。

而这个算法正如上面的图那样,缺点很明显,会产生内存碎片,这样就会导致之后需要存放大对象时没有足够的连续分片,而不得不再次GC。

标记整理法

而为了解决内存碎片的问题,标记整理算法就诞生了,标记整理算法的第一步也是遍历内存进行标记,第二部会将所有存活的对象向一端移动,然后将端边界外的对象全部回收。

这个算法的有点是可以解决内存碎片的问题,不过也存在缺点,就是移动的代价很大,对于现在动辄几十G的内存空间,移动对象相当的耗费资源。

复制算法

无论是标记清除法还是标记整理法,都会有存在开销或者内存碎片的问题,那么为了解决这两个问题,复制算法就出现了。

复制算法的核心是将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除。

这样同时解决了内存碎片和句柄开销的问题,但是由于需要两倍的内存,导致内存的利用率变低了。

分代收集算法

而在JVM中,结合了多种垃圾回收算法,根据内存对象的存活周期不同,将内存划分成几块:新生代和老生代;在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,第一次会将E区+S0区的存活对象复制到S1区,然后将E区+S0区清空,第二次会将E区+S1区的存活对象复制到S0区,然后将E区+S1区清空,以此循环往复,所以只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;

垃圾收集器

在讲完了”垃圾”识别算法和”垃圾”回收算法,我们再来讲讲算法的执行者,也就是垃圾收集器。

垃圾收集器的类型

垃圾收集器主要分为下面四种类型:

  1. 串行垃圾回收器(Serial Garbage Collector);
  2. 并行垃圾回收器(Parallel Garbage Collector);
  3. 并发标记扫描垃圾回收器(CMS Garbage Collector);
  4. G1垃圾回收器(G1 Garbage Collector)。

JVM中的垃圾收集器

而在JVM中垃圾收集器主要有一下七种:

  • Serial:单线程的收集器,收集垃圾时,必须STW(stop the world),使用复制算法。由于它在垃圾回收时会STW,所以对于有些应用是难以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒之内,大多数应用还是可以接受的,是客户端的默认GC方式。
  • ParNew:Serial收集器的多线程版本,也需要STW。
  • Parallel Scavenge:新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量,和ParNew的最大区别是GC自动调节策略;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量;
  • Serial Old:Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
  • Parallel Old:是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。
  • CMS:是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片;
  • G1:标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选回收。不会产生空间碎片,可以精确地控制停顿;G1将整个堆分为大小相等的多个Region(区域),G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率;

垃圾回收的时机

垃圾回收一般分为两种:Minor GCFull GC

Minor GC:只会收集年轻代,在伊甸区满时,会触发。

Full GC:收集整个堆,包括新生代,老年代和永久代。

Full GC触发条件:

  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
  • 老年代空间不够分配新的内存;
  • 在年轻代进行复制清理是,如果对象大于S0或者S1区,就会把该对象转存到老年代,且老年代的可用内存小于该对象大小。
  • 调用System.gc时,系统建议执行Full GC,但是不必然执行。
作者

ero

发布于

2022-02-28

更新于

2022-06-11

许可协议

评论