垃圾收集器与内存分配策略
说起垃圾收集(Garbage Collection,GC),不得不思考GC需要完成的3件事情:
哪些内存需要回收?
什么时间回收?
如何回收?
程序计算器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(不考虑JIT优化的情况下),所以这几个区域的内存分配和回收都具备确定性,在这几个区域就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而Java堆和方法区则不一样,一个接口的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序运行期才能知道创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存。
对象已死吗
1. 引用计数算法(Reference Counting)
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器的值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:实现简单,判定效率很高。
缺点:不能解决对象之间互相循环引用的问题。
1 /** 2 * Created by sakura on 2017/12/9. 3 */ 4 public class ReferenceCountingGC { 5 public Object instance=null; 6 private static final int _1MB=1024*1024; 7 /* 8 这个成员属性的唯一意义就是占点内存,以便在GC日志中看清楚是否被回收过 9 */ 10 private byte[] bigSize=new byte[2*_1MB]; 11 public static void main(String[] args) { 12 ReferenceCountingGC objA=new ReferenceCountingGC(); 13 ReferenceCountingGC objB=new ReferenceCountingGC(); 14 objA.instance=objB; 15 objB.instance=objA; 16 objA=null; 17 objB=null; 18 System.gc(); 19 } 20 }
GC日志中的7437K->848K,意味着虚拟机并没有因为这两个对象互相引用就不回收它们,这也侧面说明了虚拟机并不是通过引用计数算法来判断对象是否存活的。
2. 可达性分析算法 (Reachability Analysis)
通过一系列的GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地向量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
3. 再谈引用
在JDK1.2之前,Java中引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但不够细致。在JDK1.2之后,Java对引用的概念进行扩充,引用被分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
强引用:只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
1 Object object = new Object(); 2 String str = "hello";
软引用:用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示且只有在内存不足时JVM才会回收被软引用关联的对象。这个特性比较适合实现缓存。
1 import java.lang.ref.SoftReference; 2 /** 3 * Created by sakura on 2017/12/9. 4 */ 5 public class SoftRef { 6 public static void main(String[] args) { 7 SoftReference<String> str=new SoftReference<String>("hello"); 8 System.out.println(str.get());//hello 9 System.gc(); 10 System.out.println(str.get());//hello 11 } 12 }
弱引用:也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被软引用关联的对象只能生存到下一次垃圾收集发生之前。当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在JDK1.2之后提供了WeakReference类来实现弱引用。弱引用能用来在回调函数中防止内存泄露。
1 import java.lang.ref.WeakReference; 2 /** 3 * Created by sakura on 2017/12/9. 4 */ 5 public class WeakRef { 6 public static void main(String[] args) { 7 WeakReference<String> str=new WeakReference<String>("hello"); 8 System.out.println(str.get()); 9 System.gc(); 10 System.out.println(str.get()); 11 } 12 }
虚引用:也称幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
4. 生存还是死亡
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经理两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机把这两种情况都视为“没有必要执行”。如果这个对象被判定为有必要执行finalize()方法,那么这个对象会被放置在一个叫F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束(防止发生死循环)。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC会对F-Queue中的对象进行第二次标记,如果对象在finalize()方法中成功拯救自己——只要重新和引用链上的任何一个对象建立关联即可,那么在第二次标记时它会被移除出“即将回收”的集合,如果对象这个时间点还没有逃脱,那基本上它就真的被回收了。
1 /** 2 * Created by sakura on 2017/12/9. 3 */ 4 /* 5 1. 对象可以在被GC时自我拯救 6 2. 这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统调用一次 7 */ 8 public class FinalizeEscapeGC { 9 public static FinalizeEscapeGC SAVE_HOOK=null; 10 public void isAlive(){ 11 System.out.println("yes, i am still alive :)"); 12 } 13 @Override 14 protected void finalize() throws Throwable{ 15 super.finalize(); 16 System.out.println("finalize method executed!"); 17 FinalizeEscapeGC.SAVE_HOOK=this; 18 } 19 public static void main(String[] args) throws InterruptedException { 20 SAVE_HOOK =new FinalizeEscapeGC(); 21 //对象第一次成功拯救自己 22 SAVE_HOOK=null; 23 System.gc(); 24 //因为finalize方法优先级很低,所以暂停0.5秒以等待它 25 Thread.sleep(500); 26 if(SAVE_HOOK!=null) 27 SAVE_HOOK.isAlive(); 28 else 29 System.out.println("no, i am dead :("); 30 //下面这段代码与上面的完全相同,但是这次自救却失败了 31 SAVE_HOOK=null; 32 System.gc(); 33 //因为finalize方法优先级很低,所以暂停0.5秒以等待它 34 Thread.sleep(500); 35 if(SAVE_HOOK!=null) 36 SAVE_HOOK.isAlive(); 37 else 38 System.out.println("no, i am dead :("); 39 } 40 } 41 /* 42 finalize method executed! 43 yes, i am still alive :) 44 no, i am dead :( 45 */
5. 回收方法区
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符创“abc”已经进入了常量池中,但是当前系统没有任何一个String对象叫“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”对象,也没有其他地方引用了这个字面量,如果这时发生GC且有必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻很多,对“无用的类”的判定需要同时满足下面3个条件:
此类所有的实例都已经被回收,也就是Java堆中不存在此类的任何实例。
加载此类的ClassLoader已经被回收。
此类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问到此类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。
垃圾收集算法
1. 标记-清除算法(Mark-Sweep)
标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。这是最基础的收集算法,后续的收集算法都是基于这种思路并对其不足进行改进而得到的。它的不足主要有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,导致后续内存分配时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2. 复制算法(Copying)
为了解决效率问题,一种称为“复制”的收集算法出现了,它把可用内存按容量划分为大小的两块,每次只使用其中的一块。当这一块的内存用完了,就把还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:不用考虑内存碎片的情况,实现简单,运行高效。
缺点:把内存缩小为原来的一半,代价有点高。
现在的商业虚拟机都采用这种收集算法来回收新生代,研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是把内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,把Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion)。
3. 标记-整理算法(Mark-Compact)
复制收集算法在对象存活率较高时就要进行较多的复制操作,性能会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所有在老年代一般不能直接选用这种算法。有人提出另一种“标记-整理”算法,标记过程仍然和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4. 分代收集算法(Generational Collection)
根据对象存活周期的不同把内存分为几块。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高且没有额外空间对它进行分配担保,就必须使用标记-清理或者标记-整理算法来进行回收。
HotSpot的算法实现
1. 枚举根节点
可达性分析的时间停顿,因为这项分析工作必须在一个能确保一致性的快照中进行,这里一致性的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况。这点是导致GC进行时必须停顿所有Java执行线程(Stop The World)的其中一个重要原因。在执行系统停下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机通过OopMap数据结构直接得知哪些地方存放着对象引用。
2. 安全点
HotSpot没有为每条指令都生成OopMap,只是在特定位置记录了这些信息,这些位置被称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来GC,只有在到达安全点时才能暂停。
3. 安全区域
线程处于Sleep状态或者Blocked状态时,无法响应JVM的中断请求,走到安全的地方去中断挂起,对于这种情况,就需要安全区域(Safe Region)来解决。 安全区域是指在一段代码片段之中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
HotSpot虚拟机的垃圾收集器
1. Serial收集器
最基本、发展历史最悠久的收集器。是一个单线程的收集器,这里的单线程并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
Serial是虚拟机运行在Client模式下的默认新生代收集器。它的优点是简单而高效(与其他收集器的单线程比)。
2. ParNew收集器
其实就是Serial收集器的多线程版本。除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。
ParNew是虚拟机运行在Server模式下的首选新生代收集器。一个与性能无关的原因是除了Serial收集器外,目前只有它能与CMS收集器配合工作。CMS收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
注意并行与并发是两个容易混淆的概念:
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
3. Parallel Scavenge收集器
是一个新生代收集器,也是使用复制算法且并行的多线程收集器。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),所谓吞吐量就是CPU用于运行用户代码的时间和CPU总消耗时间的比值。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
4. Serial Old收集器
是Serial收集器的老年代版本。主要有两大用途:一种用途是在JDK1.5之前与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
5. Parallel Old收集器
是Parallel Scavenge收集器的老年代版本。使用多线程和标记-整理算法,这个收集器是在JDK1.6中才开始提供的。
6. CMS收集器
即Concurrent Mark Sweep收集器,是一种以获取最短回收停顿时间为目标的收集器。可以提高服务的响应速度,带给用户较好的体验。CMS是基于标记-清除算法实现的,运行过程分为4个步骤:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍需要STW。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续寻做而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
优点:并发收集、低停顿。
缺点:
a. CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
b. CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,只好留待下一次GC时再清理掉。这一部分垃圾就成为浮动垃圾。也是由于在垃圾收集阶段用户线程还需要运行,那就还需要预留有足够的内存空间给用户线程使用,所以CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集。
c. 标记-清除算法会产生大量的内存碎片,会给接下来的内存分配带来麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
7. G1收集器
即Garbage-First收集器,是一款面向服务器端的垃圾收集器,具有以下特点:
并行与并发:充分利用多核优势缩短STW停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
分代收集:采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:与CMS的标记-清理算法不同,G1从整体上看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于复制算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。
可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
G1收集器的运作大致可划分为以下几个步骤(不考虑维护Remembered Set的操作):
初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)
初始标记阶段仅仅只是标记以下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时很长,但可与用户程序并发执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机把这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划(这个阶段也可以与用户线程一起并发执行)。
8. GC日志
内存分配与回收策略
Java的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。
1. 对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机会发起一次Minor GC。
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多具有朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
1 /** 2 * Created by sakura on 2018/3/5. 3 */ 4 /* 5 VM Args:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 6 */ 7 public class Demo { 8 private static final int _1MB=1024*1024; 9 public static void main(String[] args) { 10 byte[] allocation1,allocation2,allocation3,allocation4; 11 allocation1=new byte[2*_1MB]; 12 allocation2=new byte[2*_1MB]; 13 allocation3=new byte[2*_1MB]; 14 allocation4=new byte[4*_1MB];//出现一次Minor GC 15 } 16 } 17 /* 18 运行时通过-Xms20M -Xmx20M -Xmn10M这3个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配会新生代,剩下的10MB分配给老年代。 19 -XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1,从输出结果也可以清晰地看到"eden space 8192K、from space 1024K、 20 to space 1024K"的信息,新生代可用空间为9216KB(Eden区+1个Survivor区的总容量)。执行allocation4=new byte[4*_1MB];时会发生一次Minor GC, 21 这次GC的结果是新生代6298KB变为904KB,而总内存占用量则几乎没有减少(因为allocation1、allocation2、allocation3三个对象都是存活的,虚拟机几乎 22 没有找到可回收的对象)。这次GC发生的原因是给allocation4分配内存时,发现Eden已经被占用了6MB,剩余空间已不足以分配allocation4所需的4MB内存,因此 23 发生Minor GC。GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制提前转移到老 24 年代去。这次GC结束后,4MB的allocation4对象顺利分配在Eden中,因此程序执行完的结果是Eden空间占用4MB(被allocation4占用),Survivor空闲,老年代被 25 占用6MB(被allocation1、allocation2、allocation3占用)。 26 */ 27 /* 28 [GC (Allocation Failure) [PSYoungGen: 6298K->904K(9216K)] 6298K->5008K(19456K), 0.0334379 secs] [Times: user=0.00 sys=0.00, real=0.04 secs] 29 Heap 30 PSYoungGen total 9216K, used 7369K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) 31 eden space 8192K, 78% used [0x00000000ff600000,0x00000000ffc50640,0x00000000ffe00000) 32 from space 1024K, 88% used [0x00000000ffe00000,0x00000000ffee2020,0x00000000fff00000) 33 to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) 34 ParOldGen total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) 35 object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000) 36 Metaspace used 3485K, capacity 4502K, committed 4864K, reserved 1056768K 37 class space used 387K, capacity 390K, committed 512K, reserved 1048576K 38 */
2. 大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。一群朝生夕灭的短命大对象对Java虚拟机的内存分配来说是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来安置它们。虚拟机提供了一个-XX:PretenureSizeThreshold
参数,令大于这个设置值的对象直接在老年代分配,这样可以避免在Eden区及两个Survivor区之间发生大量的内存复制。
1 /** 2 * Created by sakura on 2018/3/5. 3 */ 4 /* 5 VM Args:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails 6 -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728 7 */ 8 public class Demo2 { 9 private static final int _1MB=1024*1024; 10 public static void main(String[] args) { 11 byte[] allocation; 12 allocation=new byte[4*_1MB];//直接分配在老年代 13 } 14 } 15 /* 16 我们看到Eden空间几乎没有被使用,而老年代的10MB空间被使用了40%,也就是4MB的allocation对象就直接分配在老年代中,这是因为 17 PretenureSizeThreshold被设置为3145728,因此超过3MB的对象都会直接在老年代进行分配。 18 */ 19 /* 20 Heap 21 PSYoungGen total 9216K, used 6462K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) 22 eden space 8192K, 78% used [0x00000000ff600000,0x00000000ffc4fa70,0x00000000ffe00000) 23 from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) 24 to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000) 25 ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) 26 object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000) 27 Metaspace used 3412K, capacity 4500K, committed 4864K, reserved 1056768K 28 class space used 379K, capacity 388K, committed 512K, reserved 1048576K 29 */
3. 长期存活的对象会进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,会被移动到Survivor空间中,并且对象的年龄设为1。对象在Survivor区中每熬过一次Minor GC年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold
设置。
1 /** 2 * Created by sakura on 2018/3/5. 3 */ 4 /* 5 VM Args:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails 6 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 7 */ 8 public class Demo3 { 9 private static final int _1MB=1024*1024; 10 public static void main(String[] args) { 11 byte[] allocation1,allocation2,allocation3; 12 allocation1=new byte[_1MB/4]; 13 allocation2=new byte[4*_1MB]; 14 allocation3=new byte[4*_1MB]; 15 allocation3=null; 16 allocation3=new byte[4*_1MB]; 17 } 18 } 19 /* 20 -XX:MaxTenuringThreshold=1 21 Heap 22 PSYoungGen total 9216K, used 6718K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) 23 eden space 8192K, 82% used [0x00000000ff600000,0x00000000ffc8fa80,0x00000000ffe00000) 24 from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) 25 to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000) 26 ParOldGen total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) 27 object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400020,0x00000000ff600000) 28 Metaspace used 3475K, capacity 4500K, committed 4864K, reserved 1056768K 29 class space used 385K, capacity 388K, committed 512K, reserved 1048576K 30 -XX:MaxTenuringThreshold=15 31 Heap 32 PSYoungGen total 9216K, used 6718K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) 33 eden space 8192K, 82% used [0x00000000ff600000,0x00000000ffc8fa80,0x00000000ffe00000) 34 from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) 35 to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000) 36 ParOldGen total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) 37 object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400020,0x00000000ff600000) 38 Metaspace used 3476K, capacity 4500K, committed 4864K, reserved 1056768K 39 class space used 385K, capacity 388K, committed 512K, reserved 1048576K 40 */
4. 动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于此年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
1 /** 2 * Created by sakura on 2018/3/6. 3 */ 4 /* 5 VM Args:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails 6 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 7 */ 8 public class Demo4 { 9 private static final int _1MB=1024*1024; 10 public static void main(String[] args) { 11 byte[] allocation1,allocation2,allocation3,allocation4; 12 allocation1=new byte[_1MB/4]; 13 allocation2=new byte[_1MB/4]; 14 allocation3=new byte[4*_1MB]; 15 allocation4=new byte[4*_1MB]; 16 allocation4=null; 17 allocation4=new byte[4*_1MB]; 18 } 19 } 20 /* 21 allocation1、allocation2对象都直接进入了老年代,而没有等到15岁的临界年龄 22 */ 23 /* 24 Heap 25 PSYoungGen total 9216K, used 6974K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) 26 eden space 8192K, 85% used [0x00000000ff600000,0x00000000ffccfa90,0x00000000ffe00000) 27 from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) 28 to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000) 29 ParOldGen total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) 30 object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400020,0x00000000ff600000) 31 Metaspace used 3475K, capacity 4500K, committed 4864K, reserved 1056768K 32 class space used 385K, capacity 388K, committed 512K, reserved 1048576K 33 */
5. 空间分配担保
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那么Minor GC可以确保是安全的,如果不成立,则虚拟机会查看HandlePromotionFailure
设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,会尝试着进行一次Minor GC,尽管这一次Minor GC是有风险的;如果小于,或者HandlePromotionFailure
设置不允许冒险,那这时也要改为进行一次Full GC(JDK6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则会进行Full GC)。