深入理解Java虚拟机 | 第三篇:垃圾收集器与内存分配策略

说起垃圾收集(Garbage Collection,GC),大部分人都把这项技术当做Java语言的伴生产物。事实上,GC的历史比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。

从上一篇文章中我们得知,程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,这部分内存的分配和回收基本都是确定的。但是Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

一:如何确定一个对象是否存活

1、引用计数法:在Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,则说明对象不太可能再被用到,那么这个对象就是可回收对象。这种方式即是引用计数法。但是这个方法有一个缺点就是无法解决循环引用的问题。

2、可达性分析:为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

二:典型的垃圾回收算法

1、标记清除算法:最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间(该算法的缺点就是内存碎片化严重)。如图:


2、复制算法:为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉(缺点是内存利用效率不高,存活对象多的时候效率比较低),如图:


3、标记压缩算法:结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端,然后清除端边界外的对象。


4、分代收集算法:分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

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


三:垃圾收集器

垃圾收集算法是垃圾收集器的理论基础,而垃圾收集器就是其具体实现。下面介绍HotSpot虚拟机提供的几种垃圾收集器。

Serial/Serial Old:最古老的收集器,是一个单线程收集器,用它进行垃圾回收时,必须暂停所有用户线程(Stop-The-World)。Serial是针对新生代的收集器,采用Copying算法;而Serial Old是针对老生代的收集器,采用Mark-Compact算法。优点是简单高效,缺点是需要暂停用户线程。


ParNew:Seral/Serial Old的多线程版本,使用多个线程进行垃圾收集。


Parallel Scavenge:新生代的并行收集器,回收期间不需要暂停其他线程,采用Copying算法。该收集器与前两个收集器不同,主要为了达到一个可控的吞吐量。

Parallel Old:Parallel Scavenge的老生代版本,采用Mark-Compact算法和多线程。


CMS:Current Mark Sweep收集器是一种以最小回收时间停顿为目标的并发回收器,因而采用Mark-Sweep算法。


G1:G1(Garbage First)收集器技术的前沿成果,是面向服务端的收集器,能充分利用CPU和多核环境。是一款并行与并发收集器,它能够建立可预测的停顿时间模型。


四:内存分配策略

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

1、对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
2、大对象(需要大量连续内存空间的Java对象)直接进入老年代,比如长字符串或者数组
3、长期存活的对象将进入老年代:如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。
4、但是如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就会可以直接进入老年代。
5、空间分配担保:在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

五:一个Java对象的一生

我是一个普通的java类文件,有一天Classloader找到了我,他找到我的时候首先我听见他打电话给他上司问有没有见过我(首先看有没有加载过),他上司给他一个否定的答案后,他就来跟我说有人需要我,要我汇报一下我文件开头和结尾的数(class文件规范)。我汇报了之后问Classloader你如何找到我的,他跟我说有人需要我,然后从目录列表上发现了我的地址,然后就过来了。他好像有点急的样子,开始对我进行检查(常量池,访问标识,字段,方法...),,发现我没问题之后,然后把我带去了虚拟机Eden区(新对象诞生的地方),然后不知道对我做了什么(加载)。我看到附近很多跟我一样的小兄弟,我们都住在一个一个的房间里面(分配内存空间)。就这样平静的生活着,突然有一天一双大手过来把好多小伙伴都抓走了(GC),但是没有抓我,因为我给好多人还干着活呢(还被引用)。我住的这地方人越来越多,后来我就搬到了From Survivor区居住了。这段时间居无定所,我每年都得搬一次家,从From Survivor到To Survivor(复制算法),我就这样长到了15岁(jvm默认参数15次GC),由于我干的优秀,被奖励了一套老年区别墅,我发现这地方的人挺多了,并且年纪都比较大了。我在这里平静的生活了几年,然后寿终正寝了,结束了光辉短暂的一生。

猜你喜欢

转载自blog.csdn.net/qq_38455201/article/details/80913727