Java 虚拟机垃圾回收策略简要介绍
垃圾回收是什么?
Java 虚拟机垃圾回收是指对不使用的内存区域进行释放,防止分配空间时因内存不足而出现内存溢出异常。
哪些内存需要回收?
垃圾回收主要发生在 Java 堆和方法区中,Java 堆和方法区是 Java 虚拟机管理内存中的两个区域,其中 Java 堆主要是用来存放 Java 程序中的对象实例,方法区则用来存储已加载的类信息、常量、静态变量等数据。Java 虚拟机管理的内存中还有其他几个区域:程序计数器、Java 虚拟机栈、本地方法栈、运行时常量池、直接内存,这几个区域对内存的使用比较具有确定性,所以不需要考虑回收策略,然而 Java 堆和方法区则不一样,创建对象实例、加载类、定义常量等操作都是在程序运行期间进行的,这两个区域的内存使用是动态的,因此需要特定的垃圾回收策略进行管理内存。
如何回收?
一般是使用一种叫分代收集算法的综合策略进行回收,这种算法是把内存根据使用状态的不同划分为几块。其中 Java 堆大致分为两块:新生代和老年代,新生代存储新创建或变动频繁的对象的内存信息,老年代则存储存活时间较长、不经常变动的对象的内存信息。方法区内存则一般分为永久代,这里的内存变动频率更加低,每次垃圾回收只有少数的废弃常量和无用的类会被回收。下面主要说下针对新生代和老年代的内存回收策略。
最基础的策略是标记-清除算法,首先标记出所有需要回收的对象,之后统一释放对象占用的内存空间。由于回收的对象在内存中不一定是连续存储的,所以这种算法执行之后可能分产生大量不连续的内存碎片,这可能会导致因无法分配较大对象而再次触发垃圾回收动作。
为了提高效率,针对新生代的垃圾回收一般采用复制算法,将内存按容量比例分成8:1:1三块内存,其中8份为常用空间,剩下两份为俩两块保留空间。使用内存时使用常用空间和一块保留空间,剩下一块保留空间空闲不使用,回收时,将使用的内存中存活的对象一次性复制到空闲的保留空间里,然后清理使用的常用空间和保留空间。如果在回收时,空闲的保留空间不够用于存储存活的对象时,那么会使用其他内存区域如老年代进行分配担保。
老年代中的对象存活率较高,采用复制算法效率反而会比较低,所以针对这块的垃圾回收一般是直接使用标记-清除算法,或使用标记-整理算法,这种算法也是先标记对象,然后将存活的对象向内存的一端移动,最后直接释放边界外的内存区域即可。
怎么判断一个对象需要回收?
上面说到需要对要回收的对象进行先标记之后才能进行回收操作,那么怎么知道那些对象需要回收呢?
判断对象是否可以继续使用主要与对象的引用有关,下表描述了一个对象的几种引用状态。
引用类型 | 失效时机 | 作用 | 实现 |
---|---|---|---|
强引用 | 一直有效 | 防止一个对象被回收 | 直接引用,如 Object o = new Object() |
软引用 | 内存溢出之前失效 | 防止一个对象导致的内存溢出 | 使用SoftReference类 |
弱引用 | 垃圾收集器工作时失效 | 防止一个对象导致的内存泄漏 | 使用WeakReference类 |
虚引用 | 一直无效 | 可以收到一个对象被回收时的通知 | 使用PhantomReference类 |
判断对象可用状态一般的策略是使用引用计数算法,就是每个对象有一个引用计数器,没当有一个有效的引用时,计数器就加1 ,当引用失效时,计数器就减1,计数器为0的对象就是不能再被使用的,需要被回收。但这种策略会出现对象相互循环引用而无法回收的问题,举例如下:
//a1、a2的引用计数分别为1
A a1 = new A();
A a2 = new A();
//a2被a1的member成员引用,所以a2的引用计数为2
a1.member = a2;
//a1同a2,引用计数为2
a2.member = a1;
//a1、a2对象的引用计数减1,都为1
a1 = null;
a2 = null;
如果这时候采用引用计数法进行垃圾回收操作,那么由于a1和a2对象引用计数不为0,将不会被回收。但其实 Java 虚拟机还是可以回收它们的,这是因为 Java 虚拟机使用了另外一种策略判断对象是否可被回收,叫可达性分析算法。
可达性分析算法是先把一些对象看作是 GC Roots 对象,对象间的引用关系可以看成 GC Roots 作为根节点而每个引用作为子节点的树状结构,如果一个对象在这颗树之外,就是从 GC Roots 向下搜索找不到这个对象,那么可以说 GC Roots 到这个对象不可达,这个对象就是可以被回收的。
总结
本文对 Java 虚拟机中的垃圾回收进行了简单的介绍,在 Java 堆中,它需要先根据对象的引用分析可达性,以标记出要回收的对象,然后使用分代收集算法进行回收,而在方法区中,则是直接回收废弃的常量和类信息,最终达到释放内存空间的目的。