目录
一、对象存活判断
1、 概述
在Java运行时内存区域划分中线程私有部分,虚拟机栈,本地方法栈,程序计数器3个区域随线程而生随线程而灭;其中虚拟机栈中的栈帧随方法的执行和结束进行着入栈和出栈操作,其中栈帧的内存是在类结构确定时已知的。因为方法结束或线程结束时,内存就回收了。所以这些区域不需要过多考虑回收问题。
而对于Java堆和方法区来说,类或方法的分支需要的内存可能不一样,只有在程序运行时才知道会创建哪些对象,这部分内存的分配和回收是动态的,所以垃圾回收主要针对这块区域来说的。
2、 引用介绍
在判断对象是否存活时都与引用有关。在JDK1.2之后有以下四种引用。
- 强引用(Strong Reference):强引用指的是在程序代码中普遍存在的,类似“Object obj = new Object()”这类引用,只要强引用还在,垃圾回收器永远不会回收掉任何被引用的对象。
- 软引用(Soft Reference):用来描述一些还有用但非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。JDK 1.2之后,提供了SoftReference类来实现软引用。
- 弱引用(Weak Reference):弱引用也是用来描述非必需对象的,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。JDK 1.2之后,提供了WeakReference类来实现弱引用。
- 虚引用(Phantom reference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
3、 对象是否存活
堆里面几乎存放着Java中的所有对象,所以垃圾回收前对堆进行内存回收时,首先要判断对象是否”存活“。
3.1、 引用计数算法
引用计数是一种简单判定效率较高的垃圾回收技术。每个对象都含有一个引用计数器,当有引用连接至对象时,引用计数加1.当引用离开作用域或引用被置为null时,引用计数减1.垃圾回收器将在含有全部对象的列表上遍历,当发现某个对象的引用计数为0时就立即释放该对象占用的空间(但是引用计数经常在引用计数为0时就立即释放对象)。
缺陷:如果对象之间存在循环引用,可能出现对象应该被回收但引用计数却不为0的情况;对垃圾回收器来说,定位这种交互自引用的对象组所需的工作量极大。(并未被应用于任何一种Java虚拟机实现中)
3.2、 可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象没有任何路径可以到达“GC Roots”时(用图论描述就是GC Roots到这个对象不可达),则证明此对象是不可用的。
可作为“GC Roots”的对象有:
1. 虚拟机栈中(栈帧中的本地变量表)引用的对象
2. 本地方法栈JNI(即一般说的Native方法)中引用的对象
3. 方法区中常量引用的对象
4. 方法区中类静态属性引用的对象
3.3、 finalize()方法最终判定对象是否存活
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
1. 第一次标记并进行一次筛选
筛选的条件是此对象是否有必要执行finalize()方法。当前对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象会被回收,不进行下一次标记。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行,进入第二次标记。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
2.第二次标记
Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
二、垃圾收集算法
简单了解算法的思想
1、 标记—清除算法
标记—清除算法分为标记和清除二个阶段:首先标记出需要回收的对象(详见上一节的可达性分析找出存活对象),在标记完成后统一回收所有被标记的对象。
缺点:
- 标记和清除二个过程的效率都不高
- 空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2、 复制算法
复制算法:将可用内存按容量划分为大小相等的二块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用的内存空间一次性清理掉。
优点:每次都是整个半区进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只需移动堆顶指针,按顺序分配内存即可。实现简单,运行高效。
缺点:将内存缩小为原来的一半,代价有点高。
应用:
在新生代中98%的对象都是“朝生夕死”,可以不按1:1来分配内存空间。将内存分为一块较大的Eden空间和二块较小的Survivor空间,每次使用Eden和其中一块Survivor空间。当回收时,将Eden和Survivor空间中还存活的对象一次性复制到另外一块Survivor空间,最后清理掉刚才使用的Eden和Survivor空间。
在Hotspot虚拟机Eden空间和Survivor空间默认本比例为8:1,也就是每次新生代可用空间为90%。当要执行垃圾清理将对象复制到另一块未使用的Survivor空间但Survivor空间不够的时候,需要其它内存(这里指老年代)进行分配担保。
3、 标记—整理算法
标记—整理算法:标记过程与标记—清除过程一样(详见上一节的可达性分析找出存活对象),只是在整理阶段是让所有存活对象都向一段移动,然后直接清理掉端边界以外的内存。
应用:适用于老年代(因为老年代对象存活率很高,这样不会”浪费“空间。)
4、 分代收集算法
分代收集算法:对复制算法及标记—整理算法的结合。当前商业虚拟机都采用“分代收集”算法,根据对象存活周期的不同将内存划分为几块。一般是把Java堆划分为新生代和老年代。
- 在新生代中,每次垃圾收集都会由大量对象死去,只有少量存活,所以采用“复制”算法。只需付出少量存活对象的复制成本就可以完成收集。
- 在老年代中,因为对象存活率较高,没有额外的空间对它进行分配担保,就必须使用“标记— 清除”或“标记—整理”算法来进行回收。
三、HotSpot的算法实现
在HotSpot虚拟机上实现这些算法时,必须对算法的执行效率有着严格的考量,才能保证虚拟机高效地运行。
1、 枚举根节点
采用可达性分析从GC Roots节点中找引用链为例
存在的缺点:
- 在前面找出还存活对象时,采用可达性分析从GC Roots节点中找引用链时,可作为GC Roots的节点主要在全局性的引用(方法区的常量或类静态属性引用)与执行上下文(虚拟机栈栈帧中的本地变量表或本地方法栈中的Native方法的引用)中,很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,必然会消耗很多时间。
- 可达性分析对时间的敏感还体现在GC停顿上,因为这项工作必须在一个能确保一致性的快照中进行。“一致性值的是”GC进行时必须停顿所有Java执行线程(Stop The World)。
HotSpot的解决方式:
- 当执行系统停下来时,不需要检查完所有的全局和执行上下文的引用位置,HotSpot采用一组称为OopMap的数据结构来记录那些地方存放着对象的引用。
- JIT(即时编译)编译过程中也会在特定位置记录下栈和寄存器中那些位置是引用。
2、 安全点
如果为每一条指令都生成对应的Oopmap,会需要大量的额外空间,GC成本增高。其实HotSpot虚拟机并不是在为每条指令都生成了Oopmap,程序执行时也并非在任何地方都能停下来开始GC,只能到达特定位置才能开始记录,这些特定位置称为安全点(Safepoint)。
安全点的选择:是否具有让程序长时间执行的特征(比如:方法调用,循环跳转,异常跳转等)。
在GC发生时如何让所有线程跑到最近的安全点再停止有二种方案:
- 抢先式中断:不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。 现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
- 主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
3、 安全区域
当程序不执行的时候即没有分配CPU时间,比如:线程处于Sleep状态或Blocked状态,对于这种情况就需要安全区域(Safe Region)来解决。
安全区域指在一段代码片段中,引用关系不会发生变化。在这个区域的任意地方开始GC都是安全的,或则可以将安全区域看做是扩展过的安全点。
安全区域工作原理:在线程中执行到安全区域的代码时,首先标识自己已经进入了安全区域,若在这段时间JVM要发起GC时,就不用管标识自己为安全区域状态的线程了。在线程执行完安全区域的代码要离开安全区域时,当前线程要检查当前系统是否已经完成了根节点枚举(或是整个GC过程),若系统已完成则可以离开安全区域;若系统未完成,则它就必须等待直到可以离开安全区域为止。
四、垃圾收集器
收集算法是内存回收的方法论,垃圾收集器则是内存回收的具体实现。
1、垃圾收集器介绍
在垃圾收集器的层面上对并行与并发的解释:
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户现场仍处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但并不一定是并行的,可能会交替执行),用户程序仍在继续执行,而垃圾收集程序运行于另一个CPU上。
对于不同的厂商,不同的版本的虚拟机都可能有很大的差别。此处讨论的是jdk1.7之后的HotSpot虚拟机,如下图所示:
2、Serial收集器
Serial/Serial Old收集器的运行过程如下图所示:
Serial收集器的特点:
- 最基本,发展历史最悠久的收集器(jdk1.3.1之前是虚拟机新生代唯一的选择)
- 是一个单线程的收集器,它的“单线程”不仅仅指它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它进行垃圾收集时必须暂停其它所有的工作线程,直到它收集结束。
- 在用户并不可见的情况下把用户正常工作的线程全部停掉,由虚拟机在后台自动发起和自动完成。(当然随着其它收集器的出现,用户线程的停顿时间在不断缩短,但仍然没办法完全消除)
Serial收集器的优点:
简单而高效(与其他收集器的单线程比),对于限定单个CPU环境来说,Serial没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
Serial收集器的特应用:
Serial是虚拟机运行在Client模式下的默认新生代收集器。
3、ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器完全一样。
ParNew收集器的运行过程如下图所示:
ParNew收集器的特点:
- ParNew收集器除了多线程收集之外,其它与Serial收集器并没有太多创新之处。
- ParNew收集器在单CPU的环境下绝对不会有比Serial收集器更好的效果。(甚至在二个CPU的环境中由于线程交互的开销,都不能100%保证超过Serial收集器)
ParNew收集器的应用:
运行在server模式下的虚拟机首选的新生代收集器。(一个与性能无关的重要原因是除了Seria就只有ParNewl能与CMS收集器配合工作)
4、Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代、使用复制算法、并行的多线程收集器。
Parallel Scavenge收集器的特点:
- Parallel Scavenge收集器的目的是达到一个可控制的吞吐量(Throughput)而其他收集器目的是尽可能缩短垃圾收集时用户线程的停顿时间(如CMS等收集器)。
- “吞吐量优先”的收集器,吞吐量即运行用户代码的时间 / (垃圾收集时间+运行用户代码时间)。
- Parallel Scavenge收集器提供了二个参数用于精确控制吞吐量。
控制最大垃圾收集时间:-XX:MaxGCPauseMillis(大于0的毫秒数)。通过调小新生代空间大小,提高垃圾收集频率来使垃圾收集停顿时间下降,但同时也降低了吞吐量。
直接设置吞吐量大小:-XX:GCTimeRatio(0-100的整数)。垃圾收集时间占总时间的比率,相当于吞吐量的倒数,参数默认值为99。如将参数设置为19,则允许最大的垃圾收集时间占总时间的1 / (1+ 19)即5%。 - GC自适应调节策略:Parallel Scavenge收集器通过设置-XX:+UseAdaptiveSizePolicy这个开关参数,虚拟机通过当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量。
设置步骤为:先设置基本内存数据(如:-Xmx设置最大堆),然后给虚拟机一个优化目标MaxGCPauseMillis参数(关注最大垃圾收集停顿时间)或GCTimeRatio参数(关注吞吐量)即可,最后具体细节参数由虚拟机自动完成。
Parallel Scavenge收集器的适用场景:
高吞吐量则可以高效的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
5、Serial Old收集器
Serial Old是一个单线程、使用”标记-整理“算法、Serial的老年代版本的收集器。Serial/Serial Old收集器的运行过程如下图所示:
Serial Old收集器的使用场景:
- Serial Old可以在Client模式下的虚拟机使用
- Serial Old可以与Parallel Scavenge搭配使用
- Serial Old可以作为CMS收集器的后备方案在并发收集时发生Concurrent Mode Failure时使用
6、Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。Parallel Old收集器的工作过程如下图:
Parallel Old收集器适用场景:注重吞吐量及CPU资源敏感的场合。
7、CMS收集器
在jdk1.5时期,HotSpot推出了CMS收集器,这是HotSpot虚拟机中第一款真正意义上的并发收集器,他第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
CMS(Concurrent Mark Sweep):并发标志清除收集器是一种以获取最短回收停顿时间为目标的收集器。工作过程如下图:
CMS收集器使用的是“标记—清除算法”,整个过程分为4步:
- 初始标记(CMS initial mark):只标记GC roots能直接关联到的对象,速度很快。
- 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程。
- 重新标记(CMS remark):修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。重新标记的标记时间比初始标记时间稍长,但远比并发标记时间短。
- 并发清除(CMS concurrent sweep)
其中初始标记及重新标记仍然需要“stop the world”,但总体时间较短。而耗时较长的并发标记及并发清除可以与用户线程一起工作。所以总体上来说CMS收集器的内存回收是与用户线程一起执行的。
CMS收集器的优点:并发收集、低停顿。
CMS收集器的缺点:
- CMS收集器对CPU资源敏感,在并发阶段虽然不会导致用户线程停顿,但是因为占用了一部分CPU资源会导致应用程序变慢,总吞吐量降低。
- CMS收集器无法处理浮动垃圾(并发运行时,用户程序运行时产生的垃圾),可能导致“Concurrent Model Failure”失败而导致另一次Full GC的产生。同时由于内存回收线程与用户线程同时运行,在垃圾回收时不能等老年代空间快满了时进行回收,必须为用户线程的运行预留一部分空间。若预留的内存无法满足用户程序的需要就会出现“Concurrent Model Failure”失败,这是虚拟机将临时采用Serial Old收集器进行收集,导致停顿时间变很长。
- 空间碎片太多,给大对象分配空间时找不到足够大的连续空间来分配当前对象,而提前触发一次Full GC。为了解决这个问题,提供了二个参数
-XX:UseCMSCompactAtFullCollection开关参数(默认开启),用于在顶不住要进行Full GC时进行内存碎片整理(不能并发,停顿时间变长);
-XX:CMSFullGCsforeCompaction用于设置执行多少次不压缩的Full GC后带来一次压缩的(默认为0,表示每次进入Full GC都进行碎片整理)。
CMS的适用场景:
集中在互联网站或者B/S系统的服务端上,重视服务的响应速度,以带给用户较好的体验。
8、G1收集器
G1(Garbage-First)收集器是一款面向服务端应用的垃圾收集器。使用G1收集器时,它将Java堆分成多个大小相等的独立区域(Region),新生代与老年代不在物理相隔了,它们都是一部分Region(不需要连续)的集合。工作过程如下图:
G1收集器的特点:
- 并行与并发:充分利用多CPU、多核环境下的硬件优势来缩短“stop-the-world”的时间。
- 分代收集:不需要其他收集器的配合,就能独立管理整个GC堆,但也能采用不同的方式对不同阶段的对象(新创建的、存活了一段时间的,经过多次GC的旧对象)进行收集。
- 空间整合:整体基于“标记-整理”算法;局部(二个Region)之间采用“复制”算法。都不会产生内存空间碎片。
- 可预测的停顿:能让使用者指定在一个长度为M毫秒的时间片段内,垃圾收集消耗的时间在N毫秒内。(之所以能建立可预测的时间停顿模型在于G1避免了在整个Java堆中进行全区域的垃圾回收;G1跟踪每个Region里面的垃圾堆积的价值大小(时间、空间),维护一个优先列表,根据允许的收集时间优先选择回收价值大的Region。)
为了避免因为Region之间存在引用,从而在进行可达性分析判断对象是否存活时进行整个Java堆全堆扫描,G1中每个Region都有一个对应的Remenbered Set来记录区域之间对象的引用信息,它的工作过程如下:
- 虚拟机发现程序在对Reference进行写操作时,产生一个Write Barrier暂时中断写操作
- 检查Referrence引用的对象是否在不同的Region中(在分代收集中即判断是否老年代的对象引用了新生代的对象)
- 如果是在不同的Region之间,便通过CardTable把相关的引用信息记录到被引用对象所属的Region的Remenbered Set中
- 进行内存回收时,在GC Roots枚举范围中中加入Remenbered Set即可保证不对全堆扫描也不会有遗漏
G1收集器的工作步骤(不考虑Remenbered Set的维护操作):
- 初始标记(Initial Marking):标记GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
- 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
- 最终标记(Final Marking):为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
- 筛选回收(LIve Data Counting and Evacuation):最后筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,从Sun透露出来的信息来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。
五、垃圾日志与常见参数
1、理解GC日志
每个收集器的日志格式都可以不一样,但各个每个收集器的日志都维持一定的共性。如下面二段日志:
33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
- 最前面的数字“33.125:”和“100.667:”代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。
- GC和Full GC说明此次垃圾垃圾收集停顿的类型。如果有“Full”说明这次GC是发生了Stop-The-World的。如果是调用System.gc()方法触发的收集,那么这里将显示“[ Full GC(System) ]”
- 接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的。
(1).上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew ]”。
(2).如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。
(3).如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。 - .后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量-> GC后该内存区域已使用容量 (该内存区域总容量)”。
- 而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量 -> GC后Java堆已使用容量 (Java堆总容量)”。
- 再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。
下面这段新生代收集器ParNew的日志也会出现“[Full GC”(这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。
[Full GC 283.736: [ParNew: 261599K->261599K(261952K), 0.0000288 secs]
2、垃圾收集参数总结
-XX:+< option > 启用选项
-XX:-< option > 不启用选项
-XX:< option > = < number >
-XX:< option > = < string >
参数 | 描述 |
---|---|
-XX:+UseSerialGC | Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
-XX:+UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收 |
-XX:+UseConcMarkSweepGC | 使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。 |
-XX:+UseParallelGC | Jvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收 |
-XX:+UseParallelOldGC | 使用Parallel Scavenge + Parallel Old的收集器组合进行回收 |
-XX:SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1 |
-XX:PretenureSizeThreshold | 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
-XX:MaxTenuringThreshold | 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代 |
-XX:UseAdaptiveSizePolicy | 动态调整java堆中各个区域的大小以及进入老年代的年龄 |
-XX:+HandlePromotionFailure | 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留 |
-XX:ParallelGCThreads | 设置并行GC进行内存回收的线程数 |
-XX:GCTimeRatio | GC时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效 |
-XX:MaxGCPauseMillis | 设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效 |
-XX:CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70 |
-XX:+UseCMSCompactAtFullCollection | 由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效 |
-XX:+CMSFullGCBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用 |
-XX:+UseFastAccessorMethods | 原始类型优化 |
-XX:+DisableExplicitGC | 是否关闭手动System.gc |
-XX:+CMSParallelRemarkEnabled | 降低标记停顿 |
-XX:LargePageSizeInBytes | 内存页的大小不可设置过大,会影响Perm的大小,-XX:LargePageSizeInBytes=128m |
Client、Server模式默认GC
新生代GC方式 | 老年代和持久带GC方式 | |
---|---|---|
Client | Serial串行GC | Serial Old串行GC |
Server | Parallel Scavenge 并行回收GC | Parallel Old 并行GC |
Sun/Oracle JDK GC组合方式
六、内存分配与回收策略
对象的内存分配,一般来说就是在堆上的分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象分配的细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数设置。
区分Minor GC与 Full GC:
- 新生代GC(Minor GC):指发生在新生代的的垃圾收集动作,因为Java对象大多具有朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- 老年代GC(Full GC / Major GC):老年代的GC,速度一般比Minor GC慢。
1、对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行内存分配时,虚拟机将发起一次Minor GC(新生代内存回收)。
虚拟机参数设置为-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8。其中-Xms20表示Java堆大小为20M; -Xmx20M与堆大小相等表示堆空间不可扩展; -Xmn10M表示其中10M分配给新生代(可推出老年代还剩下10M);-XX:SurvivorRatio=8表示当前Eden空间与survivor比例为8:1。
其中虚拟机通过参数 -XX:+PrintGCDetails打印垃圾收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志。
2、大对象直接进入老年代
大对象是指需要大量连续内存空间的Java对象,如很长的字符串以及数组。
虚拟机提供参数-XX:PretenureSizeThreshold参数来指定大对象,大于该值的对象都是大对象直接在老年代分配,避免在Eden和二个survivor之间发生大量内存复制。
编程时应尽量避免“朝生夕死”的大对象。
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 Update24之后,HandlePromotionFailure参数将不会影响到虚拟机的空间分配担保策略,观察OpenJDK中的源码可以发现虽然还定义了该参数,但是代码中已不使用它了。JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。