03 JVM的垃圾回收机制
1、前言
理解JVM的垃圾回收机制(简称GC)有什么好处呢?作为一名软件开发者,满足自己的好奇心将是一个很好的理由,不过更重要的是,理解GC工作机制可以帮助你写出更好的Java程序。
在学习GC前,你应该知道一个技术名词:“stop-the-world” ,无论你选择哪种GC算法,“stop-the-world”都会发生。“stop-the-world”意味着JVM停止应用程序,而去进行垃圾回收。当“stop-the-world”发生时,除了进行垃圾回收的线程,其他所有线程都将停止运行。被中断的任务将在GC任务完成后恢复执行。GC调优往往意味着减少“stop-the-world”的时间。
2、分代垃圾收集机制
在HotSpot虚拟机中,将内存分为 年轻代(young generation)、老年代(old generation) 和 永久代(permanent generation)
我们看一下这幅图:
年轻代: 新创建的对象都存放在这里。因为大多数对象很快变得不可达,所以大多数对象在年轻代中创建,然后消失。当对象从这块内存区域消失时,我们说发生了一次“minor GC”。
老年代: 没有变得不可达,存活下来的年轻代对象被复制到这里。这块内存区域一般大于年轻代。因为它更大的规模,GC发生的次数比在年轻代的少。对象从老年代消失时,我们说“major GC”(或“full GC”)发生了。
永久代: 永久代(permanent generation) 也称为“方法区(method area)”,它存储class对象和字符串常量。所以这块内存区域绝对不是永久的存放从老年代存活下来的对象的。发生在这里的垃圾回收也被称为major GC。
3、垃圾收集算法
垃圾收集算法如下:
- 年轻代-复制算法
- 老年代-标记清除算法
- 老年代-标记整理算法
- 永久代-方法区回收
年轻代-复制算法
该算法的核心是将可用内存按容量划分为大小相等的两块,每次只用其中一块,当这一块的内存用完,就将还存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。这使得每次只对其中一块内存进行回收,分配也就不用考虑内存碎片等复杂情况,实现简单且运行高效。如下图:
老年代-标记清除算法
该算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象(可达性分析),在标记完成后统一清理掉所有被标记的对象。如下图:
该算法会有以下两个问题:
- 效率问题:标记过程和清除过程的效率都不高;
- 空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。
老年代-标记整理算法
标记清除算法会产生内存碎片,而复制算法需要有额外的内存担保空间,于是针对老年代的特点,又有了标记整理算法。标记整理算法的标记过程与标记清除算法相同,但后续步骤不再对可回收对象直接清理,而是让所有存活的对象都向一端移动,然后清理掉这一端边界以外的内存。如下图:
永久代-方法区回收
在方法区进行垃圾回收一般“性价比”较低,因为在方法区主要回收两部分内容:废弃常量 和 无用的类 。
回收废弃常量与回收其他年代中的对象类似。
但是判断一个类是不是无用的类的条件则相当苛刻:
- 该类所有的实例都已经被回收,Java堆中不存在该类的任何实例;
- 该类对应的Class对象没有在任何地方被引用;
- 加载该类的ClassLoader已经被回收;
但即使满足以上条件也未必一定会回收,Hotspot VM还提供了-Xnoclassgc参数控制(关闭CLASS的垃圾回收功能)。因此在大量使用动态代理、CGLib等字节码框架的应用中一定要关闭该选项,开启VM的类卸载功能,以保证方法区不会溢出。
4、垃圾回收的两个重要方法
System.gc()方法 和 finalize()方法
System.gc()方法
使用System.gc() 可以不管JVM使用的是哪一种垃圾回收的算法,都可以请求Java的垃圾回收。在命令行中有一个参数-verbosegc
可以查看Java使用的堆内存的情况,它的格式如下:java -verbosegc classfile
由于这种方法会影响系统性能,不推荐使用,所以不详细介绍。
finalize()方法
JVM垃圾回收器在回收一个对象之前,一般要求程序调用适当的方法释放资源,但在没有明确释放资源的情况下,Java提供了缺省机制来终止该对象从而释放资源,这个方法就是finalize() 。它的原型为:protected void finalize() throws Throwable
在finalize() 方法返回之后,对象消失,垃圾收集开始执行。原型中的throws Throwable 表示它可以抛出任何类型的异常。
之所以要使用finalize(),是因为存在着垃圾回收器不能处理的特殊情况。例如:
-
由于在分配内存的时候可能采用了类似 C语言的做法,而非Java通常的new 。这种情况主要发生在 native 方法中,比如 native 方法调用了
C/C++
的malloc()
函数来分配存储空间,除非调用 free() 函数,否则这些内存空间将不会得到释放,那么这个时候就可能造成内存泄漏。但是由于 free() 是C/C++
中的函数,所以 finalize() 中可以用本地方法来调用它。以释放这些“特殊”的内存空间。 -
或者是打开的文件资源,这些资源不属于垃圾回收器的回收范围。
5、触发GC(Garbage Collector)的条件
-
GC在优先级最低的线程中运行,一般在应用程序空闲即没有应用线程在运行时被调用。但下面的条件例外。
-
Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制调用GC线程。若GC一次之后仍不能满足内存分配,JVM会再进行两次GC,若仍无法满足要求,则JVM将报“out of memory”的错误,Java应用将停止。
6、减少GC开销的措施
-
不要显式调用System.gc() 。此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。大大的影响系统性能。
-
尽量减少临时对象的使用。临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。
-
对象不用时最好显式置为Null。一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。
-
尽量使用StringBuffer,而不用String来累加字符串。由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如
Str5=Str1+Str2+Str3+Str4;
这条语句执行过程中会产生多个垃圾对象,因为每次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。 -
能用基本类型如
int, long
就不用包装类型Integer, Long
。基本类型变量占用的内存资源比包装类型占用的少得多,如果没有必要,最好使用基本变量。 -
尽量少用静态对象变量。静态变量属于全局变量,不会被GC回收,它们会一直占用内存。
-
分散对象创建或删除的时间。集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。
7、几种垃圾收集器
在JDK7中,有5种垃圾收集器:
- Serial收集器
- Parallel收集器
- Parallel Old收集器 (Parallel Compacting GC)收集器
- Concurrent Mark & Sweep GC (or “CMS”)收集器
- Garbage First (G1) 收集器
其中,Serial 收集器一定不能用于服务器端。这个收集器类型仅应用于单核CPU桌面电脑。使用Serial收集器会显着降低应用程序的性能。