JVM垃圾回收机制详解
This is a hidden message
什么是垃圾回收
在JAVA中,垃圾回收(GC)是指一种自动的内存管理机制。当堆上的对象不再被引用时,JVM垃圾收集器就会回收这些对象,以此释放内存空间。
JVM内存模型
JVM将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区;
在上面这些区域中,程序计数器、虚拟机栈和本地方法栈都是线程私有的;
而堆和方法区是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,所以这块区域经常发生垃圾回收;方法区则用来存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据,而一般我们称之为持久代。
由于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区清空,以此循环往复,所以只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;
垃圾收集器
在讲完了”垃圾”识别算法和”垃圾”回收算法,我们再来讲讲算法的执行者,也就是垃圾收集器。
垃圾收集器的类型
垃圾收集器主要分为下面四种类型:
- 串行垃圾回收器(Serial Garbage Collector);
- 并行垃圾回收器(Parallel Garbage Collector);
- 并发标记扫描垃圾回收器(CMS Garbage Collector);
- 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 GC
和Full GC
。
Minor GC:只会收集年轻代,在伊甸区满时,会触发。
Full GC:收集整个堆,包括新生代,老年代和永久代。
Full GC
触发条件:
- 通过
Minor GC
后进入老年代的平均大小大于老年代的可用内存; - 老年代空间不够分配新的内存;
- 在年轻代进行复制清理是,如果对象大于S0或者S1区,就会把该对象转存到老年代,且老年代的可用内存小于该对象大小。
- 调用
System.gc
时,系统建议执行Full GC
,但是不必然执行。