对象已死吗
引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器的值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:实现简单,判定效率很高。
缺点:不能解决对象之间互相循环引用的问题。
可达性分析算法
通过一系列的GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地向量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
再谈引用
在JDK1.2之前,Java中引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但不够细致。在JDK1.2之后,Java对引用的概念进行扩充,引用被分为**强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)**4种,这4种引用强度依次逐渐减弱。
强引用:只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
Object object = new Object();
String str = "hello";
软引用:用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示且只有在内存不足时JVM才会回收被软引用关联的对象。这个特性比较适合实现缓存。
/**
* 设置JVM参数:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
* 年轻代分配10m,剩下的10m分配给老年代
*/
public class SoftRef {
public static void main(String[] args) {
SoftReference<byte[]> byteArray1 = new SoftReference<byte[]>(new byte[1024*1024*8]);
System.out.println("byteArray1 = " + byteArray1.get());
// 这里申请8m内存,导致内存分配失败而进行GC操作,这里的GC操作将会将 byteArray1 回收
byte[] byteArray2 = new byte[1024*1024*8];
System.out.println("byteArray1 = " + byteArray1.get()); // 这里返回null
}
}
弱引用:也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被软引用关联的对象只能生存到下一次垃圾收集发生之前。当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在JDK1.2之后提供了WeakReference类来实现弱引用。
/**
* Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
*/
public class WeakRef {
public static void main(String[] args) {
WeakReference<byte[]> byteArray1 = new WeakReference<byte[]>(new byte[1024*1024*4]);
System.out.println("byteArray1 = " + byteArray1.get());
// 产生GC操作
System.gc();
System.out.println("byteArray1 = " + byteArray1.get()); // byteArray1.get() 返回null
}
}
虚引用:也称幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
对象生存还是死亡
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时它们暂时处于“缓刑”阶段,**要真正宣告一个对象死亡,至少要经理两次标记过程:**如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机把这两种情况都视为“没有必要执行”。如果这个对象被判定为有必要执行finalize()方法,那么这个对象会被放置在一个叫F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束(防止发生死循环)。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC会对F-Queue中的对象进行第二次标记,如果对象在finalize()方法中成功拯救自己即只要重新和引用链上的任何一个对象建立关联即可,那么在第二次标记时它会被移除出“即将回收”的集合,如果对象这个时间点还没有逃脱,那基本上它就真的被回收了。
/*
* 1. 对象可以在被GC时自我拯救
* 2. 这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统调用一次
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive(){
System.out.println("yes, i am still alive :)");
}
@Override
protected void finalize() throws Throwable{
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
// 对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为finalize方法优先级很低,所以暂停0.5秒以等待它
Thread.sleep(500);
if(SAVE_HOOK != null)
SAVE_HOOK.isAlive();
else
System.out.println("no, i am dead :(");
// 下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为finalize方法优先级很低,所以暂停0.5秒以等待它
Thread.sleep(500);
if(SAVE_HOOK != null)
SAVE_HOOK.isAlive();
else
System.out.println("no, i am dead :(");
}
}
运行结果
finalize method executed!
yes, i am still alive :)
no, i am dead :(
回收方法区
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符创“abc”已经进入了常量池中,但是当前系统没有任何一个String对象叫“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”对象,也没有其他地方引用了这个字面量,如果这时发生GC且有必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻很多,对“无用的类”的判定需要同时满足下面3个条件:
此类所有的实例都已经被回收,也就是Java堆中不存在此类的任何实例。
加载此类的ClassLoader已经被回收。
此类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问到此类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。
垃圾收集算法
标记-清除算法(Mark-Sweep)
标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。这是最基础的收集算法,后续的收集算法都是基于这种思路并对其不足进行改进而得到的。它的不足主要有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,导致后续内存分配时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法(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)。
标记-整理算法(Mark-Compact)
复制收集算法在对象存活率较高时就要进行较多的复制操作,性能会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。有人提出另一种“标记-整理”算法,标记过程仍然和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法(Generational Collection)
根据对象存活周期的不同把内存分为几块。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高且没有额外空间对它进行分配担保,就必须使用标记-清理或者标记-整理算法来进行回收。
HotSpot的算法实现
1. 枚举根节点
可达性分析的时间停顿,因为这项分析工作必须在一个能确保一致性的快照中进行,这里一致性的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况。这点是导致GC进行时必须停顿所有Java执行线程(Stop The World)的其中一个重要原因。在执行系统停下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机通过OopMap数据结构直接得知哪些地方存放着对象引用。
2. 安全点
HotSpot没有为每条指令都生成OopMap,只是在特定位置记录了这些信息,这些位置被称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来GC,只有在到达安全点时才能暂停。
3. 安全区域
线程处于Sleep状态或者Blocked状态时,无法响应JVM的中断请求,走到安全的地方去中断挂起,对于这种情况,就需要安全区域(Safe Region)来解决。 安全区域是指在一段代码片段之中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
HotSpot虚拟机的垃圾收集器
Serial收集器
最基本、发展历史最悠久的收集器。是一个单线程的收集器,这里的单线程并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
Serial是虚拟机运行在Client模式下的默认新生代收集器。它的优点是简单而高效(与其他收集器的单线程比)。
ParNew收集器
其实就是Serial收集器的多线程版本。除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。
ParNew是虚拟机运行在Server模式下的首选新生代收集器。一个与性能无关的原因是除了Serial收集器外,目前只有它能与CMS收集器配合工作。CMS收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
注意并行与并发是两个容易混淆的概念:
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
Parallel Scavenge收集器
是一个新生代收集器,也是使用复制算法且并行的多线程收集器。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),所谓吞吐量就是CPU用于运行用户代码的时间和CPU总消耗时间的比值。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Serial Old收集器
是Serial收集器的老年代版本。主要有两大用途:一种用途是在JDK1.5之前与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old收集器
是Parallel Scavenge收集器的老年代版本。使用多线程和标记-整理算法,这个收集器是在JDK1.6中才开始提供的。
CMS收集器
即Concurrent Mark Sweep收集器,是一种以获取最短回收停顿时间为目标的收集器。可以提高服务的响应速度,带给用户较好的体验。CMS是基于标记-清除算法实现的,运行过程分为4个步骤:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍需要Stop The World。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
优点:并发收集、低停顿。
缺点:
a. CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
b. CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,只好留待下一次GC时再清理掉。这一部分垃圾就成为浮动垃圾。也是由于在垃圾收集阶段用户线程还需要运行,那就还需要预留有足够的内存空间给用户线程使用,所以CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集。
c. 标记-清除算法会产生大量的内存碎片,会给接下来的内存分配带来麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
G1收集器
即Garbage-First收集器,是一款面向服务器端的垃圾收集器,具有以下特点:
并行与并发:充分利用多核优势缩短Stop The World 停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
分代收集:采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:与CMS的标记-清理算法不同,G1从整体上看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于复制算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。
可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
在G1之前的其他收集器进行收集的范围是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1收集器之所以建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
G1把内存“化整为零”的思路。把Java堆分为多个Region后,垃圾收集是否就真的能以Region为单位进行了?Region不可能是孤立的。一个对象分配在某个Region中,它并非只能被本Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,岂不是还得扫描整个Java堆才能保证准确性?在以前的分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象时也面临相同的问题,如果回收新生代时也不得不同时扫描老年代的话,那么Minor GC的效率可能下降不少。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。
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停顿时间来制定回收计划(这个阶段也可以与用户线程一起并发执行)。
理解GC日志
[GC (Allocation Failure) [PSYoungGen: 2996K->504K(3584K)] 9140K->7221K(11776K), 0.0021554 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
GC (Allocation Failure) :代表内存分配失败,发生GC操作
2966k:代表YoungGC之前使用了多少内存空间
504K:代表YoungGC之后使用了多少内存空间
3584K:代表年轻代总内存大小
9140K:YoungGC前JVM堆内存大小
7221K:YoungGC后JVM堆内存大小
0.0021554 secs:YoungGC耗时
user=0.00:YoungGC用户耗时
sys=0.00:YoungGC系统耗时
real=0.00 :YoungGC实际耗时
Full 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倍以上。
/**
* -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=10m
* -XX:SurvivorRatio=8 决定了新生代中Eden区与一个Survivor区的空间比例是8:1
* eden = 8m, from = 1m, to = 1m
* 设置-XX:+UseSerialGC 才能使 -XX:PretenureSizeThreshold=10m生效,即当大对象大于等于10m时直接进去老年代
*/
public class Demo {
private static final int _1MB = 1024*1024;
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3;
allocation1 = new byte[2*_1MB];
allocation2 = new byte[2*_1MB];
// 前面已经在eden区分配了4MB,此时已经无法分配6MB内存了
allocation3 = new byte[6*_1MB]; // 出现一次Minor GC
}
}
控制台打印
[GC (Allocation Failure) [DefNew: 6292K->666K(9216K), 0.0046027 secs] 6292K->4762K(19456K), 0.0046624 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 6865K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 75% used [0x00000000fec00000, 0x00000000ff20dbf8, 0x00000000ff400000)
from space 1024K, 65% used [0x00000000ff500000, 0x00000000ff5a6960, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00020, 0x00000000ffa00200, 0x0000000100000000)
Metaspace used 3276K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
这次GC发生的原因是给allocation3分配内存时,发现Eden剩余空间已不足以分配allocation3所需的6MB内存,因此发生Minor GC。GC期间虚拟机又发现已有的2个2MB大小的对象全部无法放入Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制提前转移到老年代去。这次GC结束后,6MB的allocation3对象顺利分配在Eden中,老年代被占用4MB(被allocation1、allocation2占用)。
2. 大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。一群朝生夕灭的短命大对象对Java虚拟机的内存分配来说是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来安置它们。虚拟机提供了一个**-XX:PretenureSizeThreshold**参数,令大于这个设置值的对象直接在老年代分配,这样可以避免在Eden区及两个Survivor区之间发生大量的内存复制。
/**
* -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=2m
* 设置-XX:+UseSerialGC 才能使 -XX:PretenureSizeThreshold=2m生效,即当大对象大于等于2m时直接进去老年代
*/
public class Demo {
private static final int _1MB = 1024*1024;
public static void main(String[] args) {
byte[] allocation1;
allocation1 = new byte[2*_1MB];
}
}
控制台打印
Heap
def new generation total 9216K, used 2360K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 28% used [0x00000000fec00000, 0x00000000fee4e0b8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 2048K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 20% used [0x00000000ff600000, 0x00000000ff800010, 0x00000000ff800200, 0x0000000100000000)
Metaspace used 3275K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
我们看到老年代的2MB空间被使用了20%,也就是2MB的allocation对象就直接分配在老年代中
3. 长期存活的对象会进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,会被移动到Survivor空间中,并且对象的年龄设为1。对象在Survivor区中每熬过一次Minor GC年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数**-XX:MaxTenuringThreshold**设置。
4.动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于此年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
5.空间分配担保
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那么Minor GC可以确保是安全的,如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,会尝试着进行一次Minor GC,尽管这一次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC(JDK6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则会进行Full GC)。
常用参数
参数 | |
---|---|
-Xms | 设置堆初始值,如-Xms10m |
-Xmx | 设置堆最大值,如-Xmx10m |
-Xmn | 设置年轻代大小,如-Xmn10m |
-XX:SurvivorRatio=N | Eden区与Survivor区的大小比值为N:1 |
-XX:NewRatio=N | 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)为1:N |
-XX:PretenureSizeThreshold=N | 大于这个值的参数直接在老年代分配,如 -XX:PretenureSizeThreshold=1m。该参数只对Serial和ParNew两款收集器有效,例如,设置-XX:+UseSerialGC可以查看效果 |
-XX:MaxTenuringThreshold=N | 对象晋升到老年代的年龄阈值,默认为15, |
-XX:+HeapDumpOnOutOfMemoryError | 当JVM发生OOM时,自动生成DUMP文件。 |
-XX:HeapDumpPath=D:\LZC\gc.hprof | 当JVM发生OOM时,设置dump文件文件路径 |
-XX:+PrintGCDetails | 打印GC信息 |
-Xss | 设置每个线程的堆栈大小,如-Xss128k |
代码
/**
* -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails
*
* 常用参数
* -Xms -> 设置堆初始值,如-Xms10m
* -Xmx -> 设置堆最大值,如-Xmx10m
* -Xmn -> 设置年轻代大小,如-Xmn10m
* -XX:SurvivorRatio=N -> Eden区与Survivor区的大小比值为N:1
* -XX:NewRatio=N -> 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)为1:N
*
* -XX:PretenureSizeThreshold=N,大于这个值的参数直接在老年代分配,如 -XX:PretenureSizeThreshold=2m。
* -XX:PretenureSizeThreshold对Serial和ParNew两款收集器有效,例如,设置-XX:+UseSerialGC可以查看效果
*
* -XX:MaxTenuringThreshold -> 对象晋升到老年代的年龄阈值
*
* -XX:+HeapDumpOnOutOfMemoryError -> 表示当JVM发生OOM时,自动生成DUMP文件。
* -XX:+PrintGCDetails -> 打印GC信息
* -Xss: 设置每个线程的堆栈大小,如-Xss128k
**/
public class DemoOOM {
public static void main(String[] args) {
byte[] a = new byte[1024*1024*10];
}
}