jvm - 垃圾回收

in 编程
关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9

jvm - 垃圾回收

注意 : 本系列文章为学习系列,部分内容会取自相关书籍或者网络资源,在文章中间和末尾处会有标注

垃圾回收的意义

它使得java程序员不再时时刻刻的关注内存管理方面的工作.

垃圾回收机制会自动的管理jvm内存空间,将那些已经不会被使用到了的"垃圾对象"清理掉",释放出更多的空间给其他对象使用.

何为对象的引用?

Java中的垃圾回收一般是在Java堆中进行,因为堆中几乎存放了Java中所有的对象实例

在java中,对引用的概念简述如下(引用强度依次减弱) :

如下为相关示例代码

public class ReferenceDemo {
    public static void main(String[] arge) {
        //强引用
        Object object = new Object();
        Object[] objects = new Object[100];

        //软引用
        SoftReference<String> stringSoftReference = new SoftReference<>(new String("SoftReference"));
        System.out.println(stringSoftReference.get());
        System.gc();
        System.out.println(stringSoftReference.get()); //手动GC,这时内存充足,对象没有被回收

        System.out.println();

        //弱引用
        WeakReference<String> stringWeakReference = new WeakReference<>(new String("WeakReference"));
        System.out.println(stringWeakReference.get());
        System.gc();
        System.out.println(stringWeakReference.get()); //手动gc,这时,返回null,对象已经被回收

        System.out.println();

        //虚引用
        //虚引用主要用来跟踪对象被垃圾回收器回收的活动。
        //虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。
        //当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中
        ReferenceQueue<String> stringReferenceQueue = new ReferenceQueue<>();
        PhantomReference<String> stringPhantomReference = new PhantomReference<>(new String("PhantomReference"), stringReferenceQueue);
        System.out.println(stringPhantomReference.get());
    }
}

当然,关于这几种引用还有很多知识点,本文只做简单的介绍,后续有机会再单独的文章详细介绍.

如何确定需要回收的垃圾对象?

引用计数器

每个对象都有一个引用计数器 , 新增一个引用的时候就+1,引用释放的时候就-1,当计数器为0的时候,就表示可以回收

引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的选择,当Java语言并没有选择这种算法来进行垃圾回收,主要原因是它很难解决对象之间的相互循环引用问题

public class LoopReferenceDemo {
    
    public static void main(String[] args) {
        TestA a = new TestA(); //1
        TestB b = new TestB(); //2
        a.b = b; //3
        b.a = a; //4
        a = null; //5
        b = null; //6
    }
    
}

class TestA {
    public TestB b;

}

class TestB {
    public TestA a;
}

虽然a和b都为null,但是a和b存在循环引用,这样a和b就永远不会被回收

如果你在互联网上搜索"引用计数器"这个关键字,通常都会得到以上这一个结论,但是究竟为什么a和b不会被回,收其实还是没有说清楚的,下面简单说明一下 :

内存分布如下图

引用计数器示例-1

内存分布如下图

引用计数器示例-2

以上的知识点参考 : https://www.zhihu.com/question/21539353

可达性分析

虽然以上的"引用计数器"算法存在"循环引用"的问题,不过目前主流的虚拟机都采用"可达性分析(GC Roots Tracing)"算法来标记那些对象是可以被回收的.

该算法是从GC Roots开始向下搜索,搜索走过的路径称之为引用链.当一个对象到GC Roots没有任何引用链相连时,就代表这个对象是不可用的.称为"不可达对象"

GC Roots包括:

实际上,在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程 :

如下图所示

可达性分析

从上图上看,reference1,2,3都是gc roots

reference1指向instance1,reference2指向instance4,并且instance4又指向了instance6,reference3则指向了instance2

所以说instance1,2,4,6都具有gc roots可达性,是存活着的对象,不会被垃圾回收器回收掉

而instance3,5则不具备gc roots可达性,是不可用对象,将会被垃圾回收器回收掉

从上图描述"引用计数器"的图例场景来看,TestA和TestB虽然互相有持有引用,但是并不具备gc roots可达性,所以,在"可达性分析"算法下,是会被垃圾回收器回收掉的

垃圾收集的算法

标记-清除 算法

算法分为"标记"和"清除"两个阶段,首先标记出需要回收的对象,在标记完成后,统一回收掉之前被标记的所有对象. 它是最基础的收集算法 . 后续的收集算法都是基于这种思想,并且对其缺点进行改进而产生的

标记-清除

主要缺点:

复制 算法

将可用内存按容量划分为两块,每次只使用其中的一块,当内存使用完了后,就将还存活着的对象复制到另外一块上面,然后在把前面一块内存一次性清理掉

复制

优点 :

缺点 :

标记-压缩 算法

和标记-清除算法一样,只不过标记后的动作不是清除,而是将所有对象向一端移动,然后直接清理掉边界以外的对象(被标记的对象)

标记-压缩

特点 :

分代收集 算法

把java的堆分为"新生代"和"老年代",对于不同的年代采用不同算法

分代管理

在新生代中,由于对象生命周期非常短暂,所以每次垃圾回收的时候都会有大量的对象死去,只有少量存活,这样,采用"复制算法",就只需要付出少量存活对象的复制成本,就能完成回收

在老年代中,由于对象生命周期比较长,存活率较高,没有额外的空间对它进行分配和担保,那就必须使用"标记-清除算法"或者"标记-压缩算法"来进行回收

Minor GC: 从年轻代空间(包括Eden和Survivor区域)回收内存被称为Minor GC

Major GC: 清理老年代

Full GC: 清理整个堆空间—包括年轻代和老年代

年轻代: 是所有新对象产生的地方.年轻代被分为3个部分(Enden区和两个Survivor区,也叫From和To),当Eden区被对象填满时,就会执行Minor GC,并把所有存活下来的对象转移到其中一个survivor区(Form),Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区(To),这样在一段时间内,总会有一个空的survivor区,经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间,常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的,需要注意,Survivor的两个区是对称的,没先后关系,from和to是相对的.

老年代: 在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中,都是生命周期较长的对象.对于年老代,则会执行Major GC,来清理.在某些情况下,则会触发Full GC,来清理整个堆内存

元空间: 堆外的一部分内存,通常直接使用的是系统内存,用于存放运行时常量池,等内容,垃圾回收对应元空间来说没有明显的影响

垃圾收集器

垃圾收集器是内存回收算法的具体实现,Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别

Sun HotSpot虚拟机1.6版包含了如下收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old

这些收集器以不同的组合形式配合工作来完成不同分代区的垃圾收集工作,如下是垃圾收集器简单介绍 :

Serial收集器

串行收集器,最古老,最稳定,以及效率高的收集器,但是可能会造成程序较长时间的停顿,只使用一个线程去回收.新生代,老年代使用串行回收

新生代使用"复制算法"

老年代使用"标记压缩算法"

垃圾回收的过程中会"程序暂停"(Stop the world)

ParNew收集器

是Serial收集器的多线程版,新生代并行,老年代串行

新生代使用"复制算法"

老年代使用"标记压缩算法"

垃圾回收的过程中会"程序暂停"(Stop the world)

Paralle收集器

类似于ParNew收集器,但是更关注系统的吞吐量.

可以通过参数来打开"自适应调节策略",虚拟机会根据系统当前的运行情况收集性能监控信息,动态调整这些参数以便提供最合适的停顿时间和最大的吞吐量

也可以通过参数控制GC的时间不大于多少毫秒或者比例

新生代使用"复制算法"

老年代使用"标记压缩算法"

Parallel Old收集器

是Paralle收集器的老年代版本 , 使用多线程和"标记-整理算法",这个收集器在JDK1.6中才开始使用

CMS收集器

是基于"标记-清除"算法实现的,它的运作过程相对于前面的其中收集器要复杂一些,整个过程分为4个步骤,包括 :

初始标记和并发标记仍需要Stop the World.

初始标记仅仅只是标记一下GC Root能直接关联到的对象,速度很快.

并发标记阶段就是进行GC Root Tracing的过程.

重新标记这是为了修正并发标记期间,因用户程序继续运作而导致标记变动的那一部分的标记记录,这一阶段的停顿时间会比初始标记阶段的时间稍长一些,但远比并发标记时间短

整个过程中耗时最长的并发标记和并发清除过程中,收集器线程可以与用户线程一起工作,所以总体来说,CMS收集器的内存回收是与用户线程一起并发执行的

优点 : 并发收集,低停顿

缺点 : 产生大量的空间碎片,并发阶段会降低吞吐量

G1收集器

与CMS收集器项目,G1收集器有以下特点 :

上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不在是这样.

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

G1的新生代收集器跟ParNew类似,当新生代占用达到一定的比例的时候,开始触发收集

和CMS类似,G1收集器收集老年代对象的时候会有短暂停顿

收集步骤如下 :

参考文献

<<深入理解JVM虚拟机>>

结束

本文提到的点很多,有对象引用,如何定义垃圾对象,gc算法,现有的垃圾收集器,等.

由于篇幅和时间原因,每个点都提及的不深入(当然,本篇文章的每个点深入的聊起来,都够写本书的了,呵呵).

后续会找机会逐个的将这些点跟大家深入的讨论.

总之 "学无止境" , 与大家共勉 .

代码仓库 (博客配套代码)


想获得最快更新,请关注公众号

输入图片说明

关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9
扫一扫关注公众号添加购物返利助手,领红包
Comments are closed.

推荐使用阿里云服务器

超多优惠券

服务器最低一折,一年不到100!

朕已阅去看看