垃圾收集器与内存分配策略

这篇Java虚拟机的内存区域讲解我们说到垃圾收集器主要在Java堆中运行,那它有什么收集算法或者内存分配策略呢,其实主要有这几种算法:标记-清除算法,复制算法,标记算法,分代收集算法,但是这些算法仅仅说的是如何回收。

对于GC来说它需要完成有这3件事:1,哪些内存需要回收。2,什么时候回收。3,如何回收。

哪些内存需要回收。
上次说到程序计数器,虚拟机栈,本地方法栈属于线程内部的区域,它们是随着线程的生命周期,栈中的栈帧随着方法的进入和退出而进栈,退栈。所以这几个内存区域不用考虑。而方法区和Java堆则是需要内存回收的。
在Java堆中回收的是死对象,而方法区,有人说是永生代,其实在方法区中确实不要求回收,性价比比较低,但是可以回收无用的常量和类,常量跟对象一样的回收方法,但是类回收要满足以下条件的时候可以会被回收,但不是一定会被回收:

 
  1. 改类的所有实例被回收,也就是说没有这类的对象
  2. 加载改类的ClassLoader也被回收
  3. 改类对应的Class类没有被引用,也就是改类没有被反射。

什么是对象死亡

有人说没有被任何地方引用的对象就是死亡的,会被回收的。那怎么判断对象是否有被引用呢。

1.引用计数法
这个说的是给对象添加一个引用计数器,有地方引用时+1,引用失效时-1。任何对象的引用计数器为0时就是没有被引用。但是这个很难解决对象之间互相引用的时候。如:A内部引用B,B内部引用A,

 
  1. public class RefGc{
  2. public Object obj;
  3. private byte[] big=new byte[2*1024*1024];
  4. public static void main(String [] arg)
  5. {
  6. RefGc A=new RefGc();
  7. RefGc B=new RefGc();
  8. A.obj=B;
  9. B.obj=A;
  10. A=null;
  11. B=null;
  12. System.gc();
  13. }
  14. }

结果
kWGQ0A.md.png
按引用计数法来说它们就不会被回收了,但是主流的虚拟机会被回收的,说明主流的虚拟机不是用这种方法。

2.可达性分析算法
这个算法就是通过一系列“GC ROOT”的对象为起点,从这些节点开始向下搜索,搜索过的路径称为引用链,当对象到GCRoot没有任何引用链相连时,则说明该对象不可用。主流的虚拟机是用这种方法来判定的。如:
kWGWnJ.md.png

对于可达性分析算法中不可用的对象也不是马上就被回收的,它至少要经历2次标记的过程,当它第一次被标记时,先判断是否重载过finalize()方法,而在这个方法中可以重新引用自救的。但是这个方法只能用一次。如:

 
  1. public class FinGc {
  2. public static FinGc finGc=null;
  3. public static int i=0;
  4. public void isAlive(){
  5. System.out.println("I am live");
  6. }
  7. @Override
  8. protected void finalize() throws Throwable{
  9. super.finalize();
  10. System.out.println("活起来了 "+FinGc.i++);
  11. FinGc.finGc=this;
  12. }
  13. public static void main(String [] arg) throws Throwable{
  14. finGc=new FinGc();
  15. finGc=null;//把引用为空
  16. System.gc(); //用于调用垃圾收集器,第一次执行finalize
  17. Thread.sleep(500);//因为finalize的优先级很低,所以等几秒
  18. if (finGc!=null) {
  19. finGc.isAlive();
  20. }
  21. else{
  22. System.out.println("die");
  23. }
  24. //第2次,再次把第一次起来的引用去掉,发现不会在执行finalize方法了,自救失败
  25. finGc=null;
  26. System.gc();
  27. Thread.sleep(500);
  28. if (finGc!=null) {
  29. finGc.isAlive();
  30. }
  31. else{
  32. System.out.println("die");
  33. }
  34. }}

结果:

 
  1. 活起来了 0
  2. I am live
  3. die
  4. Process finished with exit code 0

从结果可以看出来,第一次救活了,第2次失败了,当第一次执行垃圾收集时,会执行finalize(),但是第2次再执行收集时,就不会执行finalize(),

如何回收
1.标记-清除算法
这是最基层的收集算法,其他算法都是在这改写的。思想就是分为标记和清除2个阶段,先统一标记,再回收,但是这样的效率不高,而且回收后可用空间是不连续的,等下次分配比较大对象内存时会找不到足够的连续内存而提前触发内存回收,影响性能。

2.复制算法
为了解决内存回收后不连续的问题,出现了复制算法。就是把内存分为2半,另一半在使用,一半在等另一半内存不够触发收集器回收后其他活的对象统一复制到这一半。也就是不用考虑内存碎片的问题了。但是这种算法把内存缩小了原来的一半。现在的主流虚拟机都是用这种办法来收集新生代的内存,由于新生代的98%都是很快死亡的,也就是说收集后活下来的很少,所以不用1:1来分内存,可以用8:1,当然我们也不能保证每次都是98%,当1那边的内存不够时可以向老年代借。

3.标记-整理算法
根据老年代的特点,有了这种算法在标记-清除算法上添加整理的阶段。就是说,标记了以后不是马上清除,而是把活的对象向一边移,清理另一边就行。

4.分代收集算法
根据对象生命周期的不同把内存分为新生代和老年代,然后根据年代的特点,采用适合的收集算法,新生代每次清理时都会有大量的对象死去,活的对象很少所以用复制算法。对于老年代来说是比较平稳的,没有多余的空间给他借,所以用标记-整理或者标记-清理算法。