GC简介
Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放。
对于程序员来说,分配对象使用new关键字;释放对象时,只要将对象所有引用赋值为null,让程序不能够再访问到这个对象,我们称该对象为"不可达的".GC将负责回收所有"不可达"对象的内存空间。
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的".当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。但是,为了保证GC能够在不同平台实现的问题,Java规范对GC的很多行为都没有进行严格的规定。例如,对于采用什么类型的回收算法、什么时候进行回收等重要问题都没有明确的规定。因此,不同的JVM的实现者往往有不同的实现算法
Java堆分代/分区
我们先来屡屡,为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
新生代有一个有一个Eden区和两个survivor区(默认比例为8:1,两个suivivor区,一个叫from ,一个叫to),首先将对象放入Eden区,如果空间不足就向其中一个survivor区上放,如果仍然放不下,就会引发一次在新生代的minor GC,将存活的对象放入另一个survivor去中,然后清空Eden和之前的那个survivor的内存,在某次GC过程中,如果发现仍然有放不下的对象,就将这些对象放入老年代内存中。
(因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。)
2.大对象以及长期存活的对象直接进入老年区。
3.当每次执行minor GC的时候应该对要晋升到老年代的对象进行分析,如果这些马上要到老年区的老年对象的大小超过了老年区的剩余大小,那么执行一次Full GC以尽可能的获得老年区的空间。
什么是Minor gc、Full gc、Major gc
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
大家应该注意到,目前,这些术语无论是在 JVM 规范还是在垃圾收集研究论文中都没有正式的定义。但是我们一看就知道这些在我们已经知道的基础之上做出的定义是正确的,Minor GC 清理年轻代内存应该被设计得简单。
Major GC 是清理老年代。
Full GC 是清理整个堆空间—包括年轻代和老年代。
很不幸,实际上它还有点复杂且令人困惑。首先,许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。
清理的内容
从GC Roots搜索不到,而且经历过一次标记清理之后仍然没有复活的对象。
这里有一个问题,什么叫做经历过一次标记清理之后复活的对象,在《深入理解Java虚拟机》这本书中得到了解决。
java提供finalize()方法,垃圾回收器准备释放内存的时候,会先调用finalize()。
(1).对象不一定会被回收。
(2).垃圾回收不是析构函数。
(3).垃圾回收只与内存有关。
(4).垃圾回收和finalize()都是靠不住的,只要JVM还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收的。
那么怎么复活?
重写finalized方法,在方法里给对象以引用,但只能复活一次。在垃圾回收过程中,不能对复活对象调用 Finalize。
GC Roots都有哪些:
1.虚拟机栈中的引用的对象
2.方法区中静态属性引用的对象
3.常量引用的对象
4.本地方法栈中JNI(即一般说的native方法引用的对象
堆分代/分区各自采用的GC算法
新生代:复制清理
老年代:标记-清除和标记-压缩算法
永久代:存放Java中的类和加载类的类加载器本身
详细介绍垃圾回收算法
引用计数法
对于对象设置一个引用计数器,只要任何对象引用了A,那么A的计数器就加1,每减少一个变量的引用, 引用计数器就会减1,只有当对象的引用计数器变成0时,该对象才会被回收。
存在两个问题:
1.每次在增加变量引用和减少引用时都要进行加法或减法操作,如果频繁操作对象的话,在一定程度上增加的系统的消耗。
2.假设有两个对象 A和B,A中引用了B对象,并且B中也引用了A对象,那么这时两个对象的引用计数器都不为0,但是由于存在相互引用导致无法垃圾回收A和 B,导致内存泄漏。
A a=new A();
B b=new B();
a.b1=b;
b.a1=a;
a=null;
b=null;
程序运行完后,计数器仍然是1,导致无法回收。
复制算法(Java中新生代采用)
原理:将内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存中的所有对象,交换两个内存的角色。如果系统中垃圾对象多,需要复制的存活对象数量就会相对较少,因此,复制算法的效率是很高的,而且新的内存空间中可以保证是没有内存碎片的。
缺点:
复制算法的代价是将系统内存折半,一般情况下很难让人接受。
对于存活对象较多的情况,效率不能保证
标记清除法(Mark-Sweep)
笔记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的方式是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象,因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
缺点:
标记清除算法回收后的控件时不连续的,会产生空间碎片,在对象的堆空间分配过程中,尤其是大对象的分配,不连续内存空间的工作效率要低于连续的空间。
标记压缩清除法(Java中老年代采用)
在标记清除法的基础上做了一个改进,可以说这个算法分为三个阶段:标记阶段,压缩阶段,清除阶段。标记阶段和清除阶段不变,只不过增加了一个压缩阶段,就是在做完标记阶段后, 将这些标记过的对象集中放到一起,确定开始和结束地址,比如全部放到开始处,这样再去清除,将不会产生磁盘碎片。但是我们也要注意到几个问题,压缩阶段占用了系统的消耗,并且如果标记对象过多的话,损耗可能会很大,在标记对象相对较少的时候,效率较高。