相信和小编同样的程序猿们在平常工做或面试当中常常会遇到JVM的垃圾回收问题,有没有在夜深人静的时候详细捋一捋JVM垃圾回收机制中的知识点呢?没时间捋也不要紧,由于小编接下来会给你捋一捋。java
<!-- TOC -->面试
<!-- /TOC -->算法
##1、 技术背景你要了解吧 按照套路是要先装装X,谈谈JVM垃圾回收的前世此生的。提及垃圾回收(GC),大部分人都把这项技术当作Java语言的伴生产物。事实上,GC的历史比Java久远,早在1960年Lisp这门语言中就使用了内存动态分配和垃圾回收技术。设计和优化C++这门语言的专家们要长点心啦~~多线程
##2、 哪些内存须要回收? 猿们都知道JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,所以这几个区域的内存分配和回收都具有肯定性,就不须要过多考虑回收的问题,由于方法结束或者线程结束时,内存天然就跟随着回收了。而Java堆区和方法区则不同、不同!(怎么不同说的朗朗上口),这部份内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。并发
垃圾收集器在对堆区和方法区进行回收前,首先要肯定这些区域的对象哪些能够被回收,哪些暂时还不能回收,这就要用到判断对象是否存活的算法!(面试官确定没少问你吧)高并发
###2.1 引用计数算法 ####2.1.1 算法分析 引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每一个对象实例都有一个引用计数。当一个对象被建立时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例能够被看成垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。 ####2.1.2 优缺点 优势:引用计数收集器能够很快的执行,交织在程序运行中。对程序须要不被长时间打断的实时环境比较有利。优化
缺点:没法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。 ####2.1.3 是否是很无趣,来段代码压压惊线程
public class ReferenceFindTest { public static void main(String[] args) { MyObject object1 = new MyObject(); MyObject object2 = new MyObject(); object1.object = object2; object2.object = object1; object1 = null; object2 = null; } }
这段代码是用来验证引用计数算法不能检测出循环引用。最后面两句将object1
和object2
赋值为null
,也就是说object1
和object2
指向的对象已经不可能再被访问,可是因为它们互相引用对方,致使它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。设计
###2.2 可达性分析算法 可达性分析算法是从离散数学中的图论引入的,程序把全部的引用关系看做一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点之后,继续寻找这个节点的引用节点,当全部的引用节点寻找完毕以后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被断定为是可回收的对象。3d
在Java语言中,可做为GC Roots的对象包括下面几种:
a) 虚拟机栈中引用的对象(栈帧中的本地变量表);
b) 方法区中类静态属性引用的对象;
c) 方法区中常量引用的对象;
d) 本地方法栈中JNI(Native方法)引用的对象。 ###2.3 Java中的引用你了解多少 不管是经过引用计数算法判断对象的引用数量,仍是经过可达性分析算法判断对象的引用链是否可达,断定对象是否存活都与“引用”有关。在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。
在程序代码中广泛存在的,相似 Object obj = new Object()
这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
用来描述一些还有用但并不是必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常以前,将会把这些对象列进回收范围之中进行第二次回收。若是此次回收后尚未足够的内存,才会抛出内存溢出异常。
也是用来描述非必需对象的,可是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生以前。当垃圾收集器工做时,不管当前内存是否足够,都会回收掉只被弱引用关联的对象。
也叫幽灵引用或幻影引用(名字真会取,很魔幻的样子),是最弱的一种引用关系。一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用来取得一个对象实例。它的做用是能在这个对象被收集器回收时收到一个系统通知。
不要被概念吓到,也别担忧,还没跑题,再深刻,可就很差说了。小编罗列这四个概念的目的是为了说明,不管引用计数算法仍是可达性分析算法都是基于强引用而言的。
###2.4 对象死亡(被回收)前的最后一次挣扎 即便在可达性分析算法中不可达的对象,也并不是是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
第一次标记:若是对象在进行可达性分析后发现没有与GC Roots相链接的引用链,那它将会被第一次标记;
第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()
方法。在finalize()
方法中没有从新与引用链创建关联关系的,将被进行第二次标记。
第二次标记成功的对象将真的会被回收,若是对象在finalize()
方法中从新与引用链创建了关联关系,那么将会逃离本次回收,继续存活。猿们还跟的上吧,嘿嘿。
###2.5 方法区如何判断是否须要回收 猿们,方法区存储内容是否须要回收的判断可就不同咯。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可经过引用的可达性来判断,可是对于无用的类则须要同时知足下面3个条件:
ClassLoader
已经被回收;java.lang.Class
对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。<br/> <font color=#0099ff size=5 face="黑体">讲了半天,主角终于要粉墨登场了。</font>
<br/>
##3、经常使用的垃圾收集算法 ###3.1 标记-清除算法 标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,以下图所示。标记-清除算法不须要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的状况下极为高效,但因为标记-清除算法直接回收不存活的对象,所以会形成内存碎片。
###3.2 复制算法 复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分红 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每一个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
###3.3 标记-整理算法 标记-整理算法采用标记-清除算法同样的方式进行对象的标记,但在清除时不一样,在回收不存活的对象占用的空间后,会将全部的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,所以成本更高,可是却解决了内存碎片的问题。具体流程见下图:
###3.4 分代收集算法 分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不一样的区域。通常状况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区以外还有一个代就是永久代(Permanet Generation)。老年代的特色是每次垃圾收集时只有少许对象须要被回收,而新生代的特色是每次垃圾回收时都有大量的对象须要被回收,那么就能够根据不一样代的特色采起最适合的收集算法。
####3.4.1 年轻代(Young Generation)的回收算法
a) 全部新生成的对象首先都是放在年轻代的。年轻代的目标就是尽量快速的收集掉那些生命周期短的对象。
b) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(通常而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,而后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另外一个survivor1区,而后清空eden和这个survivor0区,此时survivor0区是空的,而后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
d) 新生代发生的GC也叫作Minor GC,MinorGC发生频率比较高(不必定等Eden区满了才触发)。 ####3.4.2 年老代(Old Generation)的回收算法 a) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。所以,能够认为年老代中存放的都是一些生命周期较长的对象。
b) 内存比新生代也大不少(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。 ####3.4.3 持久代(Permanent Generation)的回收算法 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,可是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候须要设置一个比较大的持久代空间来存放这些运行过程当中新增的类。持久代也称方法区,具体的回收可参见上文2.5节。
<br/> <font color=#0099ff size=5 face="黑体">猿们加油跟上,离offer不远啦!!!</font>
<br/> ##4、常见的垃圾收集器 下面一张图是HotSpot虚拟机包含的全部收集器,图是借用过来滴:
-XX:+UseSerialGC
来强制指定。-XX:+UseParallelGC
来强制指定,用-XX:ParallelGCThreads=4
来指定线程数。##5、GC是何时触发的(面试最多见的问题之一) 因为对象进行了分代处理,所以垃圾回收区域、时间也不同。GC有两种类型:Scavenge GC和Full GC。 ###5.1 Scavenge GC 通常状况下,当新对象生成,而且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,而且把尚且存活的对象移动到Survivor区。而后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。由于大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,因此Eden区的GC会频繁进行。于是,通常在这里须要使用速度快、效率高的算法,使Eden去能尽快空闲出来。 ###5.2 Full GC 对整个堆进行整理,包括Young、Tenured和Perm。Full GC由于须要对整个堆进行回收,因此比Scavenge GC要慢,所以应该尽量减小Full GC的次数。在对JVM调优的过程当中,很大一部分工做就是对于Full GC的调节。有以下缘由可能致使Full GC:
a) 年老代(Tenured)被写满;
b) 持久代(Perm)被写满;
c) System.gc()被显示调用;
d) 上一次GC以后Heap的各域分配策略动态变化;
##结束语
内容的完整度和深度在一篇博文里面真的很难所有考虑,本文作了很大尝试,最后仍是得投降。对于各个垃圾收集器的区别、运行过程当中各内存区域参数的设置、GC日志的查看等内容后续再补上吧。文章概念不少,也借用了一些书籍和博文的经典总结,算是一个知识点整理后的输出吧,但愿对你们有所裨益。