垃圾收集器需要完成的三件事:
1)哪些内存需要回收?
2)什么时候回收?
3)如何回收?
一:需要考虑内收回收的区域:
也就说线程私有的区域(java stack、native java stack pc register)伴随的线程的产生而产生伴随的线程的消亡而回收。所以我们关心内存回收区域为:heap、以及method area
对象是否已死?
根搜索算法:
这个算法的基本思路为:就是通过一系列名为“GC Roots “的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径为称为引用链(Reference Chain),当一个对象到GC ROOT 没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象不可以用,也就是说这些对象可以回收
引用概念:
根搜索算法涉及到引用,判断对象的存活都跟“引用”有关。
如果引用类型的数据中存储的数值代表的是另一个内存的起始地址,就称这块内存代表着一个引用。
JDK1.2以后又把引用分为:强引用(strong reference)、软引用(soft reference弱引用(weak reference)、虚引用(phantom reference)四种,这四种引用强度一次逐渐减弱。
1)强引用就是指在程序代码之中普遍存在的,类似“Object obj=new Object()“,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
2)软引用 用来描述一些还有用,但并非必需对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。
3)弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
4)虚拟引用也称为幽灵引用或者幻影引用。它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象的设置虚引用关联的唯一目的就是希望能在这个对象被收集回收时受到一个系统通知。
对象生存还是死亡?
在根搜索算法中不可达对象,也并非是该对象“非死不可“的,这时候它们暂时处于待“处理状态”。要真正判断一个对象可以回收(死亡),至少经历2次标记:
一)如果对象在根搜索算法之后,有没有与GCroots 相连接的引用链。这是第一个标记。
二)有第一次标记的之后的筛选的对象,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将两种的情况都视为“没必要执行”。如果该对象“没必要执行”finalize()方法时可以直接进行回收该对象。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。
如果对象这时候还没逃脱,那它就真的离死不远了。
注意:
第一,finalize()方法只会被执行一次,所以对象只有一次复活的机会。
第二,执行GC后,要停顿半秒等待优先级很低的finalize()执行完毕。
第三:finalize()方法 并不是并需的,经查看一些资料,该方法尽量少用。因为该方法的实现的”代价比较高昂“,一般在研发在编写代码的时候很少用finalize()方法来实现对象自救。
第四:即使该对象实现一次”自救“,但是面临下次回收的时候,还是会直接被回收,而不会再次 执行 finalize()方法。
方法区回收:
主要垃圾回收区域:废弃常量和无用的类。
废弃常量:回收废弃常量与回收java堆中的对象非常类似。比如以常量池中的字面量的回收为例。
假设字符串abc在常量池中,但是当前系统没有任何一个string对象叫做“abc”的。换句话说是没有任何的string对象引用常量池中的“abc”常量,也没有其他的地方引用这个字面量。如果在这个时候发生内存回收的,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类(接口)、方法、字段的符号引用页与此类似。
无用类回收:判断一个无用类需要同时满足以下3类条件:
1)该类所有实例都已经被回收,也就是java堆中不存在该类的任何实例。
2)加载该类的ClassLoader 已经被回收。
3)该类对应的java.lang.Class(类运行标识符)对象没有在任何地方被引用,无法再任何地方通过访问该类的方法。
同时满足这3个条件仅仅是“可以”回收,而不是对象经过2次标记之后就进行回收。
二:垃圾收集算法:
标记-清除算法:算法分为“标记”和清除两个阶段。
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的。
缺点:效率问题,标记和清除过程的效率都不高;
空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序以后的运行过程中需要分配较大对象的时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法:
为了解决标记清除算法的效率问题出现复制算法。
原理:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活的对象复制到另一个块上面。然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配的时候也就不用考虑内存碎片等复杂情况。
缺点:用空间换效率导致内存使用率为原来的一半。
应用场景:
现在大多数的商业虚拟机都采用复制收集算法来回收新生代。主要是针对新生代存活对象较少的特点。其中由IBM研究指出不需要把可用内存分配成1:1的比例,而是8:1:1的比例。将可用内存分为2个区,而这2个区分为3个区域分别为:Eden、Survivor、Survivor 3个区域。其中三者的比例为:8:1:1。其中每次使用一个Eden和一个Survivor,而另一个Survivor为保留区域。也就是每次新生代可使用的内存空间为整个新生代容量的90%,只有10%的新生代容量会被浪费。
问题:那我们每次都能保证回收后剩余的存活的对象都小于10%的新生代容量吗?
事实上这种回收机制可以满足98%的需求,当Survivor的内存空间大小不能满足的时候,需要依赖其他内存(这里指老年代)进行分配担保。这些对象直接通过分配担保机制进入老年代。
标记整理-算法:
复制算法主要用再新生代回收中,那老年代怎么进行收集呢?
针对存活对象超过保留区域的情况,显然老年代不适合这种算法(老年代存活对象较高的特点)。
原理:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法:
现在大多数虚拟机收集都采用分代收集算法。
所谓的新生代和老年代是针对分代收集算法来定义。
新生代经过一次GC回收之后就立即进入老年代吗?
新生代收集的算法是根据标记-复制算法。也就是数据首先分配到Eden区当中(特殊情况:如果是大对象那么会直接放入到老年代(大对象需要大量连续的内存空间的java对象)),当Eden没有足够空间的时候就会触发JVM发起一次Monitor GC。如果对象经过一次垃圾回收还存活,并且又能被Survivor空间接受的,那么将移动到Survivor空间当中。并将其年龄设定为1,对象在survivior每熬过一次垃圾回收,年龄会增加1,当年龄达到一定程度(默认是15)时,就会被晋升到老年代中。这个默认值可以进行修改。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
只是根据对象的存活周期的不用将内存划分为几块。一般分为:heap分为新生代和老年代,根据每个年代的特点采取相应的算法收集。
垃圾收集器:
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
Serial收集器
原理:Serial收集器是单线程收集器,当回收内存的时候会暂停其他的工作线程运行,直到回收完毕。
缺点:用户体验差。程序容忍度低(运行一半的时候,暂停运行。)
Java虚拟机在启动包括2种模式:client模式和server模式。
Client模式:启动速度较快,但是性能比server模式的差很多。
Server模式:启动比client 慢10%,但是性能远远好于clent模式。
在java程序的启动的时候指定:-client或者- server来判断启动模式
大多数应用环境采用的是server模式启动。
如何查看java虚拟机启动的模式:
1)查看启动脚本:export JAVA_OPTS="-Djava.library.path=/X/X -server -Xms2048m -Xmx2048m
2)使用java–version 查看:
/X/X/jdkX/bin/java-version
javaversion "X"
Java(TM)SE Runtime Environment (x)
JavaHotSpot(TM) 64-Bit Server VM (x)
性能比较:
适用场景:
当jvm用于启动图形用户(GUI)界面交互的时候适合适用client模式,当jvm用于运行后台程序的时候建议使用server模式。
Serial收集器虽然缺点比较明显,但是它依然是jvm在client模式默认新生代收集器。
优点:
简单高效(与其他的收集器的单线程相比),对于限定单个cpu的环境来说,serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。因为client模式,大多数用于gui交互界面,分配给虚拟机管理的内存一般来说不会很大,收集几十兆或者几百兆,停顿时间大约几十毫秒最多一百毫秒以内,只要不频繁发生,这点可以接受的。所以serial收集器对于运行在client模式下的虚拟机来说是一个很好的选择。
ParNew收集器
Parnew收集器是serial收集器的多线程版本。
适用场景:ParNew收集器大多运行在jvm的server模式下,首选的新生代收集器。其中与性能无关的很重要原因是除了serial收集器,目前只有ParNew收集器可以CMS收集器配合工作。
效率:ParNew收集器在单cpu的环境中绝对不会比serial收集器更好的效果,甚至由于存在线程交互开销。该收集器在通过超线程技术实现的两个的cpu的环境中都不能百分百超越serial收集器。
概念:并行指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序继续运行,而垃圾收集程序运行于另一个cpu上。
JVM内存回收算法以及垃圾收集器,布布扣,bubuko.com
原文:http://8118556.blog.51cto.com/8108556/1395214