java垃圾收集算法与垃圾收集器

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

前言

前面一篇记录jdk新特性的时候想必也看到很多关于垃圾收集器的改进,毕竟这块对虚拟机的性能提升还是很重要的,这篇就记录下关于虚拟机的垃圾收集过程。

垃圾收集说的直白点就是内存的回收,那我们首先得明确这里的内存回收主要针对的还是堆中的内存,因为程序计数器及栈这两个区域随线程而生,随线程而灭,我们不需要考虑过多的关于内存回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。

如何判断对象是否存活?

垃圾收集器在对堆进行回收前,首先就是需要确定哪些对象还"存活",哪些已经"死去"(即不可能再被任何途径使用的对象),大致上来说有两种算法:

1、引用计数算法

这也是很多教科书中判断对象是否存活的算法:给对象添加一个引用计数器当该对象被引用时,计数器加1;当引用失效时,计数器减1,任何时刻计数器为0的对象就是不可能再被使用的。该算法实现简单,判定效率也挺高,主要问题就是不能解决对象之间相互循环引用的问题。举个例子:

假设对象A和B都存在某个某个obj字段t,A.t = B,B.t = A,A = null, B = null 除此之外这两个对象再无其他引用,也就意味着这两个对象不可能再被访问,然而根据引用计数算法,其引用计数器并不为0,于是引用计数器算法无法通知GC收集器来回收它们。

2、可达性分析算法

这是绝大部分主流语言所采用的算法,其主要过程为:通过一系列的称为“GC Roots”的对象作为起始点,开始向下搜索,搜索走过的路径称为引用链,当一个对象到达GC Roots没有任何引用链相连时(即GC Roots到这个对象不可达时),则证明此对象是不可用的。对照上述概念看下图即可明白,对象o5,o6,o7虽然之间互相引用,但是他们到达GC Roots是不可达的,因此被判断为可回收的对象:

那么在java语言中,可作为GC Roots的对象包括下面几种:

扫描二维码关注公众号,回复: 6010363 查看本文章

a、虚拟机栈(栈帧中的本地变量表)中所引用的对象

b、方法区中类静态属性和常量引用的对象

c、本地方法栈中JNI(即一般说的Native方法)引用的对象

对象死亡标记过程

即使对象在可达性分析算法中被判定为不可达时,也未必说是“非死不可”的,要真正宣告一个对象死亡,至少要经历两次标记过程

1、当被可达性分析算法判定不可达时对象将会被第一次标记并且进行一次筛选,筛选的条件时此对象是否有必要执行一次finalize()方法,如果该对象没有覆盖finalize方法或者说虚拟机已经调用过(任何一个对象的finalize()方法都只会被系统自动调用一次),这两种情况都会被虚拟机视为“没有必要执行”,直接回收。

2、当被视为有必要执行finalize()方法时,虚拟机将会把这个对象放在一个叫F-Queue的队列之中,稍后GC将会对队列中的对象进行第二次小规模的标记,这也是逃脱回收命运的最后一次机会:只要该对象重新与引用链上的任何一个对象建立关联即可,譬如把自己赋值给某个类变量或者对象的成员变量,那么在第二次标记时它就会被移除队列,完成自救。

垃圾收集算法

主要有三种算法思想,标记-清除,复制,标记-整理,有时候常听到的分代收集算法并不是一种新的算法,只是针对新生代和老年代采取不同的算法而已。

1、标记-清除(Mark-Sweep)

这是最基本的一种算法,正如其名字所示,整个过程分为”标记“和“清除”两个阶段:首先标记出需要回收的所有对象,然后统一回收所有被标记的对象,标记过程即为上文所说。该算法主要存在两个不足:

a、效率问题,标记和清除两个过程的效率都不高

b、空间问题,清除之后会产生大量的空间内存碎片,导致之后分配较大对象时找不到足够大的连续内存而不得不提前触发一次垃圾收集动作

之所以称它为是最基本的算法,是因为后两个算法都是为了改进它的不足而产生的。其主要执行过程如下图所示:

2、复制(Copying)

为了解决效率问题,一种称为复制的收集算法出现了,其基本思想就是将内存分为两块,每次只使用其中一块,当这一块内存用完时,就将仍存活的对象复制到另一块内存中,并清理前一块内存。复制算法实现简单,运行高效,还不会产生空间碎片,只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。复制算法执行过程如下图所示:

由于新生代中的对象大多“朝生夕死”,因此在新生代的垃圾收集大多采用复制算法。

新生代复制算法内存回收详细过程:

HotSpot将新生代的内存分为三块,一个Eden区和两个Survivor区(From和To),每次只使用Eden区和名为“From”的Survivor区,Survivor区“To”是空的。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理,最典型的为那种很长的字符串以及数组),这些对象在开始Minor GC(新生代的内存回收)后,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(默认为15岁,每经过一次Minor GC,年龄就会增加1岁)的对象会被移动到老年代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和“From”区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为“To”的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

上文所说的这三块区域比例大致为Eden:from:to = 8:1:1,也就是说每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被”浪费“。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用,这些对象将直接通过分配担保机制进入老年代。

小故事:一个对象的一生

我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我15岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

3、标记-整理算法(Mark-Compact)

复制收集算法在对象存活率较高时就要进行较多的复制操作,其效率将会变低,因此在老年代一般不使用该算法,根据老年代的特点,自然而然出现了一种标记-整理算法,其大致过程与标记-清除类似,先标记,只是后续并不是直接对可回收对象进行清除,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,过程如下:

垃圾收集器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。由于虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,这里列出的收集器是基于jdk1.7的HotSpot虚拟机。建议主要了解下最初的Serial、CMS及G1就好:

1、Serial收集器

这是最基本、发展历史最悠久的收集器,看名字也能知道这是一个单线程的新生代收集器,但“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束,也就是我们常说的“Stop the world”

2、ParNew收集器

这是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余的与Serial一模一样。

3、Parallel Scaveng收集器

这是一个新生代收集器,是jdk1.7和1.8默认的新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,Parallel Scaveng收集器的特定是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而它的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间),假设虚拟机总共运行了100分钟,其中垃圾收集花了1分钟,那吞吐量就是99%。

4、Serial Old收集器

是Seriabl收集器的老年代版本,同样是一个单线程收集器。

5、Parallel Old收集器

是Parallel Scaveng收集器的老年代版本,使用多线程和标记-整理算法,jdk1.6之后才开始提供,之后便于Parallel Scaveng收集器搭配使用,“吞吐量优先”组合正式出现,是jdk1.7和1.8默认的新生代收集器。

6、CMS收集器(Concurrent Mark Sweep)

是一种以获取最短回收停顿时间为目标的收集器,从名字上就能发现,这是基于标记-清除算法实现的,同时也是HotSpot虚拟机中第一款真正意义上的并发收集器整个过程大致分为:

初始标记 / 并发标记 / 重新标记 / 并发清除

7、G1收集器(Garbage-First)

是一款面向服务端应用的垃圾收集器,也是jdk1.9默认的垃圾收集器,其并行与并发技术仍旧能达到低停顿的效果,G1从整体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)来看是基于复制算法实现的。但无论如何这两种算法都意味着G1运行期间不会产生内存空间碎片。G1收集器的另一大优势即为能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

如何查看当前默认的垃圾收集器?

控制台输入:

java -XX:+PrintCommandLineFlags -version

输出结果为:

图中圈红的即代表使用了哪种默认收集器。

长路漫漫,砥砺前行

猜你喜欢

转载自blog.csdn.net/m0_38001814/article/details/88910263