Java 垃圾回收机制 [ 内存管理 GC]
一. 什么是Java垃圾回收
当新建一个对象的时候,一个变量指向该对象,同时系统会在堆中分配一块空间给该对象;当对象不再被任何变量引用的时候,这块内存就成为了垃 圾,等待GC回收。因为在Java中没有像C++中那样的delete语句来完成内存回收。其本意是想代码编写者可以更方便的实现自己的功能,不用考虑内 存的问题。但事实并非如此。
二. GC的特征
1. GC 只负责回收堆内存中的对象,而不会回收物理资源;
2. GC 无法被代码编写者精确控制,之知道当变量不再不被引用时才会被回收;
3. GC在回收任何对象之前,总会调用它的finalize 方法,该方法可能是该对象重新激活,从而导致回收失败;
三. GC机制
在开始了解GC机制之前,我们先来看一下在Java中的内存分配机制。
每当申请一个Java 线程时,它拥有自己的内存栈(stack).用来存放局部变量和返回值;在栈里的数据名字是已经舍弃了的,其名字由JVM自己生成的;栈是在线程启动时分配的。
但所有的线程共享一个内存堆(heap),所有运行时的内存分配都在堆上进行.换句话说就是所有的对象都是在堆上创建的。
其中堆栈的大小可以在Java虚拟机启动时,在JAVA_OPTS进行配置:
-Xms:初始Heap大小,使用的最小内存,cpu性能高时此值应设的大一些;
-Xmx:java heap最大值,使用的最大内存;
(上面两个值是分配JVM的最小和最大内存,取决于硬件物理内存的大小,建议均设为物理内存的一半。)
-Xss:每个线程的Stack大小;
那我们看看GC到底是负责那方面的内存回收:
static 变量是在在静态存储区域分配的,内存在程序编译时就分配好了,当程序死亡时,才回去进行释放。
各种原始数据类型的局部变量,都是在栈上创建的。当程序退出该变量的作用范围时,这些变量的内存会被自动释放。
对象都是在堆中创建的,程序运行的时候用new 创建对象,对象创建时会在堆中0为其分配内存。
可以看到在堆中的内存是没有一个明确的释放时间的,其空间由自动的存储管理系统进行控制.也就是Garbage Collector。垃圾收集器通常作为一
个独立的线程运行.
(一) 分代复制垃圾收集器
JVM使用分代复制(generational copying)算法。分代复制算法基于这样一个事实——超过95%的对象的生存期都非常 短。分代复制算法根据对象的生存期将对象分为两代。所有新创建的对象都在一个栈结构的内存区域进行分配,这块区域叫做eden。这首先使得内存分配的速度 提高了。因为此时只需要更新eden 的指针和检查eden是否溢出即可。当eden 区域已经全部分配给对象时,大部分对象已经”死亡”。垃圾收集器只 有将少量未死亡的长期对象(tenured 对象)复制到另一块内存中去,然后直接更新eden 的指针即可。其分代是根据该段内存生命周期的长短来划分 的。
生存于eden 中的对象称为新生代(young generation)。
生命较长的对象区域称为旧生代(old generation)。
其中复制是指,当eden 的内存区域分配完毕,GC 就会对eden 进行一次次要垃圾收集(minor collection),将仍然被引用的对象复制到old generation,并更新指向这些对象的引用,然后将eden 全部清空。
分代复制的效率非常高,因为清空eden花费的时间极少,主要的时间花费在复制上。因此,适当调整eden 的大小,增长或缩短较小的垃圾 收集发生的周期,是的较小的垃圾收集发生时。更多的对象已经死亡,从而减少需要复制的对象的数量,加快程序的速度。当old generation 的内 存区域全部分配完时,垃圾收集器会进行一次主要垃圾收集(major collection)。主要垃圾收集通常比次要垃圾收集慢,因为所有”存活”的对 象都会被遍历到。可见次要垃圾回收是指将“存活”对象复制到old generation 内存区域中。
其中 young generation = eden + survivor space*2
(二) 标记垃圾收集器
在旧生代中,通常采用标记垃圾收集器,这种垃圾收集器从一组根引用开始,遍历所有的对象,如果一个对象被根引用,那么就标记为“存活”;而 存活对象引用的对象也被标记为“存活”,如此循环递归,其余对象则被标记为“死亡”。死亡的对象将会被回收。根据对存活对象处理方法的不同,标记垃圾收集 器又分为两类:
1. 标记紧缩垃圾收集器(mark-and-compact collector) 将所有存活的对象复制到一个连续的内存区域中,因此可以有效地减少内存碎片。
2. 标记清除垃圾收集器(mark-and-sweep collector) 保留所有的存活对象,而将所有的死亡对象的内存空 降记录到一个自由空间列表中,虽然连续的死亡对象空间会被合并,但与标记紧缩垃圾收集器相比,这种方法仍然会产生较多的内存碎片。
(三) 增量垃圾收集器
运行标记紧缩垃圾收集器,或者标记清除垃圾收集器,对运行的程序来说,都是一场灾难.因为这些垃圾收集器在运行的时候,会停止JVM 中其 他程序的线程。而且这些垃圾收集器每次都会收集尽可能多的垃圾内存。所以每次停顿的时间是不可预测的。分代复制垃圾收集器的情况稍好一点,但也会造成明显 的停顿。
增量垃圾收集器能够提供接近于常数的固定的暂停时间,这个暂停短到用户可以忽略它,因此增量垃圾收集器特别适用于操纵大量数据的应用程序, 以及对交互性要求比较高的程序。它的基本原理是时间较长的旧生代的垃圾收集分为许多的间隔来完成,每次只收集一部分内存。对于用户来说好像完全没有停顿一 样。但是增量垃圾收集器并不能精确地保证停顿的时间,根据JVM 的实现可以应用与不同要求。
(四) 次要收集和主要收集
次要收集: 当eden 空间被分配完时,就会发生一次次要垃圾收集(minor collection)。eden中仍然存活的对象会被 复制到survivor space1 中,其他对象直接丢弃,其占用的内存被回收。在一次次要垃圾回收之后,JVM 继续在eden中创建对象。当 eden的空间再次被分配完的时候,又会发生一次次要垃圾收集。将eden中存活的对象复制到另一个survivor space2 中,并且为 survivor中的每一个对象计算年龄age和存活期threshold。age是对象在复制到old generation之前经历过的次要垃圾收集 的次数。threshold则是表示在这一次的次要垃圾收集将会被复制到old generation的对象的age。age 小于 threshold 的将会被复制survivor space2 中这些对象被称为aged 对象(老化对象)。而清空的 survivor space1 为下一次的次要垃圾收集中复制的目的地。
主要收集:在旧生带中的内存也告罄的时,就会发生一次主要垃圾收集。主要垃圾收集采用标记紧缩的方法,在旧生带中标记出所有存活的对象,然后将其它对象回 收.主要垃圾收集通常要比次要垃圾收集的时间长的多,因为标记和回收都是很费时的操作。在典型的配置中,旧生带也比新生带要大(以达到多进行次要收集少进 行主要收集的目的)。
主要参考: