垃圾回收算法与垃圾回收器总结

概述

都知道Java是自动进行内存管理的,有自己的垃圾回收机制,那么具体Java是怎样进行垃圾回收的呢?本章就来总结一下Java中的垃圾回收器与垃圾收集算法。

回收算法之前提:如何判断对象需要进行回收

进行垃圾回收之前需要判断哪些对象是不用的,是需要进行回收的。其中最简单也是最耳熟能详的判断方法就是引用计数法。顾名思义,一个对象有一个地方引用,该对象引用计数器的值就加1,当不需要引用的时候计数器的值则减1,当计数器的值为0时则代表没有别处引用该对象,该对象可以进行回收。然而这种方法正因为简单所以存在比较明显的缺点:无法回收互相引用的对象。即俩个对象互相引用,计数器的值均为1,然而当这两个对象都是无用对象时却无法对其进行回收,导致内存泄露。

第二个方法是大部分虚拟机所采用的可达性分析算法。主要原理是通过判断对象引用链能否和虚拟机规定的GC Root对象连接。对象引用链就好像链表:a引用b,b引用c 当abc均没有能和Root对象相连的引用链时,即可对abc进行回收。那么Root对象选取的标准是什么呢?首先必须是程序运行阶段会一直存在的对象,不然你自己都需要垃圾回收还怎么判断别人。。所以可以作为Root对象的包括以下几种:

1.虚拟机栈中本地变量表引用的对象,注意这里并不是栈帧中的局部变量表,因为局部变量表在方法退出后随着栈帧的销毁而销毁。

2.方法区(1.8改为元数据区)中类静态属性引用的对象

3.方法区中常量引用的对象

4.Native方法(非Java代码的方法)引用的对象


总的来说不管是引用计数还是可达性分析都跟对象引用息息相关。接下来介绍Java中的几种引用类型。Java中一共定义了4中类型的引用(不过平常用到的基本只有强引用),他们的引用强度由强到弱分别为:

1.强引用:即平常 new xxx()所生成的对象即为强引用。只要强引用存在,该对象就不会被回收。

2.软引用:使用 SoftReference 实现,他的使用和LIst差不多,使用 T t = soft.get() 得到存放在软引用中的对象。当虚拟机没有足够内存,将要发生内存溢出时会对其进行回收,利用这一特性可以用来做内存缓存。定义一个Map,value为SoftReference类型,实现一个自动管理的内存Map,不过该方式也不常用一般都是使用外部缓存的方式实现,例如Redis。

3.弱引用:WeakReference实现,用法类似于软引用,但被他引用的对象只能生存到下一次垃圾回收之前。

4.虚引用:PhantomReference实现,调用虚引用的get()方法会返回null,所以木有办法通过他获取到一个对象实例。一般用于和ReferenceQueue一起使用完成对象垃圾回收前的对象清理工作,一个对象enqueue则代表该对象已经被GC了。


除了对象,没有人引用的字符串常量和类也需要进行回收。判断类是否能够被回收的条件有三个:堆中已经没有该类的实例,该类的CLassLoader已经被回收,该类的Class对象没被引用。能够卸载的类必须是自定义类加载器加载的类,而Java本身的三大类加载器加载的类是不能被卸载的,关于类加载器的问题以后在总结吧。。。。

垃圾回收算法

1. 标记清除:这个是比较简单的回收算法,先对要回收的对象进行标记,然后进行统一删除。缺点就是当存活的对象内存地址不是连续的时候会出现大量的内存碎片,导致分配大对象的时候没有足够的内存而不得不频繁触发垃圾回收。该算法是CMS垃圾收集器的基础。


2. 复制算法:先进行标记操作,然后将存活的对象复制到另一块内存上再对其在进行清除。因为新生代对象大部分都是活不长的。。所以新生代的回收算法一般采用复制算法。将Eden 和From Surviour中存活的对象复制到To Surviour。第二次回收的时候To区和Surviour会调换名字,直到To区的对象达到一定的条件晋升老年代。该算法被新生代垃圾回收器使用。


3. 标记整理:先进行标记操作,然后将存活的对象移到一起对剩下的内存进行统一清除,避免了标记清除的缺点。该算法被老年代垃圾回收器所使用。

垃圾收集器

1. Serial系列:单线程,意味着回收的时候不能进行用户操作。

    分为Serial新生代与Serial老年代。一般服务器应用好像没人用。。所以了解一下就可以了


2. Parallel系列

    分为Parallel新生代与Parallel老年代。是吞吐量优先的垃圾回收器。一般电脑上默认安装的JDK就是使用了该收集器。


3. ParNew: Serial新生代的多线程版本即多条线程同时进行垃圾回收,Tomcat默认的新生代回收器就是他。


4. CMS:Concurrent Mark Sweep(并发标记清除),是获取最短回收停顿时间的收集器,用户线程和回收线程可以同时工作。Tomcat默认老年代收集器就是他。

由于他是大部分服务端应用的主流回收器还是写详细些吧:回收主要分为4个阶段,初始标记,并发标记,重新标记,并发清除。。每个阶段干什么了看名字应该很清楚了。由于是和用户线程并发运行,在回收的时候需要预留一部分内存给用户线程,所以要设置老年代内存达到多大比例进行垃圾回收。

虽然他是主流的收集器,但有如下缺点:1.对cpu资源敏感,应用程序分配一部分cpu资源去进行垃圾收集导致用户代码响应速度变慢。2.无法处理并发清除时所产生的垃圾,只能留到下一次回收。3. 如上标记清除的缺点问题。CMS可以设置相应参数在FullGC时进行内存碎片的整理。


5. G1:可以进行垃圾回收时间的预测,设定回收时间不超过该值。不同于其他收集器的是他自己就可以管理新生代与老年代,不需要同其他回收器合作。他将整个堆划分为多个Region,G1会跟踪每个Region回收的价值,每次回收时根据价值列表与预测的回收时间回收相应Region。他的回收与CMS基本相同,分为初始标记,并发标记,最终标记与最终的筛选回收4个阶段

6.ZGC:它是JDK 11中的垃圾回收器,是比G1更加优秀的垃圾回收器。ZGC承诺:回收的暂停时间不会超过10ms,暂停时间不会随着堆容量的增加而增加,能够处理从MB到TB大小的堆空间。ZGC主要有如下特点,并发执行,同G1一样基于Region,能够处理内存碎片问题,使用了“彩色指针”和加载屏障(这俩个术语我也不懂,这里就摘抄R大的一段话作为解释与标记对象的传统算法相比,ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World】)。

关于ZGC更详细的介绍这里就贴一篇公众号文章,江南白衣大大写的,很赞,点我查看。

关于Minor GC,Major GC,FULL GC

这里就直接拷贝R大的一段回答吧。。写的很好,受益匪浅。

作者:RednaxelaFX

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

Partial GC:并不收集整个GC堆的模式

    Young GC:只收集young gen的GC

    Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式

    Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式

Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。

最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:

young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。

full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。


后记:关于对象晋升老年代的问题

最后说一下对象晋升的问题,什么情况下对象会晋升至老年代?

1. 大对象,当对象大于虚拟机参数设置的之时会直接进入老年代

2. 在新生代经历了多次的GC(每一次GC对象年龄加1,默认为15)依然存活的对象直接进入老年代。

3. 若Surviour中相同年龄所有对象大小超过Surviour的一半,对象年龄大于等于该年龄的直接进入老年代。该行为称作动态年龄判断。

你可能感兴趣的:(垃圾回收算法与垃圾回收器总结)