Java内存回收机制

内存回收的意义

当我们使用C++这门开发语言时,每次new出来的变量都需要手动delete掉,否则会出现内存泄漏的问题。而Java语言中一个显著的特点就是引入了内存回收机制,它使得Java程序员在编写程序的时候不再需要考虑内存管理,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。

内存回收机制的算法

Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。

1.引用计数法(Reference Counting Collector)

算法分析

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,则该对象的引用计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

优点

引用计数收集器可以很快的执行,交织在程序运行中。对需要不被长时间打断的程序比较有利。

缺点

无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样他们的引用计数永远不可能为0。

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();

object1.object = object2;
object2.object = object1;

object1 = null;
object2 = null;
}
}

最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

2.tracing算法(Tracing Collector) 或 标记-清除算法(mark and sweep)

算法分析

tracing算法是为了解决引用计数法的问题而提出,它使用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。

优点

解决了循环引用的问题。

缺点

该算法不需要进行对象的移动,并且仅对不可达的对象进行处理,在可达对象比较多的情况下极为高效,但由于标记-清除算法直接回收不可达的对象,因此会造成内存碎片。

3.标记-整理算法(Mark-Compact)

算法分析

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

优点

解决了内存碎片的问题。

缺点

由于需要对对象进行移动,因此成本较高。

4.copying算法

算法分析

该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成一个对象面和一个空闲面,程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾收集就从根集中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。

优点

实现简单,运行高效且不容易产生内存碎片

缺点

对内存空间的使用付出了巨大的代价,因为可用的内存空间减少了一半。Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

5.Generational Collection(分代收集)算法

算法分析

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

  • 年轻代(Young Generation)
    年轻代就是为了快速清理掉那些生存周期短的对象而设立的,年轻代分为三个模块,一个eden区,两个survivor区(survivor0和survivor1),它们内存按8:1:1分配,一个新的对象建立首先在eden区,年轻代的回收叫做minor GC,在回收时将eden区中存活的对象复制到survivor0区中,然后清空eden区。当survivor0区的内存被存满时,eden区和survivor0区将全部存活的对象存入survivor1区。然后将survivor0区和eden区清空,将survivor1区与survivor0区交换,以此一直循环,直到回收时survivor1区存不下survivor0+eden区的存活对象时就将存活对象放入老年代。
  • 老年代(Old Generation)
    当对象在年轻代经历过次次历练后,他终于存活到了老年区,所以老年代中的对象大多都是一些生命周期比较长的对象,老年代也比年轻代分到到的内存要大,默认是1:2。当老年代内存也存满时就会触发一次full GC或者叫major GC,也就是对年轻代老年代都进行回收。
  • 持久代(Permanent Generation)
    用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

GC root

GC root是一组根引用,由于Java垃圾回收主要是针对堆内存,因此这些引用则来自于JVM运行时数据区的其它几部分:虚拟机栈,本地方法区,方法区。主要包括:

  1. 虚拟机栈中的局部变量表。
  2. 类的静态属性引用。
  3. 常量对象引用。
  4. 本地方法区中的对象引用。
0%