前言
垃圾回收器—GC(Garbage Collection),它与“java面向编程”一样是java语言的特性之一;它与“ c/c++语言”最大区别是不用手动调用 free() 和 delete() 释放内存。GC 主要是处理 Java堆Heap ,也就是作用在 Java虚拟机 用于存放对象实例的内存区域,(Java堆又称为GC堆)。JVM能够完成内存分配和内存回收,虽然降低了开发难度,避免了像C/C++直接操作内存的危险。但也正因为太过于依赖JVM去完成内存管理,导致很多Java开发者不再关心内存分配,导致很多程序低效、耗内存问题。因此开发者需要主动了解GC机制,充分利用有限的内存的程序,才能写出更高效的程序。
一、检测垃圾
1、引用计数算法(Reference Counting)
原理:给每个对象添加一引用计数器,每当有一个地方引用它,计数器+1 ,引用失效时就-1 。
分析:引用计数算法很简单高效。但是,现在主流的虚拟机没有选用引用计数算法来管理内存,原因是它很难解决对象之间相互引用的问题。试想一下,要是虚拟机用了引用计数算法,下面的objA和objB会不会回收呢?
public class ReferenceCountingGC{
public Object instance = null;
public static void testGC(){
ReferenceCountingGC objA = new ReferenceCountingGC ();
ReferenceCountingGC objB = new ReferenceCountingGC ();
objB.instance = objA;
objA.instance = objB;
objA = null;
objB = null;
System.gc();
}
}
2、可达性分析算法(Rearchability Analysis)
原理:以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java栈中引用的对象、方法区常良池中引用的对象、本地方法中引用的对象等。
可达状态:在一个对象创建后,有一个以上的引用变量引用它,那它就处于可达状态。
可恢复状态:对象不再有任何的引用变量引用它,它将先进入可恢复状态,系统会调用finalize()方法进行资源整理,发现有一个以上引用变量引用该对象,则这个对象又再次变为可达状态,否则会变成不可达状态。
不可达状态:当对象的所有引用都被切断,且系统调用 finalize() 方法进行资源整理后该对象依旧没变为可达状态,则这个对象将永久性失去引用并且变成不可达状态,系统才会真正的去回收该对象所占用的资源。
3、对象的四种引用状态
强引用 :创建一个对象并把这个对象直接赋给一个变量,不管系统资源多么紧张,强引用的对象都不会被回收,即使他以后不会再用到。
软引用 :通过SoftReference修饰的类,内存非常紧张的时候会被回收,其他时候不会被回收,在使用之前要判断是否为null从而判断他是否已经被回收了。
弱引用 :通过WeakReference修饰的类,不管内存是否足够,系统垃圾回收时必定会回收。
虚引用 :不能单独使用,主要是用于追踪对象被垃圾回收的状态。通过PhantomReference修饰和引用队列ReferenceQueue类联合使用实现。
二、垃圾回收机制
垃圾回收的算法一般有如下几种,简述如下:
串行回收和并行回收:串行回收是不管系统有多少个CPU,始终只用一个CPU来执行垃圾回收操作;并行回收就是把整个回收工作拆分成多个部分,每个部分由一个CPU负责,从而让多个CPU并行回收。并行回收的执行效率很高,但更复杂,内存会增加。
程序停止和并发执行 :顾名思义是在执行垃圾回收的同时会导致应用程序的暂停。并发执行垃圾回收虽不会导致应用程序的暂停,但需要解决和应用程序的执行冲突,因此系统开销比较高,执行时需要更多的堆内存。
标记不压缩:标记-清除要遍历两次。第一次先从根集开始访问所有可达对象,并将他们标记为可达状态。第二次遍历整个内存区域,对不达状态的对象进行回收处理。这种回收方式不压缩,不需要额外内存,但要两次遍历,会产生碎片。
标记压缩 :所有的可达对象集合到一起,将之前占用的内存全部回收,减少内存碎片的产生。
复制式的垃圾回收:将堆内存分成两个相同空间,将空间A的可达对象全部复制到空间B,然后一次性回收A的空间。因为只需访问可达对象,复制走之后直接回收整个空间,完全不用理会不可达对象,所以遍历空间成本小效率高,但复制的空间成本大。典型的拿“空间”换“时间”。
堆内存的分代回收
Java垃圾回收机制的最基本的做法就是分代回收。内存中的区域被划分成不同的世代,对象根据其存活的时间被保存在对应世代的区域中。一般的实现是划分成三个年代:年轻、年老、永久。内存的分配是发生在年轻世代中的。当一个对象存活的时间够久的时候,它就会慢慢变老(被复制到老年代中)。对于不同世代可以使用不同的垃圾回收算法。进行世代划分的出发点是对应用中对象存活时间进行研究之后得出的统计规律。一般来说,一个应用中的大部分对象的存活时间都很短。比如局部变量的存活时间就只在方法的执行过程中。因为年轻世代的对象很快会进入不可达状态,因此要求回收频率高且回收速度快,基于这一点,对于年轻世代的垃圾回收算法就可以很有针对性。
1、年轻世代
采用复制式回收算法,划分两个区域,分别是E 区和 S 区。大多数对象先分配到Eden区,内存大的对象会直接被分配到老年代中。S 区又分Form、To两个小区,一个用来保存对象,另一个是空的;每次进行年轻代垃圾回收的时候,就把E大区和From小区中的可达对象都复制到To区域中,一些生存时间长的就直接复制到了老年代。最后,清理回收E大区和From小区的内存空间,原来的To空间变为From空间,原来的From空间变为To空间。
2、年老世代
回收机制 :采用标记压缩算法回收。
对象来源 :对象大直接进入老年代、Young代中生存时间长的可达对象。
回收频率 :因为很少对象会死掉,所以执行频率不高,而且需要较长时间来完成。
3、永久世代
用 途 :用来装载Class,方法等信息,默认为64M(Android的运行时应用分配的内存),不会被回收。
对象来源 :像Hibernate,Spring这类喜欢AOP动态生成类的框架,往往会生成大量的动态代理类,因此我们经常遇到java.lang.OutOfMemoryError:PermGen space的错误,这就是Permanent代内存耗尽所导致的错误。
回收频率 :不会被回收。
小结
实际开发中常用的小技巧:
1、尽量使用直接变量,例如:String javaStr = “XXX”;
2、使用 StringBuilder 和 StringBuffer 进行字符串连接等操作;
3、尽早释放无用对象;
4、尽量少使用静态变量;
5、缓存常用的对象:可以使用开源的开源缓存实现,例如:OSCache,Ehcache;
6、尽量不使用 finalize() 方法;
7、在必要的时候可以考虑使用软引用 SoftReference。