JVM详解——垃圾回收
Posted 耶瞳
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM详解——垃圾回收相关的知识,希望对你有一定的参考价值。
如果有兴趣了解更多相关内容的话,可以看看我的个人网站:耶瞳空间
GC:垃圾收集(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存。不当的回收可能会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。这个部分会在下面可达性分析算法中详细介绍。
程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。另外,程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
一:判断对象可以被回收
垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是存活的,是不可以被回收的;哪些对象已经死亡了,需要被回收。
1.1:引用计数法
引用计数法是历史最悠久的一种算法,最早George E.Collins 在1960的时候首次提出,50年后的今天,该算法依然被很多编程语言使用,比如python。但需要注意的是,Java并不是使用的这个算法,Java使用的是可达性分析算法,避免误解所以先说一下。
引用计数法假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失效时,对象A的引用计数器就-1,如果对象A的计数器的值为0,那说明对象A没有引用了,可以被回收。
优点:
- 实时性较高,无需等到内存不够的时候才开始回收,运行是根据对象的计数器是否为0,就可以直接回收。
- 在垃圾回收过程中,应用无需挂起,如果申请内存时,内存不足,立刻报OutOfMemory错误
- 区域性,更新对象的计数器时,只是影响到该对象,不对扫描全部对象。
缺点:
- 每次对象被引用时,都需要去更新计数器,有一点时间开销。
- 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
- 无法解决循环引用的问题(最大的缺点)。比如说A引用了B,B引用了A,这时候会进入死循环,从而导致A和B无法被回收。
1.2:可达性分析算法
可达性分析算法,也可以称为根搜索算法、追踪性垃圾收集。算法的核心思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象不可用。
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。而Java也正是使用了这种算法。
上图中,Object1~Object4都可以被GC Root访问到,而Object5~Object7都不可以被访问到,这也就是说。也就是说,Object5、6、7这三个对象就是不可达的,下次垃圾回收的时候,可能就会被回收掉,注意是可能,被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,才会成为可回收对象。
两次标记过程
- 第一次标记:如果对象在进行可达性分析后发现没有 GCRoots 相连接的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果对象被判定为有必要执行,则会被放到一个F-Queue队列。
- 第二次标记:finalize()方法是对象跳脱死亡命运的最后一次机会,稍后GC将对F-Queue中对象进行第二次小规模标记,如果对象要在finalize()中重新拯救自己:只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时她将被移出即将回收的集合。
需要注意的是,并不是所有的对象都可以作为GC Roots的对象,只有下列的对象可以作为GC Roots的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象:线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中。只要方法还在运行,还没出栈,就意味这本地变量表的对象还会被访问,GC就不应该回收,所以这一类对象也可作为GC Roots。
- 方法区中类静态属性引用的对象:全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。
- 方法区中常量引用的对象:属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象:与第一条相同,无非是一个是Java方法栈中的变量引用,一个是native方法(C、C++)方法栈中的变量引用。
- 被同步锁持有的对象:假设当前有线程持有对象锁,GC如果回收了对象,锁就会失效
注意,如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。这点也是导致 GC进行时必须“stop The World”的一个重要原因。即使是号称(几乎)不会发生停顿的 CMS 收集器中,枚举根节点时也是必须要停顿的。
1.3:五种引用
在JDK1.2以前,Java中引用的定义很传统:如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。这种定义有些狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态。
我们希望能描述这一类对象: 当内存空间还足够时,则能保存在内存中;如果内存空间在进行垃圾回收后还是非常紧张,则可以抛弃这些对象。很多系统中的缓存对象都符合这样的场景。
在JDK1.2之后,Java对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。
- 强引用(StrongReference):强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 ps:强引用其实也就是我们平时A a = new A()这个意思。
- 软引用(SoftReference):如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
- 弱引用(WeakReference):弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
- 虚引用(PhantomReference):“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
二:垃圾回收算法
当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
目前在JVM中比较常见的三种垃圾收集算法:
- 标记-清除算法(Mark-Sweep)
- 复制算法(Copying)
- 标记-整理算法(Mark-Compact)
2.1:标记清除算法
算法思路:执行分为两个阶段,标记和清除。标记阶段标记所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
缺点:
- 效率不高
- 内存碎片严重化,后续可能发生对象不能找到利用空间的问题。
2.2:复制算法
算法思路:按照容量划分两个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。
优点:算法执行效率高,适用于存活对象占少数的情况。
缺点:内存使用率不高,只有原来的一半。
2.3:标记整理算法
算法思路:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
优点:有效地避免了内存碎片的产生
三:分代垃圾回收
当前大多数垃圾收集都采用的分代收集算法,这种算法会根据对象存活周期的不同将内存划分为几块,每一块使用不同的上述算法去收集。在jdk8以前分为三代:年轻代、老年代、永久代。在jdk8以后取消了永久代的说法,而是元空间取而代之。一般年轻代使用复制算法(对象存活率低),老年代使用标记整理算法(对象存活率高)。
- 年轻代(复制算法为主):尽可能快的收集掉声明周期短的对象。整个年轻代占1/3的堆空间,年轻代分为三个区,Eden、Survivor-from、Survivor-to,其内存大小默认比例为8:1:1(可调整),大部分新创建的对象都是在Eden区创建。当回收时,先将Eden区存活对象复制到一个Survivor-from区,然后清空Eden区,存活的对象年龄+1;当这个Survivor-from区也存放满了时,则将Eden区和Survivor-from区存活对象复制到另一个Survivor-to区,然后清空Eden和这个Survivor-from区,存活的对象年龄+1;此时Survivor-from区是空的,然后将Survivor-from区和Survivor-to区交换,即保持Survivor-from区为空(此时的Survivor-from是原来的Survivor-to区), 如此往复。年轻代执行的GC是Minor GC。年轻代的迭代更新很快,大多数对象的存活时间都比较短,所以对GC的效率和性能要求较高,因此使用复制算法,同时这样划分为三个区域,保证了每次GC仅浪费10%的内存,内存利用率也有所提高。
- 老年代(标记-整理算法为主):在年轻代经过很多次垃圾回收之后仍然存活的对象(默认15岁),就会被放入老年代中,因为老年代中的对象大多数是存活的,所以使用算法是标记-整理算法。老年代执行的GC是Full GC。
上面多次提到Minor GC和Full GC,那么它们有什么区别呢?
- Minor GC即新生代GC:发生在新生代的垃圾收集动作,因为Java有朝生夕灭的特性,所以Minor GC相对频繁,一般回收速度也比较快。
- Major GC / Full GC:发生在老年代,经常会伴随至少一次Minor GC。Major GC的速度一般会比Minor GC慢。
- Minor GC发生条件:当新对象生成,并且在Eden申请空间失败时
- Full GC发生条件:
- 老年代空间不足
- 永久代空间不足(jdk8以前)
- System.gc()被显示调用
- Minor GC晋升到老年代的平均大小大于老年代的剩余空间
- 使用RMI来进行RPC或管理的JDK应用,每小时执行1次Full GC
四:垃圾回收器
垃圾回收器的主要作用是用来回收内存中已被判定无用的垃圾对象。但是垃圾回收器在扫描过程中,寻找并标记的其实是还在存活的对象。当查找完全部存活对象后将未标记的对象进行统一的回收。
jvm的垃圾回收器大体上的分类主要包括四种:串行、并行、并发(CMS)和G1。
- 串行垃圾回收器(Serial):它为单线程环境设计并且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境。
- 并行垃圾回收器(Parallel):多个垃圾回收线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等弱交互场景。
- 并发垃圾回收器(CMS):用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程。互联网公司多用它,适用于对响应时间有要求的场景。
- G1垃圾回收器:G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。
目前主流的垃圾回收器有如下8个,其中有连线的代表是可以相互配合使用的。G1和ZGC在中间代表新生代和老年代混合回收
每一款不同的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。
4.1:Serial
Serial(串行收集器):只会使用一个CPU或者一条GC线程进行垃圾回收,并且在垃圾回收过程中暂停其他工作线程。
特点:
- Client模式下新生代默认使用的垃圾收集器
- 采用复制算法
- 串行回收,进行垃圾收集时,必须暂停所有工作线程,直到完成。即会"Stop The World"
应用场景:适合单个CPU的环境来说,因为Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率。
对应JVM参数:-XX:+UseSerialGC
4.2:ParNew
ParNew就是Serial的多线程版本,ParNew由多条GC线程并行的进行垃圾清理工作,清理过程中需要停掉所有的业务线程,但由于是多线程运作,其效率要高于serial。它是很多java虚拟机运行在Server模式下新生代的默认垃圾收集器。
特点:
- 除了多线程外,其余的行为、特点和Serial收集器基本一样
- 采用复制算法
- 除Serial外,目前只有它能与CMS收集器配合工作
- 可控JVM参数非常多,适用于Server模式下,即减少系统停顿,提高系统响应速度
参数设置:
-XX:+UseParNewGC
:使用ParNew收集器-XX:ParallelGCThreads
:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同
4.3:Parallel Scavenge
Parallel Scavenge是并行多线程回收器,常用于新生代,追求CPU吞吐量的优化,能在较短的时间内完成指定的任务,因此适合不需要太多交互的后台运算。正因为其与吞吐量关系密切,也称为吞吐量收集器
吞吐量是指用户线程运行时间占CPU总时间的比例,其计算公式为:
吞吐量=运行用户代码时间/(运行用户代码时间+GC的时间)
吞吐量越高表示GC时间占比越低,用户体验越好
特点:
- 采用复制算法
- JDK1.8的默认垃圾收集器
- 与ParNew收集器相似,采用复制算法,多线程收集
- 主要目标是达到一个可控制的吞吐量
应用场景:适用于应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序
参数设置:
-XX:MaxGCPauseMillis
控制最大垃圾收集停顿时间,大于0的毫秒数。如果设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降,因为可能导致垃圾收集发生得更频繁-XX:GCTimeRatio
设置垃圾收集时间占总时间的比率,0<n<100的整数,GCTimeRatio相当于设置吞吐量大小-XX:+UseAdptiveSizePolicy
动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量
降低停顿时间的两种方式:
- 在多CPU环境中使用多条GC线程,从而垃圾回收的时间减小,从而使用户线程STW的时间减小。
- 实现GC线程与用户线程并发运行,其所谓的并发指的其实是用户线程与GC线程交替运行,从而达到每次的停顿时间减小,用户的停顿感降低,单线程之间的不断切换也意味着需要额外的开销,从而垃圾回收和用户线程的总时间将会延长。
4.4:Serial Old
Serial Old是Serial的老年代版本,他们都是单线程收集器,也就是垃圾收集时只启动一条GC线程,因此都适合客户端的应用,他们之间的主要区别其实就是Serial old常被用于老年代。
特点:
- 使用标记-整理算法
- 是Client模式下默认的老年代垃圾收集器
在Server模式下,主要有两个用途:
- 在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用
- 作为老年代中使用CMS收集器的后备垃圾收集方案
4.5:Parallel Old
Parallel Old是Parallel Scavenge的老年代版木,使用多线程的标记-整理算法。
- 在jdk1.6之前,新生代使用Parallel Scavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。
- Parallel Old在jdk1.6之后才开始提供,正是为了在年老代同样提供吞吐量优先的垃圾收集器。如果系统对吞吐量要求比较高,jdk1.8后可以优先考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。
4.6:CMS
CMS作用于老年代,是一种以获取最短停顿时间为目标的收集器。给予标记-清除算法实现。整个过程分为四步:
- 初始标记:停止一切用户线程,因使用一条初始标记线程对所有与GC Roots关联的对象进行标记
- 并发标记:使用多条并发标记线程并行执行,并与用户线程并发执行。此过程进行可达性分析,标记出所有废弃的对象,速度很慢
- 重新标记:使用多条线程并行执行,将刚才并发过程中新出现的废弃对象标出来
- 并发清除:使用一条并发清除线程,和业务线程并发执行,清除无用对象,这个过程非常耗时
CMS的特点
- 吞吐量低:由于CMS在垃圾收集过程使用用户线程和GC线程并发执行,从而线程之间切换会有额外的开销,因此CPU吞吐量就不如在业务线程全部通知的情况下高
- 无法处理浮动垃圾:由于垃圾清理过程中,可能会产生浮动垃圾,当浮动垃圾过多时,可能会导致频繁的GC
优点:
- 并发收集低停顿
缺点:
- 浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然会有新垃圾产生,这部分垃圾出现在标记过程之后,所以CMS无法在当次收集中处理掉他们,只好留待下一次GC清理掉,这一部分垃圾称为浮动垃圾。在jdk1.5默认设置下,CMS收集器当老年代使用了68%的空间就会被激活,可以通过
-XX:CMSInitialOccupancyFraction
的值来提高触发百分比,在jdk1.6中CMS启动阈值提升到了92%,要是CMS运行期间预留的内存无法满足程序的需要,就会出现”Concurrent Mode Failure“,然后降级临时启用Serial Old收集器进行老年代的垃圾收集,这样停顿时间就很长了。所以-XX:CMSInitialOccupancyFraction
设置太高容易导致大量”Concurrent Mode Failure“。 - 有空间碎片:CMS是一款基于“标记-清除”算法实现的,所以会产生空间碎片。为了解决这个问题,CMS提供了
-XX:UseCMSCompactAtFullCollection
JVM参数用于开启内存碎片的合并整理,由于内存整理是无法并行的,所以停顿时间会变长。还有-XX:CMSFullGCBeforeCompaction
,这个参数用于设置多少次不压缩Full GC后,跟着来一次带压缩的(默认为0)。 - 对CPU资源敏感。在并发标记和并发清除阶段虽然不会停止用户线程,但是会因为占用一部分cpu资源进行垃圾回收导致用户程序变慢。CMS默认启动的回收线程数是
(cpu数量+3)/4
。所以CPU数量少会导致用户程序执行速度降低较多。
应用场景:适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选垃圾回收器。
4.7:G1
G1(Garbage-First)垃圾回收器是在Java7 update 4之后引入的一个新的垃圾回收器,Java 9以后代替CMS成为默认垃圾收集器。G1是一个分代的,增量的,并行与并发的垃圾回收器。它是一款面向服务端的垃圾回收器,主要针对配备多核CPU及大容量内存的机器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低停顿时间,同时兼顾良好的吞吐量。
G1回收器的特点:
- 具有并行性。在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
- 具有并发性。G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
- 整体上使用的是标记-整理算法(region之间是复制算法),因此其回收得到的空间是连续的。这避免了CMS回收器那样因为不连续空间所造成的问题,比如分配大对象时会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显,这种特性非常有利于程序长时间运行。连续空间也意味着G1垃圾回收器可以不必采用空闲链表的内存分配方式,而可以直接采用bump-the-pointer的方式
- 将内存划分一个个固定大小的region,每个region可以是年轻代、老年代的一个。内存的回收是以region作为基本单位的。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
- 具有可预测的停顿时间模型,即软实时(soft real-time)性。所谓的实时垃圾回收,是指在要求的时间内完成垃圾回收。“软实时”则是指,用户可以指定垃圾回收时间的限时,G1会努力在这个时限内完成垃圾回收,但是G1并不担保每次都能在这个时限内完成垃圾回收。通过设定一个合理的目标,可以让达到90%以上垃圾的回收时间都在这个时限内
- 会有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以才起名:Garbage First(垃圾优先) 。
G1的缺点:相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比CMS要高。从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。
G1参数:
-XX:MaxGCPauseMillis
:暂停时间,默认值200ms。这是一个软性目标,G1会尽量达成,如果达不成,会逐渐做自我调整。对于Young GC来说,会逐渐减少Eden区个数,减少Eden空间那么Young GC的处理时间就会相应减少;对于Mixed GC,G1会调整每次Choose Cset的比例,默认最大值是10%,当然每次选择的Cset少了,所要经历的Mixed GC的次数会相应增加。同时减少Eden的总空间时,就会更加频繁的触发Young GC,也就是会加快Mixed GC的执行频率,因为Mixed GC是由Young GC触发的,或者说借机同时执行的。频繁GC会对对应用的吞吐量造成影响,每次Mixed GC回收时间太短,回收的垃圾量太少,可能最后GC的垃圾清理速度赶不上应用产生的速度,那么可能会造成串行的Full GC,这是要极力避免的。所以暂停时间肯定不是设置的越小越好,当然也不能设置的偏大,转而指望G1自己会尽快的处理,这样可能会导致一次全部并发标记后触发的Mixed GC次数变少,但每次的时间变长,STW时间变长,对应用的影响更加明显。-XX:G1HeapRegionSize
:Region大小,若未指定则默认最多生成2048块,每块的大小需要为2的幂次方,如1,2,4,8,16,32,最大值为32M。Region的大小主要是关系到Humongous Object的判定,当一个对象超过Region大小的一半时,则为巨型对象,那么其会至少独占一个Region,如果一个放不下,会占用连续的多个Region。当一个Humongous Region放入了一个巨型对象,可能还有不少剩余空间,但是不能用于存放其他对象,这些空间就浪费了。所以如果应用里有很多大小差不多的巨型对象,可以适当调整Region的大小,尽量让他们以普通对象的形式分配,合理利用Region空间。- 新生代比例:新生代比例有两个数值指定,下限:
-XX:G1NewSizePercent
,默认值5%,上限:-XX:G1MaxNewSizePercent
,默认值60%。G1会根据实际的GC情况(主要是暂停时间)来动态的调整新生代的大小,主要是Eden Region的个数。最好是Eden的空间大一点,毕竟Young GC的频率更大,大的Eden空间能够降低Young GC的发生次数。但是Mixed GC是伴随着Young GC一起的,如果暂停时间短,那么需要更加频繁的Young GC,同时也需要平衡好Mixed GC中新生代和老年代的Region,因为新生代的所有Region都会被回收,如果Eden很大,那么留给老年代回收空间就不多了,最后可能会导致Full GC。 -XX:ConcGCThreads
,并发GC线程数,默认是-XX:ParallelGCThreads/4
,也就是在非STW期间的GC工作线程数,当然其他的线程很多工作在应用上。当并发周期时间过长时,可以尝试调大GC工作线程数,但是这也意味着此期间应用所占的线程数减少,会对吞吐量有一定影响。-XX:ParallelGCThreads
,并行GC线程数,也就是在STW阶段工作的GC线程数,其值遵循以下原则:- 如果用户显示指定了ParallelGCThreads,则使用用户指定的值。
- 否则,需要根据实际的CPU所能够支持的线程数来计算ParallelGCThreads的值,计算方法如下:
- 如果物理CPU所能够支持线程数小于8,则ParallelGCThreads的值为CPU所支持的线程数。这里的阀值为8,是因为JVM中调用nof_parallel_worker_threads接口所传入的switch_pt的值均为8。
- 如果物理CPU所能够支持线程数大于8,则ParallelGCThreads的值为8加上一个调整值,调整值的计算方式为:物理CPU所支持的线程数减去8所得值的5/8或者5/16,JVM会根据实际的情况来选择具体是乘以5/8还是5/16。比如,在64线程的x86 CPU上,如果用户未指定ParallelGCThreads的值,则默认的计算方式为:ParallelGCThreads = 8 + (64 - 8) * (5/8) = 8 + 35 = 43。
-XX:G1MixedGCLiveThresholdPercent
,被纳入Cset的Region的存活空间占比阈值,不同版本默认值不同,有65%和85%。在全局并发标记阶段,如果一个Region的存活对象的空间占比低于此值,则会被纳入Cset。此值直接影响到Mixed GC选择回收的区域,当发现GC时间较长时,可以尝试调低此阈值,尽量优先选择回收垃圾占比高的Region,但此举也可能导致垃圾回收的不够彻底,最终触发Full GC。-XX:InitiatingHeapOccupancyPercent
,触发全局并发标记的老年代使用占比,默认值45%,也就是老年代占堆的比例超过45%。如果Mixed GC周期结束后老年代使用率还是超过45%,那么会再次触发全局并发标记过程,这样就会导致频繁的老年代GC,影响应用吞吐量。同时老年代空间不大,Mixed GC回收的空间肯定是偏少的。可以适当调高IHOP的值,当然如果此值太高,很容易导致年轻代晋升失败而出发Full GC,所以需要多次调整测试。-XX:G1HeapWastePercent
,触发Mixed GC的堆垃圾占比,默认值5%,也就是在全局标记结束后能够统计出所有Cset内可被回收的垃圾占整对的比例值,如果超过5%,那么就会触发之后的多轮Mixed GC,如果不超过,那么会在之后的某次Young GC中重新执行全局并发标记。可以尝试适当的调高此阈值,能够适当的降低Mixed GC的频率。-XX:G1OldCSetRegionThresholdPercent
,每轮Mixed GC回收的Region最大比例,默认10%,也就是每轮Mixed GC附加的Cset的Region不超过全部Region的10%,最多10%,如果暂停时间短,那么可能会少于10%。一般这个值不需要额外调整。-XX:G1MixedGCCountTarget
,一个周期内触发Mixed GC最大次数,默认值8。也就是在一次全局并发标记后,最多接着8次Mixed GC,也就是会把全局并发标记阶段生成的Cset里的Region拆分为最多8部分,然后在每轮Mixed GC里收集一部分。这个值要和上一个参数配合使用,8*10%=80%,应该来说会大于每次标记阶段的Cset集合了。一般此参数也不需额外调整。-XX:G1ReservePercent
,G1为分配担保预留的空间比例,默认10%。也就是老年代会预留10%的空间来给新生代的对象晋升,如果经常发生新生代晋升失败而导致Full GC,那么可以适当调高此阈值。但是调高此值同时也意味着降低了老年代的实际可用空间。-XX:SoftRefLRUPolicyMSPerMB
,每兆堆空闲空间的软引用的存活时间,默认值是1000,也就是1秒。可以调低这个参数来触发更早的回收软引用。如果调高的话会有更多的存活数据,可能在GC后堆占用空间比会增加。 对于软引用,还是建议尽量少用,会增加存活数据量,导致频繁的老年代收集,增加GC的处理时间。-XX:MaxTenuringThreshold
,晋升年龄阈值,默认值15。一般新生对象经过15次Young GC会晋升到老年代,巨型对象会直接分配在老年代,同时在Young GC时,如果相同age的对象占Survivors空间的比例超过-XX:TargetSurvivorRatio
的值(默认50%),则会自动将此次晋升年龄阈值设置为此age的值,所有年龄超过此值的对象都会被晋升到老年代,此举可能会导致老年代需要不少空间应对此种晋升。一般这个值不需要额外调整。
以上是关于JVM详解——垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章