第2章 Java内存区域与内存溢出异常
2.2 运行时数据区域
程序计数器 :为当前线程所执行的字节码的行号执行器。每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,这类内存区域成为“线程私有 ”的内存。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址 ;如果正在执行的是Native方法,这个计数器则为空(Undefined) 。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域 Java虚拟机栈 :线程私有,它的生命周期与线程相同 。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧 用于存储局部变量表、操作数栈、动态链接、方法出口 等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程 。局部变量表存放了八种基本数据类型、对象引用和返回地址类型 。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余数据类型只占用1个 。局部变量表所需的内存空间在编译期间即完成分配 。在Java虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常 ;如果虚拟机动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常 本地方法栈 :与虚拟机栈所发挥的作用非常相似。只不过虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法 服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常 Java堆 :Java堆是被所有线程共享 的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例(包括数组)都在这里分配内存。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB) 。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可 。如果堆中没有内存用以完成实例分配,并且堆也无法扩展时,将会抛出OutOfMemoryError 异常方法区 :是各个线程共享 的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码 等数据。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载 。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常运行时常量池 :运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池 ,用于存放编译期生成的各种字面量和符号引用 ,这部分内容将在类加载后进入方法区的运行时常量池中存放 。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性 ,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中 ,比如String类的intern()方法 。当常量池无法再申请到内存时会抛出OutOfMemoryError 异常直接内存 :直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但也可能导致OutOfMemoryError异常。直接内存的分配不会受到Java堆大小的限制,但是仍会受到本机总内存大小以及处理器寻址空间的限制
2.3 HotSpot虚拟机对象探秘
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用 ,并且检查这个符号引用代表的类是否已被加载、解析和初始化过 。如果没有,那必须先执行相应的类加载过程。在类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定 ,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来 指针碰撞:假设Java堆中内存是绝对规整 的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离 ,这种分配方式称为“指针碰撞” 空闲列表:如果Java堆中的内存并不是规整 的,已使用的内存和空闲的内存相互交错。此时虚拟机必须维护一个列表 ,记录哪些内存块 是可用的,在分配的时候从列表中找到一块足够大 的空间划分给对象实例,并更新列表上的记录 。这种分配方式称为“空闲列表” 选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定 对于在并发情况下在虚拟机中创建对象的解决方案:方案一是对分配内存空间的动作进行同步处理----实际上虚拟机采用CAS(Compare And Swap)配上失败重试的方式更新操作的原子性 ;方案二是把内存分配的动作按照线程的不同划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定 内存分配完成后,虚拟机需要将分配到内存空间都初始化为零值 (如boolean 的零值为fasle,int的零值为0 )。如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用 接下来,虚拟机将对对象头 的信息进行必要的设置。如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄以及是否启用偏向锁等信息 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头 、实例数据 和对齐填充 HotSpot虚拟机的对象头包括两部分信息:第一部分用于存储对象自身的运行时数据 ,如哈希码 、GC分代年龄 、线程持有的锁 、锁状态标志 、偏向线程ID 、偏向时间戳 等。对象头信息是与对象自身定义的数据无关的额外存储成本 。第二部分是类型指针,即对象指向它所在类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例 。但并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身(比如说对象访问方式为使用句柄访问,对象只存储其实例数据,对象类型数据指针在句柄池中 )。另外,如果对象是一个Java数组,那么对象头中还必须有一块记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小(所以需要事先知道数组大小) 实例数据部分是对象真正存储的有效信息,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 。这部分的存储顺序会受到虚拟机分配策略参数 和字段在Java源码中定义顺序 的影响。HotSpot虚拟机默认分配的策略为 longs/doubles、ints、shorts/chars、bytes、booleans、oops(Ordinary Object Pointers) 。在满足这个前提条件的情况 下,在父类中定义的变量会出现在子类之前 。如果CompactFields 参数值为true(默认为true),那么子类之中较窄的变量也可能会插入到父类变量的空隙之中 对齐填充并不是必然存在的,只是起着占位符 的作用。由于HotSpot VM 的自动内存管理系统要求对象其实地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍 。而对象头部分正好是8字节的倍数(32bits或者是64bits,根据操作系统而定) ,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 通过栈上的reference数据来操作堆上的具体对象。目前主流的对象访问方式有使用句柄访问 和直接指针访问 两种 使用句柄访问:Java堆将会划分出一块内存来作为句柄池 ,reference中存储的就是对象的句柄地址 ,而句柄中包含了对象实例数据与类型数据各自的具体信息
使用直接指针访问:reference中存储的直接是对象地址
两种对象访问方式的比较:使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改 ;使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销 。HotSpot虚拟机主要使用第二种方式对对象进行访问
2.4 实战:OutOfMemoryError异常
Java堆溢出:无限new 对象即可。限制Java堆大小为20MB,不可扩展(将堆的初始值-Xms参数与最大值-Xmx参数设置为一样即可避免自动扩展)
/*
VM args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class Test {
static class OOMObject{
}
public static void main(String[] args) throws Exception {
List<OOMObject> list = new ArrayList<>();
while (true){
list.add(new OOMObject());
}
}
}
虚拟机栈和本地方法栈溢出:在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈 ,栈容量只由-Xss 参数设定。在单线程采用方法循环调用自己 ,虚拟机抛出的都是StackOverflowError异常。而使用多线程时则会抛出OOM异常
/*
VM args:-Xss128k
*/
public class Test {
private int stacklength = 1;
public void stackLeak() {
stacklength++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
Test oom = new Test();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stacklength);
throw e;
}
}
}
方法区和运行时常量池溢出:在JDK1.7之后,开始去永久代,常量池中存放的不是对象,而是对象的引用,真正的对象是存储在堆中 。 intern 方法:
public class Test {
public static void main(String[] args) {
String str1 = new StringBuffer("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);//true
String str2 = new StringBuffer("ja").append("va").toString();
System.out.println(str2.intern() == str2);//false
}
}
针对上例:调用字符串对象的 intern 方法时,首先会去字符串常量池中查看是否有与当前字符串对象值相等的字面量或者对象(使用equals判断)。如果没有,则将该字符串(调用者)对象添加到字符串常量池(其实只是将该对象的引用加入到了常量池),并返回该对象的引用;如果有,则将该字符串(或字符串的引用)返回。第二个结果为false是因为在类加载或编译期间时,关键字“java”就已经进入了字符串常量池中了 本机直接内存溢出:DirectMemory 容量可通过 -XX:MaxDirectMemorySize 指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样
第3章 垃圾收集器与内存分配策略
3.2 对象已死吗
引用计数法 :给对象添加一个引用计数器,每当一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。简单但很难解决对象之间相互循环引用的问题 。循环引用代码:
/*
Java虚拟机并不是通过该算法来判断对象是否存活
VM args:-verbose:gc -XX:+PrintGCDetails
*/
public class Test {
public Object instance = null;
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
a = null;
b = null;
System.gc();
}
}
可达性分析算法 :通过一系列的称为“GC Roots ”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链 ,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。在Java语言中,可作为GC Roots 的对象包括以下几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象 方法区中类静态属性 引用的对象 方法区中常量 引用的对象 本地方法栈中的本地方法引用的对象
强引用:指类似“Object obj = new Object()”这类的引用 ,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象,即使发生内存溢出,也不会回收这类对象 软引用:描述一些还有用但并非必需的对象 。对于软引用关联着的对象,在系统将要发出内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收 。如果这次回收还没有足够的内存,才会抛出内存溢出异 常。使用SoftReferenc e类来实现软引用 弱引用:也是用来描述非必需对象 的,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联着的对象 。使用WeakReference 类实现 虚引用:也称幽灵引用 或者幻影引用 。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过一个虚引用来取得一个对象实例 。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知 。使用PhantomReference 类实现
即使在可达性分析算法中不可达的对象,也并非是“非死不可” 。要真正宣告一个对象死亡,至少要经历两次标记 过程:如果对象在进行可达性分析后发现没有与GC Roots 相关联的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法 。当对象没有覆盖 finalize 方法或者 finalize 方法已经被虚拟机调用过,虚拟机则认为没有必要执行。(相当于finalize方法是一个复活甲,且只能用一次) 在执行finalize方法时,这个对象会被放入到一个叫做F-Queue 的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程 去执行(虚拟机只会触发这个方法,并不承诺会等待它运行结束 )它。如果在finalize方法里这个仍未与GC Roots上的对象建立关联,则它将会被真正回收 。finalize方法验证代码如下(不建议使用finalize方法,了解就行 ):
/*
输出:
finalizer method executed!
yes,i am alive
no,i am dead
*/
public class Test {
public static Test SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes,i am alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalizer method executed!");
SAVE_HOOK = this;//谁调用finalize方法谁就是this
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new Test();
SAVE_HOOK = null;
System.gc();
//finalizer方法优先级很低,所以暂停0.5s等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no,i am dead");
}
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no,i am dead");
}
}
}
废弃常量(以字符串常量池为例,类、方法、字段的符号引用与此类似):在常量池中的字符串没有被任何引用变量引用
String string = "123";
string = null;//位于字符串常量池中的字符串"123"将被回收
System.gc();
无用的类 :类需要同时满足以下3个条件才能算是“无用的类”:
该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例 加载该类的ClassLoader已经被回收 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
3.3 垃圾收集算法
标记-清除算法(Mark-Sweep) :首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足有两个:一个是效率问题 ,标记和清除两个过程的效率都不高;一个是空间问题 ,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集复制算法(Copying) :为了解决标记-清除算法的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块 。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把之前那块内存空间一次清理掉。实现简单,运行高效。只是这种算法的代价是将可用内存缩小为了原来的一半 现在的商业虚拟机都是采用复制算法回收新生代 。在新生代中,将内存划分为一块较大的Eden空间 和两块较小的Survivor空间 ,每次使用Eden和其中一块Survivor 。当回收时,将Eden和使用的Survivor中还存活的对象根据其年龄值大小 和进入老年代的阈值 标准判断是复制到另一块Survivor空间还是直接进入老年代(也可以通过分配担保机制 进入老年代),最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认 Eden : Survivor = 8 : 1 (有两块Survivor) 标记-整理算法(Mark-Compact) :标记过程仍然与标记-清除算法一样,之后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存 (冒泡排序?)分代收集算法:根据对象存活周期的不同将内存划分为几块。在新生代使用复制算法进行垃圾收集,在老年代使用“标记-清除”或者“标记-整理”算法进行垃圾收集
3.4 HotSpot的算法实现
枚举根节点 :GC进行时必须停顿所有Java执行线程(Stop The Word) 。在HotSpot的实现中,使用一组称为OopMap 的数据结构来得知哪些地方存放着对象引用安全点 :只有在到达安全点时,程序才能停顿下来开始GC,才能开始枚举根节点操作 。具有以下功能的指令会产生安全点:方法调用、循环跳转、异常跳转 等抢先式中断和主动式中断??? 安全区域 :在一段代码片段当中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的 。在线程执行到Safe Region 中的代码时,首先标识自己已经进入了Safe Region。那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region 状态的线程了。在线程要离开 Safe Region 时,它要检查系统是否已经完成了枚举根节点(或者是整个GC过程) ,如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止
3.5 垃圾收集器
垃圾收集器中的并行与并发解释:并行指多条垃圾收集器并行工作,但此时用户线程仍然处于等待状态 ;并发指用户线程与垃圾收集线程同时执行(但并不一定是并行的,可能会交替执行,执行在不同的CPU之上) 新生代收集器:
Serial 收集器 :最基本、发展历史最悠久的收集器,Client 模式下虚拟机默认新生代收集器 。单线程收集器,“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束 。采用复制算法,暂停所有用户线程ParNew 收集器 :Serial 收集器的多线程版本 ,是许多运行在Server模式下的虚拟机中首选的新生代收集器 。若老年代使用CMS收集器,则新生代默认是ParNew收集器 。采用复制算法,暂停所有用户线程。Parallel Scavenge 收集器 :以上两个收集器关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而该收集器的目标则是达到一个可控制的吞吐量 。吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间) 。该收集器还有一个参数为 -XX:+UseAdaptiveSizePolicy ,当这个参数打开时,虚拟机会动态调整参数以提供最合适的停顿时间或者最大的吞吐量。采用复制算法,暂停所有线程
Serial Old 收集器 :Serial 收集器的老年版本,单线程收集器,使用“标记-整理 ”算法,暂停所有用户线程Parallel Old 收集器 :Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法 ,暂停所有用户线程,一般配合Parallel Scavenge 收集器使用 CMS(Concurrent Mark Sweep) 收集器 :使用“标记-清除 ”算法。整个过程分为四步:初始标记 、并发标记 、重新标记 、并发清除 。其中,初始标记和重新标记这两个步骤仍然需要STW 。初始标记仅仅只是标记一下GC Roots能直接关联到的对象 ,并发标记阶段就是进行GC Roots Tracing 的过程 ,而重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录 ,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短
CMS 收集器对CPU资源非常敏感 ,在并发阶段,GC线程会与用户线程争夺CPU资源CMS 收集器无法处理浮动垃圾 。浮动垃圾:在CMS重新标记过程之后产生的垃圾称为浮动垃圾 。如果CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案 :临时启用Serial Old收集器来重新进行老年代的垃圾收集 CMS 是基于“标记-清除 ”算法实现的收集器,收集结束时会有大量空间碎片 产生。往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不以前触发一次Full GC
并行与并发 分代收集 空间整合 :G1从整体来看是基于“标记-整理”算法 实现的收集器,从局部(两个Region)上来看是基于“复制”算法 实现。这两种算法都意为着G1运作期间不会产生内存空间碎片可预测的停顿 :G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒
使用G1收集器时,它将这个Java堆划分为多个大小相等的独立区域(Region) ,虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合 G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region 在G1收集器中Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set 来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set ,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier 暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象) ,如果是,便通过CardTable 把相关引用信息记录到被引用对象所属的Region的Remembered Set之中 。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏 G1收集器运作四步骤:初始标记 、并发标记 、最终标记 、筛选回收
3.6 内存分配与回收策略
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC 新生代GC(Minor GC) :指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快老年代GC(Major GC / FULL GC) :指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的 Minor GC(并非绝对) 。Major GC的速度一般会比Minor GC慢10倍以上大对象直接进入老年代 :所谓大对象指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。可以通过参数-XX:PretenureSizeThreshold=???(单位为B,字节)来设置大对象的判定标准 虚拟机给每个对象定义了一个对象年龄计数器 ,用来识别该对象是否应该转移到老年代。对象晋升老年代的年龄阈值 ,可以通过参数-XX:MaxTenuringThreshold 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半 ,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄 分配失败时进行GC,先GC完后再分配,所以GC显示出的信息是分配成功之前(即还未分配时)的堆信息 空间分配担保:在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间 ,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure 设置值(true或者false)是否允许失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小 ,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于或者HandlePromotionFailure设置不允许冒险,那这时要改为进行一次Full GC JDK6 Update 24之后的规则变为了只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行 Full GC