Java垃圾回收

来源:互联网 发布:波司登淘宝旗舰店 编辑:程序博客网 时间:2024/06/05 16:05

垃圾回收(Garbage Collection, GC)是Java语言的主要特性之一,它有效解决了令C++程序员头疼的内存管理问题,有效降低了内存泄漏的风险,提高了内存利用率。下面将从以下几个方便来详细介绍Java的垃圾回收机制。

1. 什么样的对象可称之为“垃圾”

“垃圾”对象是指未被活动对象引用的对象。通常JVM采用两种机制确定一个对象实例是否为“垃圾”对象:引用计数法和可达性分析(根搜索算法)。

1.1引用计数法(Reference Counting Collector)

在Java中通过引用来和对象进行关联,即如果要操作对象,需要将对象实例先赋值给引用,那么,很简单的一个算法就是通过引用计数来判断一个对象是否有在被使用。在Java的早期垃圾回收策略中,就是通过引用计数来进行垃圾回收的。在这种方法中,Java堆中的每一个对象都维持了一个引用计数器,当一个对象被创建并赋值给一个引用后,引用计数器的值会加1。当其它任何引用被赋值为本引用时,引用计数器的值会加1;而当某对象实例的引用超过了其生命周期或被设置为一个新值时,引用计数器的值会减1。当引用计数器的值减为0时,则认为相应的对象可被回收,成为垃圾对象。

引用计数实现简单执行效率比较高,但是,该方法无法解决循环引用的问题。如下例程:

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;    }}class MyObject{    public Object object = null;}

程序将object1和object2赋值为null,但是,由于它们互相引用对方,导致两对象实例的计数均不为0,那么垃圾收集器就永远不会回收它们。

1.2 可达性分析(根搜索)

基本思想:通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的。不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

可达性分析有效解决了循环引用的问题,Java中的GC Roots对象

虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的对象(Native对象)

2. 常见的垃圾回收算法

在确定了垃圾对象后,接下来的任务就是要高效的进行垃圾回收。下面介绍几种典型的垃圾回收算法。

2.1 标记-清除算法(mark-sweep)

标记清除算法分为两个阶段:标记阶段和清除阶段。标记阶段标记需要被回收的对象,清除阶段回收标记对象所占用的内存空间,过程如下:
这里写图片描述
标记清除算法实现简单,但是,容易产生内存碎片化。碎片化太多可能会导致后续过程中为大对象分配内存时,因找不到足够大的内存而提前触发新的一次垃圾回收操作。

2.2 复制算法(copying)

复制算法将内存将内存按容量划分成大小相等的两块,每次只用其中一块。当当前在用的一块内存不足时,会触发一次垃圾回收操作,将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,过程:
这里写图片描述
复制算法解决了内存碎片化的问题,但是需要付出昂贵的内存代价,能够使用的内存减少为原先的一半。

2.3 标记-整理算法(mark-compact)

标记-整理算法在标记阶段采用和标记-清除算法相同的策略,但是,之后该算法会将存活的对象移动到一端,然后清除掉端边界以外的内存,过程如下:
这里写图片描述
标记-整理算法解决了标记-清除算法的碎片化问题及复制算法的内存利用率问题,是一种有效的垃圾回收算法。

2.4 分代收集算法(Generational Collection)

