在虚拟机对对象的回收过程中,哪些对象会被回收?什么时候回收?如何回收?或许你已有答案,或许你很好奇,那就来看一看吧。本篇文章会先对这三个问题进行解释,在文章最后聊一聊垃圾收集器和内存分配策略
01
哪些对象会被回收?
引用计数 可达性分析
为什么需要回收呢?举个例子:如果家里面有一些垃圾,我们通常会把它放入到垃圾桶中(还没有扔掉,只是放入桶中,有可能会捡出来继续用,虚拟机中也是一样,有一次"起死回生"的机会)。在Java中,一些不再被引用的对象就是"垃圾",所以不再被引用的对象会被回收。那么如何判断一个对象是否被引用呢?
通过引用计数和可达性分析来判断对象是否被引用。
扩展:
引用计数实现简单,获取对象被引用的数目,如果为0则判断没有引用。这种方法判定效率高,大部分时候都是可用的,但是当前主流的JVM不会用引用计数:
1.因为它很难解决对象之间循环引用的问题(A.instance = B;B.instance = A。因为相互引用的问题,引用计数的方法无法通知GC收集器回收)
2.对于强软弱虚引用,引用计数没法分析
可达性分析的思路是通过一系列叫做"GC Roots"的对象作为起始点,向下检索,当一个对象到GC Roots没有任何引用链相连,也就是对象不可达时候,证明该对象不可用。在Java中:哪些对象可以作为GC Roots对象?
1.虚拟机栈中引用的对象
2.方法区中类静态属性引用对象
3.方法区中常量引用的对象
4.本地方法栈中JNI引用的对象
(对虚拟机内存划分不熟悉的,可以看我前面写的Java基础一中的JVM文章或者查阅相关资料)
这里再提出一个问题:
可达性分析中不可达对象,是不是就一定要被回收掉?在《深入理解Java虚拟机中》解释我觉得很容易懂:即使在可达性分析算法中不可达的对象,也不一定是"非死不可",这时候它们处于"缓刑"阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它就会被标记一次并且进行一次筛选的条件就是此对象是否有必要执行finalize()方法如果对象没有重写finalize()方法,或者已经执行过一次finalize()方法,虚拟机认为"没有必要执行"于是宣布死亡如果认为有必要执行finalize()方法,那么这个对象会放置在一个F-Queue的队列中,并在稍后会被触发,但是虚拟机并不承诺会等待结束(防止队列中其他对象永久等待),如果此时对象被放到引用链上那么就会脱离回收的命运,如果没有就会被回收。也就是说:重写finalize能够让对象起死回生,但是不能保证一定会存活下去。
需要注意的是:一个对象finalize()方法只能被调用一次,在下一次如果对象面临回收那么就不会进行"起死回生"的操作。同时不鼓励使用这个方法来拯救对象,它的运行代价高昂,不确定性大,每个对象的调用顺序无法保证。
02
什么时候回收?
分代回收 内存不够
举一个可能不太合适的例子,如果你比较懒,不喜欢经常收拾房间,房间已经乱到你无法下脚的时候你总该收拾了吧。虚拟机可能比较"懒"吧,内存中剩余空间无法放入新的对象的时候就会进行GC.(注意不是你调用gc()方法就会进行回收,这个方法是不可控的)
我们都知道内存回收主要是指堆中的内存回收,而当前的商业虚拟机垃圾收集都采用"分代收集"算法,就是把堆中分为新生代和老年代,然后根据各个年代的特点采用适当的收集算法。新生代中的对象特性是"朝生夕死",当新生代剩余空间放不下将要进入到新生代对象的时候,会执行一次回收,此次回收会清除大批已死的对象,对于少量存活的对象,采用复制算法放入到幸存区(对于新生代和老年代不熟悉可以先看一下前面的文章),因为老年代中对象存活率高,没有额外的空间对他进行分配担保,就必须使用标记-清理或者标记-整理的算法进行回收。那到底是啥时候进行回收啊??直接点就是内存不够的时候,而我又要加入的对象比剩余空间大,此时会进行GC。
那么复制算法,标记-整理算法,标记-清除算法又是什么?这就是第三个问题的答案了.......
03
如何回收
回收算法
复制算法:在新生代中我们使用复制算法来进行回收,为什么呢?因为新生代里面的对象存活率低,大部分对象在第一次回收的时候就会被清除掉,剩下的比较少,此时我们将伊甸区中存活的对象复制到幸存区0(2个幸存区,有一个总为空),下一次回收把幸存区和伊甸区存活的对象复制到幸存1(幸存区0清空)。我们也可以知道:复制算法成本会比较大(存活对象比较多效率会低,而且会有一部分空间浪费),但是简单高效
标记-清除:分为两个阶段,标记需要回收对象,然后清除。那么作为收集算法的基础,它存在什么问题呢?首先是效率不高,然后是空间浪费
效率不高我们先不谈论了,为什么会空间浪费,因为清除的对象会造成内存碎片,如何理解呢,我们想想ABCDE五个人刚好挤在一个房子里面,现在F要进来,由于空间满了,我们调用清除算法把B和D清除出去了,B和D占用面积是一平米,F占用面积是1平米,总共剩余空间虽然够F用,但是我们不能把F撕成两半把,B和D清除后空出的地方就叫做内存碎片,虽然有内存,但是却没法用。
标记-整理:在标记-清除的基础上进行改进的算法,让所有存活的对象向一端移动,然后清理边界以外的内存。这样就会避免内存碎片出现
04
问题
笔试题
1.说一说强软弱虚引用
强引用:程序中普遍存在的,类似于new Object(),只要强引用还在,GC永远不会回收被引用的对象
软引用:被软引用关联着的对象,会在系统内存溢出之前进行第二次回收
弱引用:比软引用更弱一些,在下次GC的时候,无论是否内存是否够,都会被回收掉。
虚引用:最弱的一种引用关系,设置虚引用的唯一目的就是在这个对象被收集器回收时收到一个系统通知。
2.老年代的对象只能从新生代晋升上来吗?
若对象体积太大, 新生代无法容纳这个对象,
-XX:PretenureSizeThreshold即对象的大小大于此值, 就会绕过新生代, 直接在老年代分配, 此参数只对Serial及ParNew两款收集器有效。
05
垃圾收集器
那么主要有哪些垃圾回收器呢?
1.Serial
2.ParNew
3.Parallcl Seavenge
4.Serial Old
5.Parallel Old
6.CMS
7.G1
Serial是最基本,发展历史最悠久的收集器,在JDK1.3前是虚拟机新生代收集的唯一选择,是一个单线程的收集器,在他进行收集时候必须暂停其他所有的工作线程,直到收集结束(也就是Stop The World,停顿)停顿会极大影响用户的体验,所以历来垃圾收集器都在为缩短停顿时间而努力。Serial的优点也很明显:简单高效,没有线程切换的开销。
ParNew是Serial收集器的多线程版本,其他方面于Serial收集器完全一样,共用相当多的代码。
Parallel Scavenge是一个新生代收集器,使用的是复制算法,是并行的多线程收集器。说到并行不得不说到并发:并发是在一台处理器上“同时”处理多个任务,并行在多台处理器上同时处理多个任务。Parallel Scavenge 的特点是它的目标是达到一个可控制的吞吐量(吞吐量是CPU运行用户代码的时间和CPU总消耗时间的比值 吞吐量=运行代码时间/(运行代码时间+垃圾收集时间))。提高吞吐量可以高效利用CPU时间,尽快完成程序的运算任务,适合后台运算而不需要太多交互的任务。所以Parallel Scavenge叫做吞吐量有限收集器
Serial Old是Serial收集器老年代的版本,同样是单线程收集器,使用"标记-整理"算法
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,它在jdk1.6后开始提供,在此之前,新生代Parallel Scavenge收集器很尴尬,因为
一旦选择Parallel Scavenge ,老年代只能选择Serial Old。
CMS收集器是追求最短停顿时间为目标的收集器,采用"标记-清除"算法实现的,它的整个运作过程分为4个步骤,包括:
初始标记
并发标记
重新标记
并发清除
其中前两个阶段仍会有停顿,初始标记仅仅只标记GC Roots能直接关联的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,重新标记是为了修正并发标记期间因为用户程序还在运作而导致标记长生标动的那一部分对象,这个阶段停顿时间会稍长一些,但是比并发标记短一些。由于并发标记和并发清除两个最耗时的过程与用户线程一起工作,所以总体上说:CMS收集器的内存回收过程是于用户线程并发执行的。优点也就显而易见了:低停顿,并发收集。缺点也很明显:无法处理浮动垃圾,因为并发清理阶段用户线程还在运行,所以这段时间产生的垃圾无法清除。大量的内存碎片(因为CMS收集器采用了标记-清除算法)。对CPU资源非常敏感,因为占用一部分CPU资源导致吞吐量降低
G1收集器是面向服务端的垃圾收集器,HotSpot团队赋予它的使命是替换JDK1.5发布的CMS收集器,G1收集器有如下特点:并行于并发:G1能充分利用多CPU多核环境下的硬件优势。分代收集:于其他收集器一样,分代概念得以保留,这使得它能够采用不同的方式去处理不同时期的对象。空间整合,G1收集器采用"标记-整理"算法,意味着不会产生内存碎片,有利于程序长时间运行。可预测的停顿:G1追求低停顿的同时,建立可预测的时间模型,能有效避免在堆中进行全区域的垃圾收集。G1收集器大致可划分以下几个步骤:
初始标记
并发标记
最终标记
筛选回收
初始标记仅仅是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,这一个阶段会停顿,但耗时很短,并发标记阶段是从GC Roots开始对堆中的对象进行可达性分析,找出存活的对象,耗时很长,但是可以并发执行,最终标记则是修改正在并发标记阶段因用户程序继续运作而导致标记产生标动的那一部分标记记录,这个阶段会停顿,但是可并行执行。筛选回收阶段对各个Region的回收价值和成本进行排序。
06
内存分配和回收策略
对象的内存分配主要是指堆上的分配(一些对象被拆分原子类型的话会在栈上分配),对象主要分配在新生代的Eden区上,一些较大的对象会被直接存储在老年代中,分配的规则取决于使用哪一种垃圾收集器组合,还有虚拟机中于内存相关的参数的设置
内存回收和垃圾收集器在很多时候都是影响系统性能,并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及大量调节的参数,是因为只有根据实际应用需求,实现方式选择最优的收集方式才能获取最高的性能,没有固定的收集器,参数组合,也没有最有的调优方法,虚拟机也就没有什么必然的内存回收行为。因此学习虚拟机内存要到实践调优阶段,那么必须了解每个具体收集器的行为,优势和缺点,调节参数。
关注微信公众号--"每天学Java"--给你不一样的收获!