JVM系列之五:垃圾回收
0. 垃圾回收机制
1. C/C++需要程序员显式垃圾回收。
显式垃圾回收两大缺点: 1. 内存泄漏:程序忘记及时回收无用内存,从而导致内存泄漏,降低系统性能。 2. 错误回收:程序错误地回收系统核心类库的内存,从而导致系统崩溃。
2. Java由JRE在后台自动进行。
JRE会提供一个后台线程来进行检测和控制,一般都是在CPU空闲或者内存不足时自动进行垃圾回收。 JAVA虚拟机垃圾回收 优点: 1. 大大缩短编程时间,提高编程效率。 2. 保护程序的完整性,是安全性策略的一部分。 缺点: 1. 开销影响程序性能,花费处理器时间。 2. 垃圾回收算法的不完备性,不能保证100%
当编写Java程序时,一个基本的原则是:对于不再需要的对象,不要引用它们。如果保持这些对象的引用,垃圾回收机制暂时不会回收该对象,会导致系统内存越来越少;当系统内存越来越少时,垃圾回收执行的频率就越来越高,从而导致系统的性能下降。
2011年7月发布的Java7提供了G1垃圾回收器来代替原有的CMS并行标志/清除垃圾回收器。
2014年3月发布的Java8删除了HotSpot JVM中的永生代内存PermGen,而是改用使用本地内存来存储类的元数据信息,并称之为Metaspace元空间,这意味着以后不会遇到 java.lang.OutOfMemoryError:PermGen错误。
3. 垃圾回收
垃圾回收算法做两件事: 1. 发现无用的对象。 2. 回收被无用对象占用的内存空间,使得该空间可被程序再次使用。 垃圾回收特定: 1. 垃圾回收只能回收内存资源,对其他物理资源,如数据库连接,磁盘I/O等资源则无能为力; 2. 为了更快地让垃圾回收机制回收那些不再使用的对象,可以将该对象的引用变量设置为NULL; 3. 垃圾回收发生的不可预知性。由于不同JVM采用了不同的垃圾回收机制和不同的垃圾回收算法,因此可能是定时发生的,有可能是CPU空闲的时发生的,也有可能和原始的垃圾回收一样,等到内存消耗出现极限时发生。可以通过调用Runtime对象的gc()或System.gc()等方法来建议系统进行垃圾回收; 4. 垃圾回收的精确性主要包括两个方面:一方面是垃圾回收机制能够精确地标记存活着的对象;二是垃圾回收器能够精确地定位对象之间的引用关系。前者是完全回收所有废弃对象的前提,否则就可能造成内存泄漏;而后者是实现归并和复制等算法的必要条件,通过这种引用关系,可以保证所有对象都能被可靠地回收,所有对象都能重新分配; 5. 现在的JVM有多种不同的垃圾回收实现,每种回收机制因其算法差异可能表现各异,有的垃圾回收开始时就停止应用程序的运行,有的垃圾回收运行时运行应用程序的线程运行,还有的在同一时间允许垃圾回收多线程运行。
1. jdk1.7的堆内存
1. 堆(Java堆) 堆是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,
在JVM启动时创建,该内存区域存放了对象实例(包括基本类型的变量及其值)及数组(所有new的对象)。
但是并不是所有的对象都在堆上,由于栈上分配和标量替换,导致有些对象不在堆上。 其大小通过-Xms(最小值)和-Xmx(最大值)参数设置,
1. -Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,
2. -Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,
3. 默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation 来指定这个比列;
4. 当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation来指定这个比例,
5. 对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。 由于现在收集器都是采用分代收集算法,堆被划分为新生代和老年代。
新生代主要存储新创建的对象和尚未进入老年代的对象。
老年代存储经过多次新生代GC(Minor GC)仍然存活的对象。 堆中没有足够的内存完成实例分配,并且堆也无法扩展时,将会出现OOM异常。(内存泄漏 / 内存溢出)。满足下面两个条件就会抛出OOM。 (1)JVM 98% 的时间都花费在内存回收。 (2)每次回收的内存小于2%。 同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。 1.1 为什么要分代 堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。
给堆内存分代是为了提高对象内存分配和垃圾回收的效率。
试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,
而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率,这简直太可怕了。 有了内存分代,情况就不同了,
1. 新创建的对象会在新生代中分配内存,
2. 经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中,
3. 新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC,
4. 老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,
5. 永久代中回收效果太差,一般不进行垃圾回收
还可以根据不同年代的特点采用合适的垃圾收集算法。
分代收集大大提升了收集效率,这些都是内存分代带来的好处。 1.2 新生代 程序新创建的对象都是从新生代分配内存,
新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,默认比例为8:1:1。
划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。
新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。 1. GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。
2. GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,
3. 而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。
(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)
4. 接着清空Eden区和From Survivor区,
5. 新生代中存活的对象都在To Survivor区。
6. 接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,
总之,不管怎样都会保证To Survivor区在一轮GC后是空的。
7. GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
可通过-Xmn参数来指定新生代的大小,
也可以通过-XX:SurvivorRation来调整Eden Space及Survivor Space的大小。 1.3 老年代 用于存放经过多次新生代GC仍然存活的对象,老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。 主要存储的有:如缓存对象,新建的对象也有可能直接进入老年代,
主要有两种情况: ①大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。 ②大的数组对象,且数组中无引用外部对象。
2. jdk1.8的堆内存
1.4 Java8 内存分代的改进 在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区域了,取而代之是一个叫做 Metaspace(元空间) 的东西。 实际上在JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。
但永久代仍存在于JDK1.7中,并没完全移除,
譬如符号引用(Symbols)转移到了native heap;
字面量(interned strings)转移到了java heap;
类的静态变量(class statics)转移到了java heap。 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小: -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。 -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。 -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。 取消永久代的原因: (1)字符串存在永久代中,容易出现性能问题和内存溢出。 (2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。 (3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
3. GC基本概念
3.1 JVM为什么要进行垃圾回收?
如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。Java 中的垃圾回收一般是在 Java 堆中进行,因为堆中几乎存放了 Java 中所有的对象实例。
3.2 什么是垃圾回收?
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
- GC是垃圾收集的意思(Gabage Collection),Java提供的GC功能可以自动也只能自动地回收堆内存中不再使用的对象,释放资源(目的),Java语言没有提供释放已分配内存的显式操作方法(gc方法只是通知,不是立即执行)。
- 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。
- 垃圾回收是一种动态存储管理技术,它自动地释放不再被程序引用的对象,当一个对象不再被引用的时候,按照特定的垃圾收集算法来实现资源自动回收的功能。
垃圾回收器负责:
分配内存
保证所有正在被引用的对象还存在于内存中
回收执行代码已经不再引用的对象所占的内存
3.3 我们可以主动垃圾回收吗?
GC本身是会周期性的自动运行的,由JVM决定运行的时机,而且现在的版本有多种更智能的模式可以选择,还会根据运行的机器自动去做选择,就算真的有性能上的需求,也应该去对GC的运行机制进行微调,而不是通过使用这个命令来实现性能的优化。
每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。可以通过 getRuntime 方法获取当前运行。java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的简写,两者的行为没有任何不同。唯一的区别就是System.gc()写起来比Runtime.getRuntime().gc()简单点. 其实基本没什么机会用得到这个命令, 因为这个命令只是建议JVM安排GC运行, 还有可能完全被拒绝。
3.4 Stop-The-World(STW)
GC在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿,会带给用户不良的体验;
为什么要Stop-The-World?
可达性分析的时候为了确保快照的一致性,需要对整个系统进行冻结,不可以出现分析过程中对象引用关系还在不断变化的情况,也就是Stop-The-World。
Stop-The-World是导致GC卡顿的重要原因之一。
串行和并行都会导致STW,并发不会导致STW
4. 如何判断对象是否可回收?
4.1 如何判断一个对象已失效?
4.1.1 引用计数算法
概念:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
但是:主流的java虚拟机并没有选用引用计数算法来管理内存,其中最主要的原因是:它很难解决对象之间相互循环引用的问题。
优点:算法的实现简单,判定效率也高,大部分情况下是一个不错的算法。很多地方应用到它
缺点:引用和去引用伴随加法和减法,影响性能
致命的缺陷:对于循环引用的对象无法进行回收
4.1.2 可达性算法 (jvm采用的算法)
概念:这个算法的基本思路就是通过一系列的称谓“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
可达性分析:
4.1.3 什么可以当GC Roots?
在Java语言中,可作为GC Roots的对象包括下面几种:
1.虚拟机栈(栈帧中本地变量表)中引用的对象。
2.本地方法栈中JNI(即一般说的Native方法)引用的对象。
3.方法区中类静态属性引用的对象。
4.方法区中常量引用的对象。
4.2 四种引用类型
在 JDK1.2 之后,Java 对引用的概念进行了扩充,将其分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,引用强度依次减弱。
4.2.1 强引用(Strong Reference)
强引用具备以下三个个特点:
- 强引用可以直接访问目标对象;
- 强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常也不回收强引用所指向的对象;
- 强引用可能导致内存泄露;
如“Object obj = new Object()”,这类引用是 Java 程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
4.2.2 软引用(Soft Reference)
用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。
软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出等问题。
对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等
4.2.3 弱引用(Weak Reference)
用来描述非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发送之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。一旦一个弱引用对象被垃圾回收器回收,便会加入到一个注册引用队列中。在 JDK 1.2 之后,提供了 WeakReference类来实现弱引用。
Tips:软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。
4.2.4 虚引用(Phantom Reference)
它是最弱的一种引用关系。一个持有虚引用的对象,和没有引用几乎是一样的,随时都有可能被垃圾回收器回收。当试图通过虚引用的get()方法取得强引用时,总是会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。在 JDK 1.2 之后,提供了 PhantomReference类来实现虚引用。
4.3 不可达的对象并非“非死不可”
可达性分析算法中不可达的对象,并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
1、对象在进行可达性分析后被发现不可达,它将会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,那么就没必要执行finalize()方法;如果被判定为有必要执行finalize()方法,那么此对象将会放置在一个叫做F-Quenen的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalize线程去触发这个方法。
2、稍后GC将对F-Quenen中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关系即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那么在第二次标记时它将被移出“即将回收”集合;finalize()方法是对象逃脱死亡的最后一次机会,如果对象这时候还没有成功逃脱,那他就会真的被回收了。
如果不使用finalize()方法逃脱的话,二次标记后删除。
4.4 如何判断一个变量是废弃变量?
常量未被引用
4.5 如何判断一个类是无用类?
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
类需要同时满足下面3个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
5. 堆内存常见分配回收策略?
对象分配
对象优先在Eden区分配。当Eden区没有足够空间分配时, VM发起一次Minor GC, 将 Eden区和其中一块Survivor区内尚存活的对象放入另一块Survivor区域。如Minor GC时survivor空间不够,对象提前进入老年代,老年代空间不够时进行Full GC;
大对象直接进入老年代,如字符串,数组等大量连续内存空间的对象,避免在Eden区和Survivor区之间产生大量的内存复制, 此 外大对象容易导致还有不少空闲内存就提前触发GC以获取足够的连续空间.
对象晋级
年龄阈值(长期存活的对象将进入老年代):VM为每个对象定义了一个对象年龄(Age)计数器, 经第一次Minor GC后 仍然存活, 被移动到Survivor空间中, 并将年龄设为1. 以后对象在Survivor区中每熬 过一次Minor GC年龄就+1. 当增加到一定程度(-XX:MaxTenuringThreshold, 默认 15), 将会晋升到老年代.
提前晋升(动态对象年龄判定再分段): 动态年龄判定;如果在Survivor空间中相同年龄所有对象大小的总和大 于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代, 而无 须等到晋升年龄.
为了提升内存分配效率,在年轻代的Eden区HotSpot虚拟机使用了两种技术来加快内存分配 ,分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers)。 1. bump-the-pointer 由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度; 2. TLAB技术 而对于TLAB技术是对于多线程而言的, 它会为每个新创建的线程在新生代的Eden Space上分配一块独立的空间,这块空间称为TLAB(Thread Local Allocation Buffer),
其大小由JVM根据运行情况计算而得 在TLAB上分配内存不需要加锁,一般JVM会优先在TLAB上分配内存,如果对象过大或者TLAB空间已经用完,则仍然在堆上进行分配。 因此,在编写程序时,多个小对象比大的对象分配起来效率更高。
用- XX:TLABWasteTargetPercent来设置其可占用的Eden Space的百分比,默认是1%。
用 -XX:+PrintTLAB来查看TLAB空间的使用情况。
用 -XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。
用 -XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。
6. 虚拟机的GC过程
6.1 为什么要分代回收
在一开始的时候,JVM的GC就是采用标记-清除-压缩方式进行的,这么做并不是很高效,因为当对象分配的越来越多时,对象列表也越来也大,扫描和移动越来越耗时,造成了内存回收越来越慢。然而,经过根据对java应用的分析,发现大部分对象的存活时间都非常短,只有少部分数据存活周期是比较长的,请看下面对java对象内存存活时间的统计:
从图表中可以看出,大部分对象存活时间是非常短的,随着时间的推移,被分配的对象越来越少。
1.1 为什么要分代 1. 给堆内存分代是为了提高对象内存分配和垃圾回收的效率。 2. 还可以根据不同年代的特点采用合适的垃圾收集算法。
堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。 试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集, 而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率,这简直太可怕了。 有了内存分代,情况就不同了, 1. 新创建的对象会在新生代中分配内存, 2. 经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中, 3. 新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC, 4. 老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收, 5. 永久代中回收效果太差,一般不进行垃圾回收 分代收集大大提升了收集效率,这些都是内存分代带来的好处
6.2 什么时候进行垃圾回收
1、当年轻代或者老年代满了,Java虚拟机无法再为新的对象分配内存空间了,那么Java虚拟机就会触发一次GC去回收掉那些已经不会再被使用到的对象
2、手动调用System.gc()方法,通常这样会触发一次的Full GC以及至少一次的Minor GC
3、程序运行的时候有一条低优先级的GC线程,它是一条守护线程,当这条线程处于运行状态的时候,自然就触发了一次GC了。
当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC
当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代
当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载
6.3 Minor GC
- 又称新生代GC,指发生在新生代的垃圾收集动作;
- 因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;
- 每次垃圾回收都有大批对象死去,只有少量存活,采用复制算法。
- Minor GC触发条件:当Eden区满时,触发Minor GC。
6.4 Full GC
- 又称老年代GC,指发生在老年代的GC;
- Full GC速度一般比Minor GC慢10倍以上;
- 对象存活率高,没有额外的空间对其分配担保,采用 标记-清除,标记-整理 算法
- Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
5. 发现虚拟机频繁full GC时应该怎么办:
(full GC指的是清理整个堆空间,包括年轻代和永久代)
(1) 首先用命令查看触发GC的原因是什么 jstat –gccause 进程id
(2) 如果是System.gc(),则看下代码哪里调用了这个方法
(3) 如果是heap inspection(内存检查),可能是哪里执行jmap –histo[:live]命令
(4) 如果是GC locker,可能是程序依赖的JNI库的原因
6.5 虚拟机的GC过程
我们就详细看一下整个回收过程。
-
在初始阶段,新创建的对象被分配到Eden区,survivor的两块空间都为空。
-
当Eden区满了的时候,minor garbage 被触发
-
经过扫描与标记,存活的对象被复制到S0,不存活的对象被回收
-
在下一次的Minor GC中,Eden区的情况和上面一致,没有引用的对象被回收,存活的对象被复制到survivor区。然而在survivor区,S0的所有的数据都被复制到S1,需要注意的是,在上次minor GC过程中移动到S0中的两个对象在复制到S1后其年龄要加1。此时Eden区S0区被清空,所有存活的数据都复制到了S1区,并且S1区存在着年龄不一样的对象,过程如下图所示:
-
再下一次MinorGC则重复这个过程,这一次survivor的两个区对换,存活的对象被复制到S0,存活的对象年龄加1,Eden区和另一个survivor区被清空。
-
下面演示一下Promotion过程,再经过几次Minor GC之后,当存活对象的年龄达到一个阈值之后(可通过参数配置,默认是8),就会被从年轻代Promotion到老年代。
-
随着MinorGC一次又一次的进行,不断会有新的对象被promote到老年代。
-
上面基本上覆盖了整个年轻代所有的回收过程。最终,MajorGC将会在老年代发生,老年代的空间将会被清除和压缩。
年轻代:从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下(基于大部分对象存活周期很短的事实)高效,如果在老年代采用停止复制,则是非常不合适的。
老年代:老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-压缩算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC,否则,就查看是否设置了-XX:+HandlePromotionFailure
(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC(这代表着如果设置-XX:+Handle PromotionFailure
,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。
永久代:永久代是用于存放静态文件,如Java类、方法等。 关于方法区即永久代的回收,永久代的回收有两种:常量池中的常量,无用的类信息。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class。永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。
1. 常量的回收很简单,没有引用了就可以被回收。
2. 类需要同时满足下面3个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
7 回收算法
7.1 按照基本回收策略分
(1)标记-清除(Mark-sweep)
该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象.这是最基础的算法,后续的收集算法都是基于这个算法扩展的。
缺点:
- 效率问题: 效率低,标记和清除过程的效率都不高;
- 空间问题: 标记清除后会产生大量 不连续的内存碎片, 空间碎片太多,可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集.
(2)复制(Copying)
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。效果图如下:
优点
- 由于是每次都对整个半区进行内存 回收,内存分配时不必考虑内存碎片问题。
- 垃圾回收后空间连续,只要移动堆顶指针,按顺序分配内存即可;
- 特别适合java朝生夕死的对象特点
- 效率高
缺点
- 内存减少为原来的一半,太浪费了;
- 对象存活率较高的时候就要执行较多的复制操作,效率变低;
- 如果不使用50%的对分策略,老年代需要考虑的空间担保策略
(3)标记-整理(Mark-Compact)
该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象 ( 可达性分析 ), 在标记完成后让所有存活的对象都向一端移动,然后清理掉端边界以外的 内存。
此算法结合了“标记-清除”和“复制”两个算法的优点。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。效果图如下:
优点
- 不会损失50%的空间;
- 垃圾回收后空间连续,只要移动堆顶指针,按顺序分配内存即可;
- 比较适合有大量存活对象的垃圾回收
缺点
- 标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象, 还要整理所有存活对象的引用地址。 从效率上来说,标记/整理算法要低 于复制算法。
7.2 按分区对待的方式分
(4)增量收集(Incremental Collecting)
实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。
(5)分代收集(Generational Collecting)
把对象分为年轻代、年老代、持久代(元空间),对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。
为什么要分代?
1. 新生代,每次垃圾回收都会有大量对象死去,可用复制算法,只需要付出少量对象复制成本就可以完成GC
2. 老年代,存活几率高,没有额外空间对它进行分配担保,必须选择(标记-清除,标记-整理)进行GC
7.3 按系统线程分(回收器类型)
(6)串行收集
串行收集使用单线程处理所有垃圾回收工作
优点:无需多线程交互,实现容易,而且效率比较高。
局限性:无法使用多处理器的优势
适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。默认使用串行收集器。
(7)并行收集Parallel
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;如ParNew、Parallel Scavenge、Parallel Old;
并行收集使用多线程处理垃圾回收工作
优点: 速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。
适合对吞吐量优先,无过多交互的应用。吞吐量(Throughput)=业务处理时间/(业务处理时间+垃圾回收时间)。
(8)并发收集Concurrent
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上; 如CMS、G1(也有并行);
相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。
并发收集器不会暂停应用,适合响应时间优先的应用。
优点: 保证系统的响应时间,减少垃圾收集时的停顿时间。
适用于应用服务器、电信领域等。
8. 垃圾收集器
JVM是一个进程,垃圾收集器就是一个线程,垃圾收集线程是一个守护线程,优先级低,其在当前系统空闲或堆中老年代占用率较大时触发。
JDK7/8后,HotSpot虚拟机所有收集器及组合(连线),如下图:
新生代收集器还是老年代收集器:
- 新生代收集器:Serial、ParNew、Parallel Scavenge;
- 老年代收集器:Serial Old、Parallel Old、CMS;
- 整堆收集器:G1
吞吐量优先、停顿时间优先
- 吞吐量优先:Parallel Scavenge收集器、Parallel Old 收集器。【交互少,计算多,适合在后台运算的场景】
- 停顿时间优先:CMS(Concurrent Mark-Sweep)收集器。【交互多,对响应速度要求高】
串行并行并发
- 串行:Serial、Serial Old
- 并行:ParNew、Parallel Scavenge、Parallel Old
- 并发:CMS、G1
算法
- 复制算法:Serial、ParNew、Parallel Scavenge、G1
- 标记-清除:CMS
- 标记-整理:Serial Old、Parallel Old、G1
8.1 Serial收集器
Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;JDK1.3.1前是HotSpot新生代收集的唯一选择;
特点: 串行(单线程),新生代,复制算法,STW(Stop the world), 响应速度优先
适用环境:单CPU环境下的Client模式
Tips:单线程一方面意味着它只会使用一个CPU或一条线程去完成垃圾收集工作,另一方面也意味着在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束为止,这个过程也称为 Stop The world。后者意味着,在用户不可见的情况下要把用户正常工作的线程全部停掉,这显然对很多应用是难以接受的。
Tips:Stop the World是在用户不可见的情况下执行的,会造成某些应用响应变慢; Tips:因为新生代的特点是对象存活率低,所以收集算法用的是复制算法,把新生代存活对象复制到老年代,复制的内容不多,性能较好。 Tips:单线程地好处就是减少上下文切换,减少系统资源的开销,提高效率。但这种方式的缺点也很明显,在GC的过程中,会暂停程序的执行。若GC不是频繁发生,这或许是一个不错的选择,否则将会影响程序的执行性能。 对于新生代来说,区域比较小,停顿时间短,所以比较使用。 参数 -XX:+UseSerialGC:串联收集器 Tips:在JDK Client模式,不指定VM参数,默认是串行垃圾回收器
8.2 ParNew收集器
ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。
特点: 并行(多线程),新生代,复制算法,STW(Stop the world), 响应速度优先
应用场景:多CPU环境下在Serer模式下与CMS配合
ParNew收集器的工作过程如下图:
ParNew收集器除了使用多线程收集外,其他与Serial收集器相比并无太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作,CMS收集器是JDK 1.5推出的一个具有划时代意义的收集器,具体内容将在稍后进行介绍。 ParNew 收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。 特点 ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余行为和Serial收集器完全一样,包括Serial收集器可用的所有控制参数、收集算法、Stop The world、对象分配规则、回收策略等都一样。在实现上也共用了相当多的代码。
应用场景 ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。很重要的原因是:除了Serial收集器之外,目前只有它能与CMS收集器配合工作(看图)。在JDK1.5时期,HotSpot推出了一款几乎可以认为具有划时代意义的垃圾收集器-----CMS收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
参数 "-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器; "-XX:+UseParNewGC":强制指定使用ParNew; "-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同; 为什么只有ParNew能与CMS收集器配合 CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作; CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作; 因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;
8.3 Parallel Scavenge收集器
Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。
特点: 并行(多线程),新生代,复制算法,STW(Stop the world), 高吞吐量为目标
应用场景:在后台运算而不需要太多交互的任务
Parallel Scavenge收集器和ParNew类似,新生代的收集器,同样用的是复制算法,也是并行多线程收集。与ParNew最大的不同,它关注的是垃圾回收的吞吐量。
应用场景 Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器。 高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;适合那种交互少、运算多的场景 例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序; 参数 "-XX:+MaxGCPauseMillis":控制最大垃圾收集停顿时间,大于0的毫秒数;这个参数设置的越小,停顿时间可能会缩短,但也会导致吞吐量下降,导致垃圾收集发生得更频繁。 "-XX:GCTimeRatio":设置垃圾收集时间占总时间的比率,0<n<100的整数,就相当于设置吞吐量的大小。 先垃圾收集执行时间占应用程序执行时间的比例的计算方法是:1 / (1 + n);
例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%=1/(1+19); 默认值是1%--1/(1+99),即n=99; 垃圾收集所花费的时间是年轻一代和老年代收集的总时间; "-XX:+UseAdptiveSizePolicy" 开启这个参数后,就不用手工指定一些细节参数,如:
新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等; JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomiscs); Parallel Scavenge收集器 VS CMS等收集器: Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。 由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。
另外值得注意的一点是,Parallel Scavenge收集器无法与CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge收集器,老年代只有Serial Old收集器能与之配合使用。
Parallel Scavenge收集器 VS ParNew收集器:
Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。
8.4 Serial Old 收集器
Serial Old是 Serial收集器的老年代版本
特点: 串行(单线程),老年代,标记-整理算法,STW(Stop the world),响应速度优先
应用场景:单CPU环境下的Client模式、CMS的后备预案
Serial收集器的工作流程如下图:
如上图所示,Serial 收集器在新生代和老年代都有对应的版本,除了收集算法不同,两个版本并没有其他差异。 Serial 新生代收集器采用的是复制算法。 Serial Old 老年代采用的是标记 - 整理算法。 应用场景 Client模式:Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。 Server模式:如果在Server模式下,那么它主要还有两大用途:
一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用;
另一种用途就是作为CMS收集器的后备预案,在并发收集发生"Concurrent Mode Failure"时使用。
8.5 Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年版本,它也使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6开始提供。
特点: 并行(多线程),老年代,标记-整理算法(还有压缩,Mark-Sweep-Compact),STW(Stop the world),吞吐量优先
应用场景:在后台运算而不需要太多交互的任务
如上图所示,Parallel 收集器在新生代和老年代也都有对应的版本,除了收集算法不同,两个版本并没有其他差异。 特点 Parallel Old是Parallel Scavenge的老年代版本 Parallel Old 老年代采用的是标记 - 整理算法,其他特点与Parallel Scavenge相同 使用场景 在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器组合。 JDK1.6及之后用来代替老年代的Serial Old收集器; 特别是在Server模式,多CPU的情况下; 参数 -XX:+UseParallelOldGC:指定使用Parallel Old收集器;
8.6 CMS(Concurrent Mark Sweep)收集器
CMS是HotSpot在JDK5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
特点: 并发(多线程),老年代,标记-清除算法 (不进行压缩操作,产生内存碎片),收集过程中不需要暂停用户线程,以获取最短回收停顿时间为目标
应用场景:与用户交互较多的场景,互联网或者B/S系统,重视响应速度和用户体验的应用
它关注的是垃圾回收最短的停顿时间(低停顿),在老年代并不频繁GC的场景下,是比较适用的。
CMS,全称Concurrent Mark and Sweep,用于对年老代进行回收,目标是尽量减少应用的暂停时间,减少full gc发生的机率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代
CMS并非没有暂停,而是用两次短暂停来替代串行标记整理算法的长暂停。
应用场景 与用户交互较多的场景。CMS 收集器是一种以获取最短回收停顿时间为目标的收集器。
目前很大一部分的Java应用集中在互联网或者B/S系统的服务端上,这类应用尤其注重服务的响应速度,希望系统停顿时间最短,以给用户带来极好的体验。 CMS是一种以获取最短回收停顿时间为目标的收集器。在重视响应速度和用户体验的应用中,CMS应用很多。
参数
-XX:+UseConcMarkSweepGC:使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection:Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction:设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads:设定CMS的线程数量(一般情况约等于可用CPU数量)
8.6.1 CMS GC收集周期
CMS GC收集周期分四步完成: 初始标记(initial mark)STW 耗时最短,并发标记(concurrent mark)耗时最长,重新标记(remark)STW 耗时较长,并发清除(concurrent sweep)耗时最长.
1、初始标记(initial mark)
这个阶段的任务是标记老年代中被GC Roots直接可达和被年轻代对象引用的对象,这个阶段也是第一次STW发生的阶段
特点:单线程执行;需要“Stop The World”;仅把GC Roots的直接关联可达的对象给标记一下,由于直接关联对象比较小,所以这里的速度非常快。
2、并发标记(concurrent mark)
这个阶段主要是通过从初始标记阶段中寻找到的标记对象开始,遍历老年代并且标记所有存活着的对象(可达性分析)
特点:这个阶段与应用程序共同运行,其他线程仍可以继续工作。此处时间较长,但不停顿。并不能保证可以标记出所有的存活对象;
需要注意的是,并非所有在老年代中存活的对象都会被标记,因为程序在标记期间可能会更改引用(比如图中的Current obj,它是并发标记阶段伴随着程序一起被删除了引用的对象)
3、Concurrent Preclean 执行预清理
注: 相当于两次 concurrent-mark. 因为上一次concurrent-mark耗时较长,会有从新生代晋升到老年代的对象出现,将其清理掉
这也是一个并发阶段,与应用程序的线程并行执行。并发标记阶段与应用程序同时运行时,一些对象的引用可能会被改变,一旦这种情况发生,JVM就会标记堆上面的这块包含了变化对象的区域(这个堆的区域被称为"Card",这种方式被称为"Card Marking")
在这个阶段,这些脏对象将会被声明,并且这些对象能够到达的对象也会被标记。这些Card将会在上面的工作完成之后被清理掉
此外,还将执行一些必要的整理和重新标记阶段的准备工作。
4、Concurrent Abortable Preclean 执行可中止预清理
这个阶段也是和程序线程并发执行的。它的工作就是尽可能地进行清理工作,以减少重新标记阶段的任务(即减少了STW的停顿时间)
这个阶段的持续时间取决于很多因素,因为它需要不断地做一些相同的工作,直到满足某个终止条件为止(比如一定的迭代次数、一定的有效工作量、一定的时间等等)
5、重新标记(Final remark)
这个阶段是第二次,也是最后一次STW。这个阶段的目的是标记在老年代中被标记的所有存活下来的对象。
在并发标记的过程中,由于可能还会产生新的垃圾,所以此时需要重新标记新产生的垃圾。
此处执行并行标记,与用户线程不并发,所以依然是“Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短。
6、并发清除(concurrent sweep)
并发清除之前所标记的垃圾,移除未使用的对象,并且回收其占用的空间。其他用户线程仍可以工作,不需要停顿。
7、Concurrent Reset 并发重置
重置CMS算法内部的数据结构,为下一个周期做准备
8、总结
Tips:其中,初始标记和重写标记仍然需要Stop the World、初始标记仅仅标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC RootsTracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段长,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以整体上说,CMS收集器的内存回收过程是与用户线程一共并发执行的。
8.6.2 CMS 缺点
1、对CPU资源非常敏感
对CPU资源非常敏感 其实,面向并发设计的程序都对CPU资源比较敏感。
在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
CMS默认启动的回收线程数是(CPU数量+3)/ 4,
也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。
但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。
2、浮动垃圾(Floating Garbage)
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
由于在垃圾收集阶段用户线程还需要运行,那就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,也可以认为CMS所需要的空间比其他垃圾收集器大;
"-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间;
JDK1.5默认值为68%;
JDK1.6变为大约92%;
3、"Concurrent Mode Failure"失败
如果CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样会导致另一次Full GC的产生。这样停顿时间就更长了,代价会更大,所以 "-XX:CMSInitiatingOccupancyFraction"不能设置得太大。
4、产生大量内存碎片
这个问题并不是CMS的问题,而是算法的问题。由于CMS基于"标记-清除"算法,清除后不进行压缩操作,所以会产生碎片
"标记-清除"算法介绍时曾说过:产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。
碎片解决方法:
(1)"-XX:+UseCMSCompactAtFullCollection"
使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;
但合并整理过程无法并发,停顿时间会变长;
默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);
(2)"-XX:+CMSFullGCsBeforeCompaction"
设置执行多少次不压缩的Full GC后,来一次压缩整理;
为减少合并整理过程的停顿时间;
默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大;
总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;最大的优点就是低停顿。
8.6.3 补充
CMS减少停顿的原理: 标记过程分三步:
并发标记是最主要的标记过程,而这个过程是并发执行的,可以与应用程序线程同时进行,
初始标记和重新标记虽然不能和应用程序并发执行,但这两个过程标记速度快,时间短,所以对应用程序不会产生太大的影响。
最后并发清除的过程,也是和应用程序同时进行的,避免了应用程序的停顿。 CMS的特点: 减少了应用程序的停顿时间,让回收线程和应用程序线程可以并发执行,它的回收并不彻底(浮动垃圾)。
因此CMS回收的频率相较其他回收器要高,频繁的回收将影响应用程序的吞吐量,空间碎片多。 CMS何时开始? cms gc 通过一个后台线程触发,该线程随着堆一起初始化,触发机制是默认每隔2秒判断一下当前老年代的内存使用率是否达到阈值,如果高于某个阈值的时候将激发CMS。 两次STW的原因: 当虚拟机完成两次标记后,便确认了可以回收的对象。
垃圾回收并不会阻塞程序的线程,如果当GC线程标记好了一个对象的时候,此时程序的线程又将该对象重新加入了GC-Roots的“关系网”中,当执行二次标记的时候,该对象也没有重写finalize()方法,因此回收的时候就会回收这个不该回收的对象。 为了解决这个问题,虚拟机会在一些特定指令位置设置一些“安全点”,当程序运行到这些“安全点”的时候就会暂停所有当前运行的线程(Stop The World 所以叫STW),暂停后再找到“GC Roots”进行关系的组建,进而执行标记和清除。 这些特定的指令位置主要在: 1、循环的末尾 2、方法临返回前 / 调用方法的call指令后 3、可能抛异常的位置
8.7 G1收集器
8.7.1 概述
G1(Garbage - First)名称的由来是G1跟踪各个Region里面的垃圾堆的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
G1(Garbage-First)是JDK7-u4才推出商用的收集器;
注意:G1与前面的垃圾收集器有很大不同,它把新生代、老年代的划分取消了!这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。
取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。
在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
PS:在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。
8.7.2 特点
G1除了降低停顿外,还能建立可预测的停顿时间模型;
1、Region概念
- 横跨整个堆内存
- 在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。
- G1在使用时,Java堆的内存布局与其他收集器有很大区别,
- 它将整个Java堆划分为多个大小相等的独立区域(Region),
- 虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(可以不连续)的集合。
2、可并行,可并发
- 能充分利用多CPU、多核环境下的硬件优势;
- 并行:使用多个CPU来缩短“Stop The World”停顿时间
- 并发:也可以并发让垃圾收集与用户程序同时进行
3、分代收集
- 收集范围包括新生代和老年代
- 能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
- 能够采用不同方式处理不同时期的对象;
4、结合多种垃圾收集算法(空间整合,不产生碎片)
- 从整体看,是基于标记-整理算法;
- 从局部(两个Region间)看,是基于复制算法;
- 都不会产生内存碎片,有利于长时间运行;
- 这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
5、可预测的停顿:低停顿的同时实现高吞吐量
- G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,
- 每次根据允许的收集时间,优先回收价值最大的Region,这样就保证了在有限的时间内尽可能提高效率。(这也就是Garbage-First名称的来由)。
- 这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
8.7.3 应用场景
如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。
1. 面向服务端应用,针对具有大内存、多处理器的机器;最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;
如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;
2. 用来替换掉JDK1.5的CMS收集器;
(1)超过50%的Java堆被活动数据占用;
(2)对象分配频率或年代提升频率变化很大;
(3)GC停顿时间过长(长与0.5至1秒)。
8.7.4 参数
"-XX:+UseG1GC":指定使用G1收集器;
"-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
"-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒;
"-XX:G1HeapRegionSize":设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;
8.7.5 G1收集器运作过程
不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。
1、初始标记(Initial Marking)
- 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,
- 速度很快,
- 需要“Stop The World”。(OopMap)
- 且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;
2、并发标记(Concurrent Marking)
- 进行GC Roots Tracing的过程,从刚才产生的集合中标记出存活对象;(也就是从GC Roots 开始对堆进行可达性分析,找出存活对象。)
- 耗时较长,但应用程序也在运行;
- 并不能保证可以标记出所有的存活对象;
3、最终标记(Final Marking)
- 最终标记和CMS的重新标记阶段一样,也是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,
- 这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,
- 也需要“Stop The World”。(修正Remebered Set)
- 上一阶段对象的变化记录在线程的Remembered Set Log,这里把Remembered Set Log合并到Remembered Set中;
- 采用多线程并行执行来提升效率。
4、筛选回收(Live Data Counting and Evacuation)
- 首先排序各个Region的回收价值和成本;
- 然后根据用户期望的GC停顿时间来制定回收计划;
- 最后按计划回收一些价值高的Region中垃圾对象;
- 回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;
- 可以并发进行,降低停顿时间,并增加吞吐量;
8.8 ZGC
8.8.1 概述
在JDK 11当中,加入了实验性质的ZGC。它的回收耗时平均不到2毫秒。它是一款低停顿高并发的收集器。
ZGC几乎在所有地方并发执行的,除了初始标记的是STW的。所以停顿时间几乎就耗费在初始标记上,这部分的实际是非常少的。那么其他阶段是怎么做到可以并发执行的呢?
ZGC主要新增了两项技术,一个是着色指针Colored Pointer,另一个是读屏障Load Barrier。
ZGC 是一个并发、基于区域(region)、增量式压缩的收集器。Stop-The-World 阶段只会在根对象扫描(root scanning)阶段发生,这样的话 GC 暂停时间并不会随着堆和存活对象的数量而增加。
与标记对象的传统算法相比,ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),
比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,
也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World。
ZGC虽然目前还在JDK 11还在实验阶段,但由于算法与思想是一个非常大的提升,相信在未来不久会成为主流的GC收集器使用。
ZGC回收机预计在jdk11支持,ZGC目前仅适用于Linux / x64 。和G1开启很像,用下面参数即可开启:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
8.8.2 ZGC 的设计目标
- TB 级别的堆内存管理;
- 最大 GC Pause 不高于 10ms;
- 最大的吞吐率(Throughput)损耗不高于 15%;
- 关键点:GC Pause 不会随着 堆大小的增加 而增大。
8.8.3 ZGC 中关键技术
- 加载屏障(Load barriers)技术;
- 有色对象指针(Colored pointers);
- 单一分代内存管理(没有分代);
- 基于区域的内存管理;
- 部分内存压缩;
- 即时内存复用。
8.8.4 并行化处理阶段
- 标记(Marking);
- 重定位(Relocation)/压缩(Compaction);
- 重新分配集的选择(Relocation set selection);
- 引用处理(Reference processing);
- 弱引用的清理(WeakRefs Cleaning);
- 字符串常量池(String Table)和符号表(Symbol Table)的清理;
- 类卸载(Class unloading)。
8.8.5 并发执行的保证机制:Colored Pointer 和 Load Barrier
所有阶段几乎都是并发执行的
这里的并发(Concurrent),说的是应用线程与GC线程齐头并进,互不添堵。
几乎就是还有三个非常短暂的STW的阶段,所以ZGC并不是Zero Pause GC啦。比如开始的Pause Mark Start阶段,要做根集合(root set)扫描,包括全局变量啊、线程栈啊啥的里面的对象指针,但不包括GC堆里的对象指针,所以这个暂停就不会随着GC堆的大小而变化(不过会根据线程的多少啊、线程栈的大小之类的而变化)”。
着色指针Colored Pointer
ZGC利用指针的64位中的几位表示Finalizable、Remapped、Marked1、Marked0(ZGC仅支持64位平台),以标记该指向内存的存储状态。相当于在对象的指针上标注了对象的信息。注意,这里的指针相当于Java术语当中的引用。(所以它不支持32位指针也不支持压缩指针, 且堆的上限是4TB。)
在这个被指向的内存发生变化的时候(内存在Compact被移动时),颜色就会发生变化。
在G1的时候就说到过,Compact阶段是需要STW,否则会影响用户线程执行。那么怎么解决这个问题呢?
读屏障Load Barrier
由于着色指针的存在,在程序运行时访问对象的时候,可以轻易知道对象在内存的存储状态(通过指针访问对象),若请求读的内存在被着色了,那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有一定的耗费,从而达到与用户线程并发的效果。
8.8.6 像G1一样划分Region,但更加灵活
ZGC将堆划分为Region作为清理,移动,以及并行GC线程工作分配的单位。
不过G1一开始就把堆划分成固定大小的Region,而ZGC 可以有2MB,32MB,N× 2MB 三种Size Groups,动态地创建和销毁Region,动态地决定Region的大小。
256k以下的对象分配在Small Page, 4M以下对象在Medium Page,以上在Large Page。
所以ZGC能更好的处理大对象的分配。
![](https://img2018.cnblogs.com/blog/728328/201908/728328-20190814142353234-1050634414.png)
8.8.7 和G1一样会做Compacting-压缩
CMS是Mark-Sweep标记过期对象后原地回收,这样就会造成内存碎片,越来越难以找到连续的空间,直到发生Full GC才进行压缩整理。
ZGC是Mark-Compact ,会将活着的对象都移动到另一个Region,整个回收掉原来的Region。
而G1 是 incremental copying collector,一样会做压缩。
1. Pause Mark Start -初始停顿标记
停顿JVM地标记Root对象,1,2,4三个被标为live。
![](https://img2018.cnblogs.com/blog/728328/201908/728328-20190814142422626-1396902386.png)
2. Concurrent Mark -并发标记
并发地递归标记其他对象,5和8也被标记为live。
![](https://img2018.cnblogs.com/blog/728328/201908/728328-20190814142508145-512686177.png)
3. Relocate - 移动对象
对比发现3、6、7是过期对象,也就是中间的两个灰色region需要被压缩清理,所以陆续将4、5、8 对象移动到最右边的新Region。
移动过程中,有个forward table纪录这种转向。
活的对象都移走之后,这个region可以立即释放掉,并且用来当作下一个要扫描的region的to region。所以理论上要收集整个堆,只需要有一个空region就OK了。
![](https://img2018.cnblogs.com/blog/728328/201908/728328-20190814142538765-409002580.png)
4. Remap - 修正指针
最后将指针都妥帖地更新指向新地址。上一个阶段的Remap,和下一个阶段的Mark是混搭在一起完成的,这样非常高效,省却了重复遍历对象图的开销。”
![](https://img2018.cnblogs.com/blog/728328/201908/728328-20190814142605876-1544604617.png)
8.8.8 没有G1占内存的Remember Set,没有Write Barrier的开销
G1 保证“每次GC停顿时间不会过长”的方式,是“每次只清理一部分而不是全部的Region”的增量式清理。那独立清理某个Region时 , 就需要有RememberSet来记录Region之间的对象引用关系, 这样就能依赖它来辅助计算对象的存活性而不用扫描全堆, RS通常占了整个Heap的20%或更高。
这里还需要使用Write Barrier(写屏障)技术,G1在平时写引用时,GC移动对象时,都要同步去更新RememberSet,跟踪跨代跨Region间的引用,特别的重。而CMS里只有新老生代间的CardTable,要轻很多。
ZGC几乎没有停顿,所以划分Region并不是为了增量回收,每次都会对所有Region进行回收,所以也就不需要这个占内存的RememberSet了,又因为它暂时连分代都还没实现,所以完全没有Write Barrier。
8.8.9 单代
没分代,应该是ZGC唯一的弱点了。
分代原本是因为most object die young的假设,而让新生代和老生代使用不同的GC算法。
如果对整个堆做一个完整并发收集周期,持续的时间可能很长比如几分钟,而此期间新创建的对象,大致上只能当作活对象来处理,即使它们在这周期里其实早就死掉可以被收集了。如果有分代算法,新生对象都在一个专门的区域创建,专门针对这个区域的收集能更频繁更快,意外留活的对象更也少。
9. 常见配置汇总
- 堆设置
- -Xms:初始堆大小
- -Xmx:最大堆大小
- -XX:NewSize=n:设置年轻代大小
- -XX:NewRatio=n:设置年轻代和年老代的比值。默认为2.,表示年轻代与年老代比值为1:2,年轻代占整个年轻代年老代和的1/3
- -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。默认为8,表示Eden:Survivor=8:2,一个Survivor区占整个年轻代的1/10
- -XX:MaxPermSize=n:设置持久代大小
- 收集器设置
- -XX:+UseSerialGC:设置串行收集器
- -XX:+UseParallelGC:设置并行收集器
- -XX:+UseParalledlOldGC:设置并行年老代收集器
- -XX:+UseConcMarkSweepGC:设置并发收集器
- 垃圾回收统计信息
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename
- 并行收集器设置
- -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
- -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
- 并发收集器设置
- -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
- -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
学习网址
- JVM基础(三):Java垃圾回收
- Java虚拟机(JVM)你只要看这一篇就够了!(**************)
- Java——七种垃圾收集器+JDK11最新ZGC
- https://note.youdao.com/ynoteshare1/index.html?id=920f10f97acfc22fe0c27cc52a97cb28&type=note
- 如何判断对象是否死亡
- 如何判断一个类是无用的类?
- https://blog.csdn.net/qq_18657175/article/category/8862992
- jvm原理,内存模型及GC机制
- JVM 完整深入解析
- https://www.jianshu.com/p/904b15a8281f
- Java虚拟机(JVM)你只要看这一篇就够了!
- Java中的常量池(字符串常量池、class常量池和运行时常量池)
- JDK11的ZGC - 学习笔记
- 问十六:你了解哪些垃圾收集器呢?
- 【Java面试整理之JVM】深入理解JVM结构、类加载机制、垃圾回收GC原理、JVM内存分配策略、JVM内存泄露和溢出
- jvm原理,内存模型及GC机制
- JVM内存管理及GC机制
- 学习JVM是如何从入门到放弃的?