前言
这篇主要从GC算法、GC收集器以及哪些对象可GC等方面来记录。在这我不会详细的去阐述GC的细节,简单的记录我的一些疑惑和看法。
可达性分析、 引用计数法
首先第一个需要明白的问题就是哪些对象需要回收,在JVM中有两种方法,分别是可达性分析以引用计数法。
引用计数法
引用计数法给每一个对象配置一个引用计数器,如果有其他对象引用它,计数器就加1,当引用失效时,计数器减1,直到该对象的计数器为0为止,则可以清理。
引用计数法效率很高,但是存在些许问题,如不能解决循环引用问题、计数器需要占用对位。为了更好的理解引用计数法,先来看下面的Demo:
1 | public void main(String args[]){ |
要知道引用计数法是针对对象的,上面obj_0是存在帧栈中,只是一个引用,或者说是指向堆中对象的指针。这意味着对象new的时候,就有了一个引用。这个对象和obj_0不一样,它是存放在堆中的。当帧栈方法结束后存放在帧栈中的局部引用变量就会被回收,那么对象的引用就没有了,就会被回收。
可达性分析
通过一系列的GC Roots的对象作为起始点,从这些根节点开始向下搜索,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
可以作为GC Roots的对象包括以下几点:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中的类静态属性引用的对象或者常量引用的对象。
本地方法栈中JNI(就是native方法)引用的对象。
谈一下我的看法,首先GCRoots是引用的对象,并不是引用指针。那么它是如何来解决循环引用的问题呢?看下面的解析:
1 | public static void main(String[] args) { |
首先GC Roots是还存在引用的对象,step5,step6会导致栈帧中的本地变量表的引用失效,所以这个时候obj1实例
,obj2实例
不是能够引用到的对象,所以是不能够作为GC Roots的,这两个对象是会被回收的。
无论是可达性还是引用计数器都涉及到了引用的概念,为了使得对象回收更加的合理性,引入了四种引用来帮助垃圾回收。
四种引用
强引用
我的疑惑:强引用对象不会被回收吗?A a = new A()
。经常看到有人是这么描述强引用的:就算内存抛出异常。也不会回收强引用对象。这么说是没错,但是有一个前提就是强引用还存在的时候。
1 | public void main(String args[]){ |
原本的的看法是obj_0是不会被回收的,因为周志明的书上写强引用是不会被回收的,但是注意只有在强引用还存在的情况下才不会被回收!如果这个帧栈结束后,那么帧栈中指向堆中对象的引用就会被回收,那么强引用就不存在了,所以方法结束后,该对象会被回收。
软引用
先来说一下软引用的应用场景:当内存充足的时候,不会回收软引用对象,不足时则回收。这个情况不就和缓存场景一致嘛!缓存是将数据加载至内存中,加快这些数据的访问速度,但是你需要保证这些数据不会挤爆内存导致程序崩溃吧,所以这些缓存对象就可以使用软引用来做。更加具体的应用可以看这里:利用软引用实现高速缓存
从上面的描述大概就可以知道,软引用对象会在内存不足的时候被回收。
弱引用
弱引用是一种比软引用重要程度更低的一种引用。弱引用对象在下次GC的时候,无论内存是否足够,都会回收。
这里来讲一下它的应用场景:假如需要加载很多图片,图片名作为Key,value是图片的内容。如果是直接放在HashMap就会占用很大的内存,并且不会被回收。
1 | Bitmap bitmap = new Img(...); |
一种做法是可以使用weakHashMap,这个map中的key是一个指向bitmap对象的软引用指针。所以当你将bitmap = null
之后,key指向bitmap就是一个软引用了,当一个对象只有软引用的时候,它就会在下次GC的时候回收。
虚引用
对象销毁前的一些操作,比如说资源释放等。Object.finalize()
虽然也可以做这类动作,但是这个方式即不安全又低效。
介绍完引用的概念,再来介绍一下垃圾对象回收都有哪些策略。
垃圾回收算法
常用的GC算法有清除、复制、整理、分代收集算法。
标记-清除
最基础的垃圾收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。
两个不足点:效率低下以及标记清除之后会产生大量的不连续的内存碎片。(空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作)
复制算法
将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。
不足点:可使用的内存降为原来一半。
标记-整理
标记-整理算法在标记-清除算法基础上做了改进。
分代收集算法
根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
垃圾收集器
常用的垃圾收集器有如下几种:(下图每一条连线表示他们可以组合)
年轻代使用的收集器
- Serial 收集器是最基本、发展历史最悠久的收集器。是单线程的收集器。(复制算法)
- ParNew 收集器其实就是Serial收集器的多线程版本。(复制算法)
- Parallel Scavenge 收集器是一个新生代收集器,与ParNew一样都是多线程收集器,区别在于它对吞吐量是可控的。(复制算法)
上面三个是在年轻代使用的垃圾收集器,因为年轻代分为了Eden和S1,S2,而且大多数分配在年轻代的对象只有小部分会存活下来,所以可以将Eden和S1复制到S2,所以都采用的了复制算法。
老年代使用的收集器
- Serial Old 是Serial收集器的老年代版本,它同样是一个单线程收集器。(标记整理算法)
- Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在1.6中才开始提供。(标记整理算法)
说一下我的看法:为什么在老年代中不采用复制算法呢?第一它没有具备新生代的多个堆区的条件,s1,s2。如果要采用就需要把老年代划分成两个相等大小的,比较浪费空间。第二老年代大多数都是大对象,采用复制算法比较费时。所以它们采用了标记整理算法。
- CMS 收集器是一种以获取最短回收停顿时间为目标的收集器。(标记-清除)
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的。具体回收过程如下:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
我的第一个疑惑就是:为什么CMS不像上诉两个收集器一样采用标记整理算法呢?
我觉得需要从CMS的目标去解释,它的目标是获取最短的停顿时间,标记整理算法还需要一个整理的过程,使得它的回收速度变慢。
我的第二个疑惑:CMS目标是最短回收停顿时间,这个Parallel的目标可控的吞吐量有什么区别呢?
首先需要明白Parallel的吞吐量是怎么来的:
CMS是为了减少垃圾回收时间的这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。而GC的停顿时间缩短是以牺牲吞吐量和新生代空间换来的。系统把新生代调小一点:原来的500MB的新生代空间,每10秒收集一次,每次停顿100毫秒。现在变成了300MB,每5秒收集一次,每次停顿时间70毫秒。每次停顿时间是减少了,但是整体的吞吐量却下降了。
我的第三个疑惑:CMS的和Parallel的应用场景?
Parallel是可控吞吐量的,也就是吞吐量越高CPU执行时间程序的代码的时间就会越长,根据这个特性Parallel比较适用于需要尽快完成程序的任务。
CMS更实用于和用户交互比较频繁的,因为它的停顿时间短,所以用户体验会比较好。
我的第四个疑惑:CMS的后备预案是什么?
这个需要从CMS的一个不足点说起,CMS因为有一个并发清理的阶段,所以在清理的时候也会产生垃圾,所以不能在老年代满了再去回收,一般默认是在垃圾为堆大小的68%的时候才去回收。如果在回收的时候,预留的内存不够用户使用,则会出现“Concurrent Mode Failure”,虚拟机就会采用一次后备预案启用serial Old对老年代进行清理。
- G1收集器(Garbage-First):是当今收集器技术发展的最前沿的成果之一,G1是一款面向服务器端应用的垃圾收集器。
于CMS一样它的关注点在于降低停顿时间,不过G1一个大优势是可预测的停顿。G1可以指定一段时间,垃圾消耗不会超过这个时间。
G1大致的做法是将堆化分成了若干个Region,在每次回收的时候,会优先回收价值最大的Region。保证G1在有限的时间内可以尽可能高的收集效率。
默认情况下使用的垃圾收集器
在XXFOX下查看了JDK6、7、8下的GC参数可以看到 UseParallelGC 和 UseParallelOldGC 都是默认开启的。当使用UseParallelGC 的时候新生代老年代分别使用了Parallel Scavenge + Serial Old 。UseParallelOldGC 使得老年代的GC变成了Parallel Old 。
所以在默认的情况下Client、Server模式默认GC为:
新生代GC方式 | 老年代和持久代GC方式 | |
---|---|---|
Client | Serial 串行GC | Serial Old 串行GC |
Server | Parallel Scavenge 并行回收GC | Parallel Old 并行GC |