JVM初识之垃圾回收机制(GC)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Weixiaohuai/article/details/86559166

一、简介

大家都知道,随着程序中不断的new创建对象,创建对象意味着要不断分配内存,而计算机的内存大小是有限的,不可能一直分配内存不释放,如果一直这样下去迟早引发内存溢出,为了尽量避免出现内存溢出,虚拟机提供了垃圾回收机制(garbage collection),对一些无用对象进行回收以释放不再使用的内存,减轻计算机的内存压力。

二、哪些是属于要被回收的垃圾

通常来说,那些不可能再被任何途径(反射调用、引用等)使用的对象,就会被垃圾回收器列为被回收的名单当中。

三、判断某个对象是否还被使用的方法

JVM提供了两种方法用于判断某个对象是否还在被其他对象引用或者调用:引用计数法 和 可达性分析法(跟搜索法),下面分别介绍一下这两种方法:

【a】引用计数法: 类似计数器的思想,当对象在其他地方被引用时,计数器数值加一;反之,当其它地方释放对对象的引用时,计数器数值减一,任何时刻,如果计数器数值为0,垃圾回收器就是认为这个对象是不可能再被使用的。

假设a = b ,则b引用的对象实例的计数器数值加一

优点: 适用于大部分场景

缺点:对于对象之间相互引用的情况很难解决,如果使用引用计数法,会造成垃圾回收器会将互相引用的两个对象同时回收掉。

下面通过一个示例来说明这种现象,代码如下:

public class TestReferenceCounter {

    private Object obj = null;

    public static void main(String[] args) {
        TestReferenceCounter counter1 = new TestReferenceCounter();
        TestReferenceCounter counter2 = new TestReferenceCounter();

        //两个对象相互引用
        counter1.obj = counter2;
        counter2.obj = counter1;

        //将对象置空,并通知垃圾回收器进行回收
        counter1 = null;
        counter2 = null;
        System.gc();
    }

}

然后再配置一下VMoptions打印gc日志:

查看控制台gc日志:

可见,相互引用的两个对象被垃圾回收器当做垃圾回收,这也是不太友好的地方。

【b】可达性分析法 : 也叫“根搜索”方法,可以把它想象成一棵树,从根节点开始搜索,寻找对应的引用节点,搜索走过的路径,称为引用链,当一个对象到根节点(GC Roots)没有引用链时,则说明该对象是不再使用的对象,垃圾回收器会将这些对象列入被回收对象的名单。

可以作为根节点(即GC Roots跟搜索节点)的对象一般有下面几种:

  • JVM栈中栈帧中局部变量表中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI(Native本地方法)引用的对象;

如上图中,对象1、2、3、4、5、6、7到GCRoots都有引用链,所以这些对象不会被回收,对象8、9、10、11、12中虽然相互之间可能有引用关系,但是到GCRoot都没有引用关系,所以这些对象将会被垃圾回收器列为不可达对象。

四、对象引用分类

在java中,将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次减弱。

引用类型 描述
强引用 例如 Object object = new Object(),对于强引用对象,垃圾回收器永远不会回收被强引用着的对象
软引用 有用非必须对象,在即将发生内存溢出时会进行回收
弱引用 非必须对象,被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,不管内存足不足够都会回收这些对象
虚引用 无用对象,对象被收集器回收时收到一个系统通知

五、方法区垃圾回收

方法区中的垃圾回收主要针对废弃常量和无用对象进行回收。

  • 废弃常量:假如系统中没有任何一个对象引用了方法区常量池中的常量字面量,如果有必要进行垃圾回收时,这些常量将会被移出常量池中,被回收掉。
  • 无用对象:满足下面三个条件才能说明对象是无用的:

【a】在Java堆内存中不存在该类的任何实例;

【b】加载该类的ClassLoader类加载器被回收掉;

【c】该类对应的Class对象没有在任何地方使用到,任何地方都没有使用反射(Class.forName(String name))来调用该类的方法等信息;