分代收集算法是目前大多数JVM的垃圾收集器采用的垃圾回收算法,其核心思想是根据对象存活周期的不同将内存划分成不同的区域。一般将堆区划分为老年代(Tenured Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象被回收,新生代的特点是每次都有大量对象被回收,因此可以根据不同代的特点,设计相应的垃圾回收算法。
目前大部分垃圾收集器对于新生代都采用Copying算法,因为新生代每次都有大量垃圾对象需要被回收,也就是说需要复制的操作较少。但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,如下:
这里写图片描述
每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
由于老年代每次需要回收的对象较少,因此,一般采用mark-compact算法。在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

3 垃圾收集器

垃圾收集算法是垃圾收集的理论基础,而垃圾收集器则是垃圾回收算法的具体实现。下面就HotSpot(JDK 7)虚拟机提供的几种垃圾收集器作简单的介绍:

3.1 Serial/Serial old

单线程收集器,在其进行垃圾回收时,必须暂停所有用户线程,其中Serial收集器是针对新生代的收集器,采用Copying算法;Serial Old收集器是针对老生代设计的收集器,采用mark-compact算法。该收集器实现简单,但是会造成用户停顿。

3.2 ParNew

Serial收集器的多线程版本。

3.3 Parrllel Scavenge/Parrllel Old

Parrllel Scavenge针对新生代设计的并发多线程收集器,采用Copying算法,回收期间无需暂停用户活动,可以达到一个可控的吞吐量;Parrllel Old是Parrllel Scavenge的老生代版本,采用mark-compact算法。

3.4 CMS(Current mark sweep)

以获取最短回收停顿时间为目的的并发收集器,采用mark-sweep算法。

3.5 G1

并行与并发收集器,面向服务端应用,能充分利用多CPU、多核环境,能建立可预测的停顿时间模型。

GC机制是需要组合使用的,指定方式由下表所示:
这里写图片描述

4 Scavenge GC/ Full GC

目前大多数JVM的垃圾收集器采用的是分代收集策略,由于对象进行了分代处理,因此回收区域、时间也不一样,通常设计两种GC:Scavenge GC和Full GC.

4.1 Scavenge GC

一般情况下,当生成新对象并在Eden区申请空间失败时,就会触发一次Scavenge GC,清除Eden区和当前使用的Suvivor区的非存活对象,并把存活对象复制到另一个Survivor区。

4.1 Full GC

对整个共享区进行整理,包括Young Generation、Tenured Generation、Permanent Generation,运行速度比较慢,因此应尽可能减少Full GC的次数。在JVM调优的过程中,很大一部分工作都是针对Full GC进行的。可能导致Full GC的原因如下:
老年代被写满/持久代被写满/显示调用System.GC/Heap的各域分配策略动态变化
注意:调用System.gc()也仅仅是一个请求(建议)。JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。

5 内存泄漏

Java的垃圾回收机制大大降低了内存泄漏的概率,然而,依然无法完全避免内存泄漏,以下原因依然可能导致内存泄漏:

5.1 静态集合

由于静态集合类(如HashMap、Vector)的生命周期和应用程序一致,所以很容易导致内存泄漏,如下例程:

Static Vector v = new Vector(); for (int i = 1; i<100; i++) {     Object o = new Object();     v.add(o);     o = null; }

在该例程中,代码栈中存有Vector对象的引用v和Object对象的应用o。在for循环中,不断生成新的对象并赋值给o,然后将其添加到Vetor对象中,最后再将o置空。那么,当o置空后,如果发生GC,创建的对象Object能否被GC回收呢?答案是否定的。因为,GC在跟踪代码栈的引用时,会发现v引用,然后继续跟踪下去,就会发现v引用指向的内存空间中又存在指向Object对象的引用。也就是说尽管o引用已经被置空,但是Object对象仍然存在其他引用,即仍然可以被访问到,所以GC仍然无法将其释放掉。如果Object对象对程序已经没有任何作用了,那么我们就说此处存在内存泄漏。

5.2 连接

数据库连接、网络连接、I/O连接等没有显示调用close方法进行关闭,不被GC回收,就有可能导致内存泄漏。

5.2 监听器

在释放对象的同时没有删除监听器也可能导致内存泄漏。

6 GC调优

GC的调优主要是根据GC的工作机制,采取适当的措施以减少GC的触发。

6.1 不要显示调用System.GC

此函数建议JVM执行Full GC,虽然只是一个建议,但是在很多情况下它会增加主GC的频率,也增加了间歇性停顿的次数。

6.2 减少临时对象的使用

临时对象在跳出函数后就会成为垃圾,少用临时对象就相当于减少了垃圾的产生,从而减少了Full GC发生的概率。

6.3 对象不用时显示地设置为null

一般null对象会被当作垃圾处理,所以,将不用的对象显示地设置为null,有利于GC收集器判定垃圾,从而提高了GC的效率。

6.3 用StringBuffer代替String

由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

6.4 使用基本数据类型

基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

6.5 少用静态变量

静态变量属于全局变量,不会被GC回收,它们会一直占用内存

6.6 分散对象创建/删除的时间

集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

7 参考文献

(1)Java垃圾回收机制
(2)Java垃圾回收机制
(3)深入理解Java垃圾回收机制

0 0
原创粉丝点击