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)来表明超过多大时就不在新生代分配,而是直接在老年代分配。 ②大的数组对象,且数组中无引用外部对象。
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 带来没必要要的复杂度,而且回收效率偏低。
若是不进行垃圾回收,内存早晚都会被消耗空,由于咱们在不断的分配内存空间而不进行回收。除非内存无限大,咱们能够任性的分配而不回收,可是事实并不是如此。因此,垃圾回收是必须的。Java 中的垃圾回收通常是在 Java 堆中进行,由于堆中几乎存放了 Java 中全部的对象实例。html
在java中,程序员是不须要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常状况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。java
垃圾回收器负责:
分配内存
保证全部正在被引用的对象还存在于内存中
回收执行代码已经再也不引用的对象所占的内存
GC自己是会周期性的自动运行的,由JVM决定运行的时机,并且如今的版本有多种更智能的模式能够选择,还会根据运行的机器自动去作选择,就算真的有性能上的需求,也应该去对GC的运行机制进行微调,而不是经过使用这个命令来实现性能的优化。程序员
每一个 Java 应用程序都有一个 Runtime 类实例,使应用程序可以与其运行的环境相链接。能够经过 getRuntime 方法获取当前运行。java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的简写,二者的行为没有任何不一样。惟一的区别就是System.gc()写起来比Runtime.getRuntime().gc()简单点. 其实基本没什么机会用获得这个命令, 由于这个命令只是建议JVM安排GC运行, 还有可能彻底被拒绝。 面试
GC在后台自动发起和自动完成的,在用户不可见的状况下,把用户正常的工做线程所有停掉,即GC停顿,会带给用户不良的体验;算法
为何要Stop-The-World?
可达性分析的时候为了确保快照的一致性,须要对整个系统进行冻结,不能够出现分析过程当中对象引用关系还在不断变化的状况,也就是Stop-The-World。segmentfault
Stop-The-World是致使GC卡顿的重要缘由之一。数组
串行和并行都会致使STW,并发不会致使STW缓存
概念:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任什么时候刻计数器为0的对象就是不可能再被使用的。安全
可是:主流的java虚拟机并无选用引用计数算法来管理内存,其中最主要的缘由是:它很难解决对象之间相互循环引用的问题。服务器
优势:算法的实现简单,断定效率也高,大部分状况下是一个不错的算法。不少地方应用到它
缺点:引用和去引用伴随加法和减法,影响性能
致命的缺陷:对于循环引用的对象没法进行回收
概念:这个算法的基本思路就是经过一系列的称谓“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证实此对象是不可用的。
可达性分析:
在Java语言中,可做为GC Roots的对象包括下面几种:
1.虚拟机栈(栈帧中本地变量表)中引用的对象。
2.本地方法栈中JNI(即通常说的Native方法)引用的对象。
3.方法区中类静态属性引用的对象。
4.方法区中常量引用的对象。
在 JDK1.2 以后,Java 对引用的概念进行了扩充,将其分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,引用强度依次减弱。
强引用具有如下三个个特色:
如“Object obj = new Object()”,这类引用是 Java 程序中最广泛的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
用来描述一些还有用但并不是必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常以前,将会把这些对象列进回收范围之中进行第二次回收。若是此次回收尚未足够的内存,才会抛出内存溢出异常。在 JDK 1.2 以后,提供了 SoftReference 类来实现软引用。
软引用能够加速JVM对垃圾内存的回收速度,能够维护系统的运行安全,防止内存溢出等问题。
对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。所以,这一点能够很好地用来解决OOM的问题,而且这个特性很适合用来实现缓存:好比网页缓存、图片缓存等
用来描述非必须的对象,可是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发送以前。当垃圾收集器工做时,不管当前内存是否足够,都会回收掉只被弱引用关联的对象。一旦一个弱引用对象被垃圾回收器回收,便会加入到一个注册引用队列中。在 JDK 1.2 以后,提供了 WeakReference类来实现弱引用。
Tips:软引用、弱引用都很是适合来保存那些无关紧要的缓存数据。若是这么作,当系统内存不足时,这些缓存数据会被回收,不会致使内存溢出。而当内存资源充足时,这些缓存数据又能够存在至关长的时间,从而起到加速系统的做用。
它是最弱的一种引用关系。一个持有虚引用的对象,和没有引用几乎是同样的,随时都有可能被垃圾回收器回收。当试图经过虚引用的get()方法取得强引用时,老是会失败。而且,虚引用必须和引用队列一块儿使用,它的做用在于跟踪垃圾回收过程。在 JDK 1.2 以后,提供了 PhantomReference类来实现虚引用。
可达性分析算法中不可达的对象,并不是是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
一、对象在进行可达性分析后被发现不可达,它将会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,那么就不必执行finalize()方法;若是被断定为有必要执行finalize()方法,那么此对象将会放置在一个叫作F-Quenen的队列之中,并在稍后由一个虚拟机自动创建的、低优先级的Finalize线程去触发这个方法。
二、稍后GC将对F-Quenen中的对象进行第二次小规模的标记,若是对象要在finalize()中成功拯救本身——只要从新与引用链上的任何一个对象创建关系便可,譬如把本身(this关键字)赋值给某个类变量或者对象的成员变量,那么在第二次标记时它将被移出“即将回收”集合;finalize()方法是对象逃脱死亡的最后一次机会,若是对象这时候尚未成功逃脱,那他就会真的被回收了。
若是不使用finalize()方法逃脱的话,二次标记后删除。
常量未被引用
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
类须要同时知足下面3个条件才能算是 “无用的类” :
虚拟机能够对知足上述3个条件的无用类进行回收,这里说的仅仅是“能够”,而并非和对象同样不使用了就会必然被回收。
对象分配
对象优先在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来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。
在一开始的时候,JVM的GC就是采用标记-清除-压缩方式进行的,这么作并非很高效,由于当对象分配的愈来愈多时,对象列表也越来也大,扫描和移动愈来愈耗时,形成了内存回收愈来愈慢。然而,通过根据对java应用的分析,发现大部分对象的存活时间都很是短,只有少部分数据存活周期是比较长的,请看下面对java对象内存存活时间的统计:
从图表中能够看出,大部分对象存活时间是很是短的,随着时间的推移,被分配的对象愈来愈少。
1.1 为何要分代 1. 给堆内存分代是为了提升对象内存分配和垃圾回收的效率。 2. 还能够根据不一样年代的特色采用合适的垃圾收集算法。
堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,咱们程序全部的对象实例都存放在堆内存中。 试想一下,若是堆内存没有区域划分,全部的新建立的对象和生命周期很长的对象放在一块儿,随着程序的执行,堆内存须要频繁进行垃圾收集, 而每次回收都要遍历全部的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响咱们的GC效率,这简直太可怕了。 有了内存分代,状况就不一样了, 1. 新建立的对象会在新生代中分配内存, 2. 通过屡次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中, 3. 新生代中的对象存活时间短,只须要在新生代区域中频繁进行GC, 4. 老年代中对象生命周期长,内存回收的频率相对较低,不须要频繁进行回收, 5. 永久代中回收效果太差,通常不进行垃圾回收 分代收集大大提高了收集效率,这些都是内存分代带来的好处
一、当年轻代或者老年代满了,Java虚拟机没法再为新的对象分配内存空间了,那么Java虚拟机就会触发一次GC去回收掉那些已经不会再被使用到的对象
二、手动调用System.gc()方法,一般这样会触发一次的Full GC以及至少一次的Minor GC
三、程序运行的时候有一条低优先级的GC线程,它是一条守护线程,当这条线程处于运行状态的时候,天然就触发了一次GC了。
当年轻代内存满时,会引起一次普通GC,该GC仅回收年轻代。须要强调的时,年轻代尽是指Eden代满,Survivor满不会引起GC
当年老代满时会引起Full GC,Full GC将会同时回收年轻代、年老代
当永久代满时也会引起Full GC,会致使Class、Method元信息的卸载
(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库的缘由
咱们就详细看一下整个回收过程。
在初始阶段,新建立的对象被分配到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个条件才能算是 “无用的类” :
虚拟机能够对知足上述3个条件的无用类进行回收,这里说的仅仅是“能够”,而并非和对象同样不使用了就会必然被回收。
该算法分为“标记”和“清除”两个阶段: 首先标记出全部须要回收的对象(可达性分析), 在标记完成后统一清理掉全部被标记的对象.这是最基础的算法,后续的收集算法都是基于这个算法扩展的。
缺点:
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另一个区域中。此算法每次只处理正在使用中的对象,所以复制成本比较小,同时复制过去之后还能进行相应的内存整理,不会出现“碎片”问题。固然,此算法的缺点也是很明显的,就是须要两倍内存空间。效果图以下:
优势
缺点
该算法分为“标记”和“清除”两个阶段: 首先标记出全部须要回收的对象 ( 可达性分析 ), 在标记完成后让全部存活的对象都向一端移动,而后清理掉端边界之外的 内存。
此算法结合了“标记-清除”和“复制”两个算法的优势。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。效果图以下:
优势
缺点
实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么缘由JDK5.0中的收集器没有使用这种算法的。
把对象分为年轻代、年老代、持久代(元空间),对不一样生命周期的对象使用不一样的算法(上述方式中的一个)进行回收。
为何要分代?
1. 新生代,每次垃圾回收都会有大量对象死去,可用复制算法,只须要付出少许对象复制成本就能够完成GC
2. 老年代,存活概率高,没有额外空间对它进行分配担保,必须选择(标记-清除,标记-整理)进行GC
串行收集使用单线程处理全部垃圾回收工做
优势:无需多线程交互,实现容易,并且效率比较高。
局限性:没法使用多处理器的优点
适合单处理器机器。固然,此收集器也能够用在小数据量(100M左右)状况下的多处理器机器上。默认使用串行收集器。
指多条垃圾收集线程并行工做,但此时用户线程仍然处于等待状态;如ParNew、Parallel Scavenge、Parallel Old;
并行收集使用多线程处理垃圾回收工做
优势: 速度快,效率高。并且理论上CPU数目越多,越能体现出并行收集器的优点。
适合对吞吐量优先,无过多交互的应用。吞吐量(Throughput)=业务处理时间/(业务处理时间+垃圾回收时间)。
指用户线程与垃圾收集线程同时执行(但不必定是并行的,可能会交替执行);
用户程序在继续运行,而垃圾收集程序线程运行于另外一个CPU上; 如CMS、G1(也有并行);
相对于串行收集和并行收集而言,前面两个在进行垃圾回收工做时,须要暂停整个运行环境,而只有垃圾回收程序在运行,所以,系统在垃圾回收时会有明显的暂停,并且暂停时间会由于堆越大而越长。
并发收集器不会暂停应用,适合响应时间优先的应用。
优势: 保证系统的响应时间,减小垃圾收集时的停顿时间。
适用于应用服务器、电信领域等。
JVM是一个进程,垃圾收集器就是一个线程,垃圾收集线程是一个守护线程,优先级低,其在当前系统空闲或堆中老年代占用率较大时触发。
JDK7/8后,HotSpot虚拟机全部收集器及组合(连线),以下图:
新生代收集器仍是老年代收集器:
吞吐量优先、停顿时间优先
串行并行并发
算法
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参数,默认是串行垃圾回收器
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收集器代码框架,而另外独立实现;而其他几种收集器则共用了部分的框架代码;
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收集器的一个重要区别是它具备自适应调节策略。
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"时使用。
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收集器;
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数量)
CMS GC收集周期分四步完成: 初始标记(initial mark)STW 耗时最短,并发标记(concurrent mark)耗时最长,从新标记(remark)STW 耗时较长,并发清除(concurrent sweep)耗时最长.
一、初始标记(initial mark)
这个阶段的任务是标记老年代中被GC Roots直接可达和被年轻代对象引用的对象,这个阶段也是第一次STW发生的阶段
特色:单线程执行;须要“Stop The World”;仅把GC Roots的直接关联可达的对象给标记一下,因为直接关联对象比较小,因此这里的速度很是快。
二、并发标记(concurrent mark)
这个阶段主要是经过从初始标记阶段中寻找到的标记对象开始,遍历老年代而且标记全部存活着的对象(可达性分析)
特色:这个阶段与应用程序共同运行,其余线程仍能够继续工做。此处时间较长,但不停顿。并不能保证能够标记出全部的存活对象;
须要注意的是,并不是全部在老年代中存活的对象都会被标记,由于程序在标记期间可能会更改引用(好比图中的Current obj,它是并发标记阶段伴随着程序一块儿被删除了引用的对象)
三、Concurrent Preclean 执行预清理
注: 至关于两次 concurrent-mark. 由于上一次concurrent-mark耗时较长,会有重新生代晋升到老年代的对象出现,将其清理掉
这也是一个并发阶段,与应用程序的线程并行执行。并发标记阶段与应用程序同时运行时,一些对象的引用可能会被改变,一旦这种状况发生,JVM就会标记堆上面的这块包含了变化对象的区域(这个堆的区域被称为"Card",这种方式被称为"Card Marking")
在这个阶段,这些脏对象将会被声明,而且这些对象可以到达的对象也会被标记。这些Card将会在上面的工做完成以后被清理掉
此外,还将执行一些必要的整理和从新标记阶段的准备工做。
四、Concurrent Abortable Preclean 执行可停止预清理
这个阶段也是和程序线程并发执行的。它的工做就是尽量地进行清理工做,以减小从新标记阶段的任务(即减小了STW的停顿时间)
这个阶段的持续时间取决于不少因素,由于它须要不断地作一些相同的工做,直到知足某个终止条件为止(好比必定的迭代次数、必定的有效工做量、必定的时间等等)
五、从新标记(Final remark)
这个阶段是第二次,也是最后一次STW。这个阶段的目的是标记在老年代中被标记的全部存活下来的对象。
在并发标记的过程当中,因为可能还会产生新的垃圾,因此此时须要从新标记新产生的垃圾。
此处执行并行标记,与用户线程不并发,因此依然是“Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短。
六、并发清除(concurrent sweep)
并发清除以前所标记的垃圾,移除未使用的对象,而且回收其占用的空间。其余用户线程仍能够工做,不须要停顿。
七、Concurrent Reset 并发重置
重置CMS算法内部的数据结构,为下一个周期作准备
八、总结
Tips:其中,初始标记和重写标记仍然须要Stop the World、初始标记仅仅标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC RootsTracing的过程,而从新标记阶段则是为了修正并发标记期间因用户程序继续运行而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段长,但远比并发标记的时间短。
因为整个过程当中耗时最长的并发标记和并发清除过程收集器线程均可以与用户线程一块儿工做,因此总体上说,CMS收集器的内存回收过程是与用户线程一共并发执行的。
一、对CPU资源很是敏感
对CPU资源很是敏感 其实,面向并发设计的程序都对CPU资源比较敏感。
在并发阶段,它虽然不会致使用户线程停顿,但会由于占用了一部分线程(或者说CPU资源)而致使应用程序变慢,总吞吐量会下降。
CMS默认启动的回收线程数是(CPU数量+3)/ 4,
也就是当CPU在4个以上时,并发回收时垃圾收集线程很多于25%的CPU资源,而且随着CPU数量的增长而降低。
可是当CPU不足4个时(好比2个),CMS对用户程序的影响就可能变得很大,若是原本CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能致使用户程序的执行速度突然下降了50%,其实也让人没法接受。
二、浮动垃圾(Floating Garbage)
因为CMS并发清理阶段用户线程还在运行着,伴随程序运行天然就还会有新的垃圾不断产生,这一部分垃圾出如今标记过程以后,CMS没法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
因为在垃圾收集阶段用户线程还须要运行,那就还须要预留有足够的内存空间给用户线程使用,所以CMS收集器不能像其余收集器那样等到老年代几乎彻底被填满了再进行收集,也能够认为CMS所须要的空间比其余垃圾收集器大;
"-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间;
JDK1.5默认值为68%;
JDK1.6变为大约92%;
三、"Concurrent Mode Failure"失败
若是CMS运行期间预留的内存没法知足程序须要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来从新进行老年代的垃圾收集,这样会致使另外一次Full GC的产生。这样停顿时间就更长了,代价会更大,因此 "-XX:CMSInitiatingOccupancyFraction"不能设置得太大。
四、产生大量内存碎片
这个问题并非CMS的问题,而是算法的问题。因为CMS基于"标记-清除"算法,清除后不进行压缩操做,因此会产生碎片
"标记-清除"算法介绍时曾说过:产生大量不连续的内存碎片会致使分配大内存对象时,没法找到足够的连续内存,从而须要提早触发另外一次Full GC动做。
碎片解决方法:
(1)"-XX:+UseCMSCompactAtFullCollection"
使得CMS出现上面这种状况时不进行Full GC,而开启内存碎片的合并整理过程;
但合并整理过程没法并发,停顿时间会变长;
默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);
(2)"-XX:+CMSFullGCsBeforeCompaction"
设置执行多少次不压缩的Full GC后,来一次压缩整理;
为减小合并整理过程的停顿时间;
默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
因为空间再也不连续,CMS须要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大;
整体来看,与Parallel Old垃圾收集器相比,CMS减小了执行老年代垃圾收集时应用暂停的时间;但却增长了新生代垃圾收集时应用暂停的时间、下降了吞吐量并且须要占用更大的堆空间;最大的优势就是低停顿。
CMS减小停顿的原理: 标记过程分三步:
并发标记是最主要的标记过程,而这个过程是并发执行的,能够与应用程序线程同时进行,
初始标记和从新标记虽然不能和应用程序并发执行,但这两个过程标记速度快,时间短,因此对应用程序不会产生太大的影响。
最后并发清除的过程,也是和应用程序同时进行的,避免了应用程序的停顿。 CMS的特色: 减小了应用程序的停顿时间,让回收线程和应用程序线程能够并发执行,它的回收并不完全(浮动垃圾)。
所以CMS回收的频率相较其余回收器要高,频繁的回收将影响应用程序的吞吐量,空间碎片多。 CMS什么时候开始? cms gc 经过一个后台线程触发,该线程随着堆一块儿初始化,触发机制是默认每隔2秒判断一下当前老年代的内存使用率是否达到阈值,若是高于某个阈值的时候将激发CMS。 两次STW的缘由: 当虚拟机完成两次标记后,便确认了能够回收的对象。
垃圾回收并不会阻塞程序的线程,若是当GC线程标记好了一个对象的时候,此时程序的线程又将该对象从新加入了GC-Roots的“关系网”中,当执行二次标记的时候,该对象也没有重写finalize()方法,所以回收的时候就会回收这个不应回收的对象。 为了解决这个问题,虚拟机会在一些特定指令位置设置一些“安全点”,当程序运行到这些“安全点”的时候就会暂停全部当前运行的线程(Stop The World 因此叫STW),暂停后再找到“GC Roots”进行关系的组建,进而执行标记和清除。 这些特定的指令位置主要在: 一、循环的末尾 二、方法临返回前 / 调用方法的call指令后 三、可能抛异常的位置
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中,持久代也移动到了普通的堆内存空间中,改成元空间。
G1除了下降停顿外,还能创建可预测的停顿时间模型;
一、Region概念
二、可并行,可并发
三、分代收集
四、结合多种垃圾收集算法(空间整合,不产生碎片)
五、可预测的停顿:低停顿的同时实现高吞吐量
若是你的应用追求低停顿,那G1如今已经能够做为一个可尝试选择,若是你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。
1. 面向服务端应用,针对具备大内存、多处理器的机器;最主要的应用是为须要低GC延迟,并具备大堆的应用程序提供解决方案;
如:在堆大小约6GB或更大时,可预测的暂停时间能够低于0.5秒;
2. 用来替换掉JDK1.5的CMS收集器;
(1)超过50%的Java堆被活动数据占用;
(2)对象分配频率或年代提高频率变化很大;
(3)GC停顿时间过长(长与0.5至1秒)。
"-XX:+UseG1GC":指定使用G1收集器;
"-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
"-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒;
"-XX:G1HeapRegionSize":设置每一个Region大小,范围1MB到32MB;目标是在最小Java堆时能够拥有约2048个Region;
不计算维护Remembered Set的操做,能够分为4个步骤(与CMS较为类似)。
一、初始标记(Initial Marking)
二、并发标记(Concurrent Marking)
三、最终标记(Final Marking)
四、筛选回收(Live Data Counting and Evacuation)
在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
这里的并发(Concurrent),说的是应用线程与GC线程齐头并进,互不添堵。
几乎就是还有三个很是短暂的STW的阶段,因此ZGC并非Zero Pause GC啦。好比开始的Pause Mark Start阶段,要作根集合(root set)扫描,包括全局变量啊、线程栈啊啥的里面的对象指针,但不包括GC堆里的对象指针,因此这个暂停就不会随着GC堆的大小而变化(不过会根据线程的多少啊、线程栈的大小之类的而变化)”。
ZGC利用指针的64位中的几位表示Finalizable、Remapped、Marked一、Marked0(ZGC仅支持64位平台),以标记该指向内存的存储状态。至关于在对象的指针上标注了对象的信息。注意,这里的指针至关于Java术语当中的引用。(因此它不支持32位指针也不支持压缩指针, 且堆的上限是4TB。)
在这个被指向的内存发生变化的时候(内存在Compact被移动时),颜色就会发生变化。
在G1的时候就说到过,Compact阶段是须要STW,不然会影响用户线程执行。那么怎么解决这个问题呢?
因为着色指针的存在,在程序运行时访问对象的时候,能够轻易知道对象在内存的存储状态(经过指针访问对象),若请求读的内存在被着色了,那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有必定的耗费,从而达到与用户线程并发的效果。
ZGC将堆划分为Region做为清理,移动,以及并行GC线程工做分配的单位。
不过G1一开始就把堆划分红固定大小的Region,而ZGC 能够有2MB,32MB,N× 2MB 三种Size Groups,动态地建立和销毁Region,动态地决定Region的大小。
256k如下的对象分配在Small Page, 4M如下对象在Medium Page,以上在Large Page。
因此ZGC能更好的处理大对象的分配。
CMS是Mark-Sweep标记过时对象后原地回收,这样就会形成内存碎片,愈来愈难以找到连续的空间,直到发生Full GC才进行压缩整理。
ZGC是Mark-Compact ,会将活着的对象都移动到另外一个Region,整个回收掉原来的Region。
而G1 是 incremental copying collector,同样会作压缩。
1. Pause Mark Start -初始停顿标记
停顿JVM地标记Root对象,1,2,4三个被标为live。
2. Concurrent Mark -并发标记
并发地递归标记其余对象,5和8也被标记为live。
3. Relocate - 移动对象
对比发现三、六、7是过时对象,也就是中间的两个灰色region须要被压缩清理,因此陆续将四、五、8 对象移动到最右边的新Region。
移动过程当中,有个forward table纪录这种转向。
活的对象都移走以后,这个region能够当即释放掉,而且用来看成下一个要扫描的region的to region。因此理论上要收集整个堆,只须要有一个空region就OK了。
4. Remap - 修正指针
最后将指针都妥帖地更新指向新地址。上一个阶段的Remap,和下一个阶段的Mark是混搭在一块儿完成的,这样很是高效,省却了重复遍历对象图的开销。”
G1 保证“每次GC停顿时间不会过长”的方式,是“每次只清理一部分而不是所有的Region”的增量式清理。那独立清理某个Region时 , 就须要有RememberSet来记录Region之间的对象引用关系, 这样就能依赖它来辅助计算对象的存活性而不用扫描全堆, RS一般占了整个Heap的20%或更高。
这里还须要使用Write Barrier(写屏障)技术,G1在平时写引用时,GC移动对象时,都要同步去更新RememberSet,跟踪跨代跨Region间的引用,特别的重。而CMS里只有新老生代间的CardTable,要轻不少。
ZGC几乎没有停顿,因此划分Region并非为了增量回收,每次都会对全部Region进行回收,因此也就不须要这个占内存的RememberSet了,又由于它暂时连分代都还没实现,因此彻底没有Write Barrier。
没分代,应该是ZGC惟一的弱点了。
分代本来是由于most object die young的假设,而让新生代和老生代使用不一样的GC算法。
若是对整个堆作一个完整并发收集周期,持续的时间可能很长好比几分钟,而此期间新建立的对象,大体上只能看成活对象来处理,即便它们在这周期里其实早就死掉能够被收集了。若是有分代算法,新生对象都在一个专门的区域建立,专门针对这个区域的收集能更频繁更快,意外留活的对象更也少。