六、垃圾回收算法

常用的垃圾回收算法主要有:标记-清除法、复制算法、标记-整理算法、分代收集算法,下面具体对每种算法的思想做一个简单的概述:

【a】标记-清除(Mark-Sweep)算法

标记-清除算法,主要分为两步进行: 【标记】和【清除】,首先对需要被回收的对象进行标记,标记完成之后统一对这些对象进行回收处理。

优点:标记- 清除算法比较简单,也是最基础的算法。

缺点:标记-清除算法效率并不高,如果有大量对象需要被回收,也就是需要对大量对象进行标记,显然标记这些对象耗费的时间将会比较长,所以标记的效率并不高。另外,标记-清除算法容易产生大量不连续的内存碎片,如果某个时刻需要分配一个较大的对象时,由于无法找到那么多连续的空间分配,这就会提前触发下一次gc垃圾收集动作。

下面通过画图简单的描述一下标记-清除算法的思想:

【b】复制(Copying)算法

复制算法,就是将内存分为两块区域,每一次都只使用其中的一块区域,当一块内存区域用完时,就将那些还存活的对象复制到另外一块区域上面,然后再将使用过的内存区域一次性清空。每次只需要对一块区域进行内存回收,内存分配时也不需要考虑内存碎片等情况,只需移动指针,按照顺序分配。

优点:效率相对较高,不会产生大量的内存碎片。

缺点:使用的内存只有一半,代价相对较大。比较适合用来回收新生代对象,因为新生代对象的存活时间短,复制对象到另外一块区域时效率就比较高。当对象存活率比较高时,由于需要复制大量的对象到另外一块内存,所以耗费的时间很长,效率低。不适用于老年代对象的收集。

下面通过画图简单的描述一下复制算法的思想:

【c】标记-整理(Mark-Compact)算法

标记-整理算法,是针对老年代对象提出的,因为老年代对象存活率比较高,不适合使用复制算法,标记-整理算法,思想类似于标记-清除算法,不过不是对标记的可回收对象进行回收,而是将尚存活的对象向一端移动,然后直接清除端边界以外的所有内存。

下面通过画图简单的描述一下标记-整理算法的思想:

【d】分代收集算法

现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。分代收集算法的思想,就是根据对象的不同生命周期,采用不同的垃圾回收算法,根据每一代的特点,选择最适合该代的回收算法,达到最高的回收效率。对象存活率比较低(新生代),使用复制算法,复制成本低;对象存活率高(老年代),采用标记-清理算法或者标记-整理算法。

七、垃圾收集器

常见的垃圾收集器有:Serial收集器、Serial old收集器、ParNew收集器、Parallel Scavenge收集器、Parallel Old收集器、CMS收集器、G1收集器。下图为HotSpot虚拟机中包含的收集器:

下面对每种收集器做简单描述:

【a】Serial收集器

Serial收集器,串行收集器,采用复制算法实现的单线程收集器,在进行收集工作时,需要暂停其他线程的所有工作,直到收集工作结束,这就会造成一定的停顿时间,不过Serial收集器简单而且比较高效,是虚拟机运行在Client(客户端模式)模式下的默认新生代收集器。

特点:

1. 需要暂停其他工作线程(STW(Stop The World)),直到收集工作完成,停顿时间长。

2. 简单高效,对于单个CPU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。

下图展示了Serial收集器的运行示意图(从网上下载)

【b】Serial old收集器

Serial old收集器是Serial串行收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理算法”。

【c】ParNew收集器

ParNew收集器就是Serial收集器的多线程版本,也是使用复制算法实现,是服务端模式(Server模式)下的虚拟机首选的新生代收集器,随着CPU数量的增加,ParNew收集器对于GC时系统资源的有效利用还是很有好处的,默认开启的收集线程数与CPU数量相同,在CPU数量非常多的情况下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数

【d】Parallel Scavenge收集器

Parallel Scavenge收集器也是一个新生代收集器,也是用复制算法的收集器,也是并行的多线程收集器、Parallel Scavenge收集器的目标则是打造一个可控制的吞吐量,Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器

虚拟机提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio两个参数来精确控制最大垃圾收集停顿时间和吞吐量大小。

Parallel Scavenge收集器有一个-XX:+UseAdaptiveSizePolicy参数,虚拟机会根据当前系统的运行情况手机性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

特点:并行收集器,追求高吞吐量,高效利用CPU,适合后台应用等对交互要求不高的场景,是Server级别默认的收集器。

【e】Parallel Old收集器

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

【f】CMS收集器

CMS收集器以获取最短回收停顿时间为目标的收集器。使用标记 - 清除算法实现。收集过程分为如下四步:

(1). 初始标记,标记GCRoots能直接关联到的对象,时间很短。

(2). 并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。

(3). 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。

(4). 并发清除,回收内存空间,时间很长。

其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。

特点:高并发、低停顿、追求最短GC回收时间、cpu占用率高、响应时间快、停顿时间短、多核cpu追求高响应时间的选择。

【g】G1收集器

G1收集器可以同时用新生代和老年代中,与其他收集器相比,G1收集器有下面一些特点:

(1). 并行和并发。使用多个CPU来缩短Stop The World停顿时间,与用户线程并发执行。

(2). 分代收集。

(3). 空间整合。基于标记 - 整理算法,无内存碎片产生。

(4). 可预测的停顿。

使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分(可以不连续)Region的集合。

八、GC触发时机

GC有两种类型:Scanvenge GC和Full GC。

【Scanvenge GC / Minor GC(新生代GC)】:一般情况下,当新对象产生,并且在新生代Eden区域申请空间失败时就会触发Scanvenge GC。

【Full GC / Major GC(老年代GC)】:对整个堆空间进行整理,由于需要对整个堆进行整理,所以耗费的饿时间比Scanvenge GC长。一般以下原因会导致Full GC:

  • 1. 年老代被写满;
  • 2. 持久代被写满;
  • 3. 显式调用System.gc()方法;
  • 4.上一次GC之后堆的各区域分配策略的动态变化;

九、查看GC日志

下面是一段GC的日志,我们分析一下GC日志的内容:

[GC (System.gc()) [PSYoungGen: 2477K->680K(15360K)] 2477K->688K(51200K), 0.0029335 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 680K->0K(15360K)] [ParOldGen: 8K->607K(35840K)] 688K->607K(51200K), [Metaspace: 3235K->3235K(1056768K)], 0.0113457 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
Heap
 PSYoungGen      total 15360K, used 355K [0x00000000eee00000, 0x00000000eff00000, 0x0000000100000000)
  eden space 13312K, 2% used [0x00000000eee00000,0x00000000eee58d78,0x00000000efb00000)
  from space 2048K, 0% used [0x00000000efb00000,0x00000000efb00000,0x00000000efd00000)
  to   space 2048K, 0% used [0x00000000efd00000,0x00000000efd00000,0x00000000eff00000)
 ParOldGen       total 35840K, used 607K [0x00000000cca00000, 0x00000000ced00000, 0x00000000eee00000)
  object space 35840K, 1% used [0x00000000cca00000,0x00000000cca97ea0,0x00000000ced00000)
 Metaspace       used 3252K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 355K, capacity 388K, committed 512K, reserved 1048576K

下面对部分GC日志进行分析:

十、总结

看了两天关于垃圾回收机制方面的资料,包括看了很多大牛的博客和视频,以此作为笔记进行总结。本文介绍了java中判断无用对象的方法(引用计数法和跟搜索算法),并介绍了常用的垃圾回收算法和常用的垃圾回收器,这些知识笔者学习的一些总结和见解,不对的地方,希望大家能够指出来,希望能对大家的学习有所帮助。

猜你喜欢

转载自blog.csdn.net/Weixiaohuai/article/details/86559166