JVM系列(2)——垃圾回收

一、什么是GC

垃圾回收(Garbage Collection,GC):释放垃圾占用的空间,防止内存泄露。有效的使用内存,对已经死亡的或者长时间没有使用的对象进行清除和回收。

二、GC发生在哪里

想了解GC发生在哪里,就一定要知道jvm内存区域,详情请参考JVM系列(1)——java内存区域。
我们简单陈述下:
(1)程序计数器:占用内存小,线程私有的,记录的是正在执行的虚拟机字节码指令的地址,只记录执行到哪里了,方法或者线程结束后,内存自然随之回收。
(2)虚拟机栈&&本地方法栈:每个栈帧中包含局部变量表、操作数栈、动态链接、方法出口等,局部变量表存放基本数据类型、对象引用类型(引用指针)等信息,因为是线程私有的,方法或者线程结束后,内存自然随之回收。
(3)方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。GC在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。是线程共享区域,内存分配和回收是动态的。
(4):是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。是垃圾收集器管理的主要区域。
总上所述,堆和方法区因为是线程共享的,而堆所占内存又极大,所以堆是GC的主要区域。

三、判断对象已"死"

对象已死吗?这个问题也可以说成:哪些对象可以被回收?其实很好回答,不用的垃圾,就要扔掉(回收)了。
在java中,怎么判断对象没用了呢,在GC里面有两种算法来判断,一种是引用计数算法,对象引用的次数为0就是垃圾,另一种是可达性算法,如果一个对象不在以GC Root根节点为起点的引用链中,则视为垃圾。

3.1 引用类型

在谈以上两种算法之前,我们先说一下引用是什么。
引用的定义:reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。引用分为强引用、软引用、弱引用、虚引用。
(1)强引用:GC时,永远不会被回收。程序中普遍存在的一种引用 Object object = new Object()。如果内存不足,JVM会抛出OOM错误也不会回收object指向的对象。强引用是引起OOM的主要原因。
如果方法或者线程运行完之后,object已经不存在了,也就是说引用没有了,所以它指向的对象会被JVM回收。
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。比如Vector类的clear方法中就是通过将引用赋值为null来实现清理工作的。
如图所示:
JVM系列(2)——垃圾回收_第1张图片

(2)软引用:有用但不是必需的对象时,例如缓存,就可以使用软引用。只要内存空间足够,软引用对象就不会被回收,将要发生内存溢出异常之前,会将软引用的对象回收。如果回收之后还没有足够的内存的话,就会抛出OOM。
在这里插入图片描述

应用场景举例:有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘中读取则会严重影响性能,如果一次全部加载到内存中又可能会造成内存溢出。这时候利用软引用存储图片的引用,就能做到及时回收。
(3)弱引用:弱引用也是用来描述非必需对象的,它的强度比软引用还要弱,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
在这里插入图片描述
参考:理解Java中的弱引用(Weak Reference)
应用场景举例:
现在有一个Product类代表一种产品,这个类被设计为不可扩展的,而此时我们想要为每个产品增加一个编号。一种解决方案是使用HashMap
于是问题来了,如果我们已经不再需要一个Product对象存在于内存中(比如已经卖出了这件产品),假设指向它的引用为productA,我们这时会给productA赋值为null,然而这时productA过去指向的Product对象并不会被回收,因为它显然还被HashMap中的key引用着。所以这种情况下,我们想要真正的回收一个Product对象,仅仅把它的强引用赋值为null是不够的,还要把相应的条目从HashMap中移除。根据前面弱引用的定义,使用弱引用能帮助我们达成这个目的。我们只需要用一个指向Product对象的弱引用对象来作为HashMap中的key就可以了。

Product productA = new Product();
WeakReference weakProductA = new WeakReference<>(productA);
HashMap = new HashMap<>();

获取productA 对象方法:

Product product = weakProductA.get();

实际上,对于这种情况,Java类库为我们提供了WeakHashMap类,使用和这个类,它的键自然就是弱引用对象,无需我们再手动包装原始对象。
(4)虚引用:虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,而直接内存是在堆内存之外,所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作。

3.2 引用计数算法

对象被引用时就会在此对象的对象头上计数器加一,每当有一个引用失效时计数器的值减一,如果没有引用(引用次数为0)则此对象可回收。
但是这种算法很难解决对象之间互相循环引用的问题,如果对象之间相互引用,它们的引用计数不会为0 。但是java虚拟机中并不会因为对象循环引用而不去回收它们,所以虚拟机不是通过引用计数算法来判断对象是否存活的。

3.3 可达性分析算法

为了解决上述问题,引入了可达性分析算法。通过一系列被称为“GC Roots”的点作为起始点,从这些节点开始向下搜索,搜索的路径称为引用链,当一个对象到GC Roots不可达的时候,则证明此对象是可回收的。
JVM系列(2)——垃圾回收_第2张图片
如下图,从GC Root不能到达对象567,所以这三个对象是不可用的。
可做GC Roots的对象有 虚拟机栈中引用的对象(本地变量表)、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的对象(Native对象)。

3.4 finalize()方法

是Object类的一个方法,因为所有的类都继承自Object类,所以所有的类都有finalize方法。
真正宣告一个对象进入死亡之前,会进行两次标记:第一次是可达性分析之后,发现可以回收,会进行标记;第二次是先进行是否有必要调用finalize()方法的筛选,筛选规则:有没用重写finalize()方法。第二次执行finalize()方法,判定为没有必要调用。
若需要调用,则进行第二次标记,然后进行对象回收。
强烈不建议使用此方法,不确定性太大,无法保证对象调用顺序。

四、垃圾回收算法

在介绍算法之前,我们看一下HotSpot的堆模型,参考文章: JVM系列(3)——内存分配与回收策略。

4.1标记—清除算法

(1)先标记、后清除。标记在上文中已经提到了。
(2)缺点:标记与清除效率低下;会产生大量内存碎片,内存不够连续。可能导致内存充足,但是大对象无法存储的情况。
JVM系列(2)——垃圾回收_第3张图片

4.2 复制算法

(1)将内存划分为相等的两块区域A和B,一次只用其中一块A,当需要垃圾回收时,将A中所有存活的对象复制到B,然后清除A,使用B。就这样周而复始。
(2)运行高效,实现简单,但是内存缩小为原来的一半,代价太高。
JVM系列(2)——垃圾回收_第4张图片
HotSpot虚拟机的新生代中使用了此方法,可以看JVM系列(3)——内存分配与回收策略。

4.3 标记—整理算法

(1)先标记,再整理(让所有存活的对象都向一端移动),最后清除。
(2)标记整理算法对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。
JVM系列(2)——垃圾回收_第5张图片

4.4 分代收集算法

当前商业虚拟机中采用的算法,分代收集算法是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳,根据对象存活周期的不同将内存划分为几块。参考:JVM系列(3)——内存分配与回收策略。
(1)在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
(2)在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理算法或者标记-整理算法来进行回收。

五、垃圾回收器

垃圾回收器是内存回收的具体实现。
JVM系列(2)——垃圾回收_第6张图片

(1)Serial收集器是最古老的收集器,它的缺点是当Serial收集器想进行垃圾回收的时候,必须暂停用户的所有进程,即stop the world。Serial收集器由于没有线程交互的开销,专心做垃圾回收自然可以获得最高的单线程收集效率。
(2)Pernew(新生代、多线程,Serial收集器的多线程版本(使用多条线程进行GC)。它是运行在server模式下的首选新生代收集器,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
(3)ParNew Scanvenge(新生代、多线程,类似ParNew,但更加关注吞吐量。
停顿时间和吞吐量不可能同时调优。在GC的时候,垃圾回收的工作总量是不变的,如果将停顿时间减少,那频率就会提高;既然频率提高了,说明就会频繁的进行GC,那吞吐量就会减少,性能就会降低。
(4)CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。
CMS收集器运行过程:(着重实现了标记的过程)

(1)初始标记
(2)并发标记(和用户线程一起运行)
(3)重新标记
(4)并发清除(和用户线程一起运行)

初始标记和并发标记是要SWT的。缺点也很明显:

(1)占用资源,导致用户的执行速度降低。
(2)无法处理浮动垃圾。因为它采用的是标记-清除算法。用户线程在运行时会不断产生新的垃圾,有可能有些垃圾在标记之后,需要等到下一次GC才会被回收。如果CMS运行期间无法满足程序需要,那么就会临时启用Serial Old收集器来重新进行老年代的收集。
(3)由于采用的是标记-清除算法,那么就会产生大量的碎片。往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次full GC。

(5)G1回收器:并行并发执行,分代收集,且结合标记——整理和复制算法,能够预测停顿。过程如下:

(1)初始标记
(2)并发标记
(3)最终标记
(4)筛选回收

G1回收器对垃圾回收进行了划分优先级的操作,这种有优先级的区域回收方式保证了它的高效率。

你可能感兴趣的:(java,jvm,java)