*对之前的一片博客重新整理
1 哪些内存需要回收?
Java运行时数据区包括:程序计数器、虚拟机栈、本地方法栈、Java堆和方法区。这里面程序计数器、虚拟机栈和本地方法栈是线程私有的,当线程结束或者方法退出时其内存自然会被回收。所以Java的垃圾回收机制主要关注的是两部分:Java堆和方法区。JVM规范并没有强制要求对方法区进行GC,其实方法区保存的都是类信息、静态变量、常量等,并不太需要GC,所以GC主要还是关注Java堆。
2 如何确定一个对象需要回收?
判断一个对象是不是需要回收,有两种方法:
2.1 引用计数法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器的值就加一;当引用失效时,计数器的值减一;任何时刻计数器的值为0的对象就是不可能再被使用的。
引用计数法实现简单,判定效率也很高,在大部分情况下是一个不错的算法。但主流的Java虚拟机都没有使用引用计数法来管理内存,其中主要的原因是:它很难解决对象之间互相引用的问题。比如对象A和B都有instance字段,然后objA.instance=B,objB.instance=A。这里构成了一个循环引用,如果使用引用计数法的话这两个对象永远不会被回收,即使实际上这两个对象都不可能被访问到了。
2.2 可达性分析法
通过一系列的称为“GC Roots"的对象作为起点,从这些起点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可用的。虚拟机主流实现都是使用可达性分析实现判断对象是否存活的。
GC Root
其实就是一个“根集合”,保存一组必须活跃的引用(注意是引用不是对象)。这些引用包括:
- 虚拟机栈中引用的对象(即当前所有被调用方法的引用类型的参数/局部变量/临时值)
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中Native方法引用的对象
引用
上面提到了引用,Java里一共有四种引用:
- 强引用:只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
- 软引用:软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。
- 弱引用:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
- 虚引用:最弱的一种引用关系。一个对象是否有需引用的存在完全不会对其生存时间构成影响,也无法通过需引用来取得一个对象实例。为一个对象设置一个需引用的唯一目的就是能够在这个对象被收集器回收时收到一个系统通知。
3 回收算法
3.1 标记-清除法:
分为标记和清除两个阶段:需要遍历堆两次,第一次标记所有需要回收的对象,第二次统一回收被标记的对象。
主要不足:
- 效率问题:标记和清除两个过程的效率都不高;
- 空间问题:标记清除后会产生大量不连续的内存碎片。
3.2 复制算法:
将内存划分成大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的那块内存一次性的清理掉。
- 优点:不必考虑内存碎片问题,实现简单,运行高效;
- 缺点:将内存缩小为了原来的一半代价太高。
改善办法:IBM公司研究表明98%的对象都是“朝生暮死”的。所以不需要按1:1分配空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和一块Survivor。当回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor中,然后清理掉原来的内存。通常Eden和Survivor的空间比例为8:1。
当然,没有办法保证每次回收都只有不到10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(年老代)进行分配担保----如果Survivor没有足够的空间收集存活的对象,这些对象直接进入年老代。
3 标记-整理法:
标记过程和标记清理法过程相同,但后续步骤不直接进行清除,而是让所有对象都像一端移动,然后直接清理掉端边界以外的内存。
4 分代收集算法:
根据对象的存活周期将内存分为几块,Java一般分为新生代和年老代,这样就可以根据各个代的不同特点采用最合适的收集算法。一般新生代采用复制算法,年老代采用标记清除法或标记整理法。