GC(Garbage Collection)垃圾回收:主要针对堆与方法区。
一、常见的内存管理算法(存活判定)
1、引用计数算法:给对象中添加一个引用计数器,当有一个地方引用就+1,引用失效,计数器-1,任何时刻计数器为0的对象都不可能在被使用。这在大部分情况下是一个不错的办法,也有一些语言利用此方法来进行内存管理(Python,Squirrel),但在Java中并未选用引用计数器来进行内存管理,最主要的原因是它很难解决对象之间的相互引用。
例如: ObjA.instance = objB 及 objB.instance = objA,除了互相引用之外再无任何引用,实际上这两个对象已经不能再被访问到了,因为互相引用导致引用计数器都不为0。
2、可达性分析算法:这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起点,从这些起点开始向下探索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。
可作为GC Roots的对象包括下面几种:
(1) 虚拟机栈中引用的对象
(2) 方法区中类静态属性引用的对象
(3) 方法区中常量引用的对象
(4) 本地方法栈中JNI(Native方法)引用的对象
二、引用的扩充
在JDK1.2以前,Java中引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用,但这种定义太多狭义,对于那些“食之无味,弃之可惜”的对象就显得无能为力,比如当内存足够时,则能保留在内存中,如果内存在进行垃圾回收之后仍然紧张,则可以抛弃这些对象,如很多系统的缓存功能。
因此,在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为4种:
(1)强引用(Strong Reference)
(2)软引用(Soft Reference)
(3)弱引用(Weak Reference)
(4)虚引用(Phantom Reference)
Strong Reference –>Weak Reference -> Soft Reference – > Phantom Reference
Strong Reference:只有在引用对象root不可达的情况下才会标识为可回收,垃圾回收才可能进行回收
Soft Reference:无论其引用的对象是否root可达,在响应内存需要时,由垃圾回收判断是否需要收。
Weak Reference:用来描述非必需对象。即使在root算法中 其引用的对象root可达到,只能生存到下一次垃圾回收之前。
Phantom Reference:无法通过虚引用获得一个对象的实例,设置虚引用的目的就是能在这个对象被收集器回收时收到一个系统通知。
三、方法区的回收
方法区(HotSpot中的永久代)的垃圾收集效率远低于Java堆的垃圾回收,永久代收集的主要内容主要分为两部分:废弃常量和无用的类。例如,一个字符串"abc"已经进入了常量池中,但是当前系统没有任何一个String对象叫做"abc",就是没有任何String对象引用常量池中的"abc"常量,也没有其他地方引用这个字面量,如果此时发生垃圾回收,必要的话,这个"abc"常量会被系统清理出常量池。常量池中其他类(接口),方法,字段的符号引用也与此类似。
判断是否是"废弃常量"比较简单,而判定一个类是否是"无用的类"的条件相对比较苛刻。需要满足一下3个条件:
(1)该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例
(2)加载该类的ClassLoader已经被回收
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问到该
类
满足上述3个条件代表虚拟机可以进行无用类进行回收,仅是可以。并不是和对象一样,不使用就必然会回收。对类是否进行回收,HotSpot提供了相应的参数,见Java对象存储结构中的参数(
http://note.youdao.com/noteshare?id=320397686c53cdcf21e174221a4034c5)
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成Jsp以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
四、垃圾收集算法
1、标记-清除算法(Mark-Sqeep):最基础的收集算法,算法分为“标记”和“清除”两个阶段,首先标记出需要回收的对象,在标记完成之后统一回收有被标记的对象,因为后续的收集算法都是基于这种思路病对其不足进行改进而得到的,所以说其为最基础。
两个不足之处:
(1)效率问题:标记和清除两个过程效率都不高。
(2)空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续程序运
行中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃
圾回收
2、复制算法(Copying): 为了解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用一块,当一块的内存用完时,就将还存活的对象复制到另一块,然后将把已使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要一动堆顶指针,按顺序分配内存即可,实现简单,运行高效。但这种算法的代价是将可用内存降低一半。
IBM公司经过研究,新生代的对象98%都是“朝生夕死”,所以不要按照1:1的比例划分内存空间,而是将内存新生代分为一块较大内存的Eden与两块较小的Survivor空间,每次都是用Eden与一块Survivor,当回收时,将Eden与Survivor中还存活着的对象一次性的复制到另一个Survivor空间上,最后清理掉Eden与刚刚用过的Survivor空间。HotSpot默认的Eden比Survivor为8:1.而98%的对象可回收只是一般场景下的数据,并不能保证每次回收只有不多于10%的对象不会被回收,当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。
3、标记-整理算法(Mark-Compact):为了应对对象100%存活的极端情况,复制收集算法在对象存活率较高时就要进行较多的复制,效率将会降低。因此老年代一般不能直接选用复制算法。根据老年代的特点,提出了另外一种“标记-整理”算法,标记的过程与“标记-清除”的标记过程相同,但并不是对回收对象进行清理,而是让存活的对象向一端移动,然后直接清理掉端边界以外的内存。
4、分代收集算法(Generational Collection):目前商业的虚拟机的垃圾收集都采用“分代收集“算法,只是根据对象的生存周期不同将内存划分为几块,一般是把Java堆分成新生代和老年代,每次垃圾收集时新生代都有大批对象死去,只用少量存活,所以选用复制算法,老年代的对象存活率高,没有额外的空间对他进行分配担保,因此必须使用“标记-清理”或“标记-整理”算法进行回收。
(本文用图出自:
https://blog.csdn.net/loveslmy/article/details/46820929)