四.JVM垃圾收集算法及常见的垃圾收集器<JVM学习>

首先,我们还是把之前的堆内存的图copy过来

我们知道,堆内存被分为年轻代和老年代,默认比例是1/3:2/3,年轻代又分eden区和survivor区,默认比例为8:1:1(之前的博文有详细的介绍,不清楚的可以翻看之前的博文)。

首先我们先来思考一个问题,为什么JVM要把堆内存设计成分代呢?

其实从能用性的角度上来看,不分代也能满足我们的要求,但是这边就会出现一个很严重的问题,如果堆内存不分代的话,那么我们所有的对象都被放在了同一块内存空间中,这样做gc回收垃圾的时候,我们就需要对堆所有的内存空间做一次扫描,这样太消耗性能了。我们都知道,我们很多的对象都是朝生夕死的,那么把这部分对象单独放在某一块内存空间中,当gc的时候优先把这一部分对象的区域进行回收,那么就可以腾出很大的空间出来了。(注意,年轻代的minor gc的效率要比full gc高很多)

分代收集理论

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块,然后根据不同的垃圾收集算法去回收。一般将java堆分为年轻代和老年代,这样我们就可以根据各个代的特点去选择合适的垃圾收集算法。

1.1 标记-复制算法

标记-复制算法可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活着的对象复制到另外一块内存中去,然后再把这一块的内存空间全部清理掉。这样就使得每次的内存回收都是对内存区间的一半进行回收。

标记复制算法的效率很高,但是说白了它只能使用一半的内存空间,对内存空间要求比较高。

1.2 标记-清除算法

标记-清除算法分为标记和清除两个阶段:标记存活的对象,统一回收没有被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,标间简单,但是会带来两个明显的问题

1.效率问题(如果需要标记的对象太多,效率不高)

2.空间问题(标记清除后会产生大量不连续的空间碎片)

1.3 标记-整理算法

标记-整理算法是根据老年代的特点特出的一种标记算法,标记过程仍和标记-清除算法一样,但后续的步骤不是直接对可回收对象进行回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存空间。

我们根据不同代的特点选择不同的垃圾收集算法。比如在年轻代中,每次收集都会有大量对象死去(近99%的年轻代对象都是朝生夕死),所以我们可以选择标记-复制算法,只需要付出少量对象的复制成本就可以完成每次的垃圾收集。而老年代中的对象存活几率比较高,而且没有额外的内存空间对它进行分配担保,所以我们必须选择标记-清除算法或者标记-整理算法进行垃圾回收。

注意:标记-清除算法或标记-整理算法会比复制算法慢10倍以上。

 

垃圾收集器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

我们来看下常见的几种垃圾收集器:

首先先要说明:虽然我们对各个垃圾收集器进行了比较,但是并非是为了挑选出一个最好的垃圾收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器。我们能做的就是根据自己的应用场景选择最合适的垃圾收集器。试想一下:如果有一种四海之内,任何场景下都适用的完美垃圾收集器存在,那么JVM就不会实现那么多不同的垃圾收集器了。

1.1 Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

Serial收集器是最基本,历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程的收集器。它的“单线程”的意义不仅仅意味着它只会用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(Stop The World),直到它收集结束。

Serial收集器分为两个版本,Serial是年轻代版本,Serial Old是老年代版本。年轻代版本采用的是标记-复制算法,老年代版本采用的是标记-整理算法。

Serial收集器相比于其他垃圾收集器的优点:它简单而高效(与其他垃圾收集器的单线程相比)。Serial收集器由于没有线程之间交互的开销,自然可以获得很高的单线程收集效率。Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是最为CMS收集器的后备方案。

1.2 Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))

Parallel收集器可以看作是Serial收集器的多线程版本,它除了使用多线程进行垃圾收集外,其余行为(控制参数,收集算法,回收策略等)都和Serial收集器类似。

Parallel收集器默认的收集线程数和cpu的核数相同,当然也可以通过参数(- XX:ParallelGCThreads)制定收集线程数,但是一般不推荐修改。

Parallel收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去做也是一个不错的选择。

Parallel收集器也分为年轻代版本和老年代版本,年轻代采用标记-复制算法,老年代采用标记-整理算法。(JDK8默认的垃圾收集器即Parallel收集器)。

Parallel收集器关注点是吞吐量(高效率的使用cpu)。下面还会介绍的CMS和G1等收集器的关注点更多的是用户线程的停顿时间(Stop The World),提高用户的体验。所谓的吞吐量就是cpu用于运行用户代码的时间和cpu总消耗时间的比值。

1.3 ParNew收集器(-XX:+UseParNewGC)

ParNew收集器和Parallel收集器很类似,区别主要在于ParNew可以和CMS收集器配合使用,而Parallel收集器不可以和CMS收集器配合使用。

ParNew收集器仅用于年轻代,采用的是标记-复制算法。它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器,只有ParNew收集器可以和CMS(真正意义上的并发收集器)收集器配合使用。

1.4 CMS收集器(-XX:+UseConcMarkSweepGC(old))

CMS收集器(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机真正意义上第一款并发收集器,它第一次实现了让用户线程和垃圾收集线程(基本上)同时工作。

从CMS的名字中可以看出来,Concurrent Mark Sweep,并发,标记-清除。可以看到CMS收集器采用的是标记-清除算法,它的主要优点:并发收集,低停顿,大大减少了STW的时间。

但是减少了STW的背后,是由于CMS底层的实现更加的复杂,所以也导致了整个gc的过程可能会比Paraller更加耗时。(具体的一些缺点我们等下会再讲)

CMS运作过程分为四个阶段:

1.初始标记:暂停其他所有的线程(STW),并记录下gc roots直接能引用的对象,这一部分运行的速度很快。

2.并发标记:并发标记阶段就是从gc roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长(大概可能会占到整个gc的80%左右)但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。但是因为由于和用户线程一起同时进行,所以可能会导致已经标记过的对象的状态发生改变。

3.重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致对象状态被改变的那些标记记录。这个阶段也会STW,停顿时间会比初始标记阶段稍长,但是远远比并发标记阶段时间要短。

4.并发清理:开启用户线程,同时gc线程开始对未标记的区域做清扫。

至于最后的并发重置,就是将此次gc过程中的所有标记数据重置。

我们发现在并发标记恶化并发清理阶段,CMS没有STW,那么在这两个阶段是可能发生对象的状态改变的,那么CMS是如何处理的呢?我们继续往下面看。

我们先来分析一下,在并发阶段,对象的状态改变,可能会导致两种情况出现。

1.多标:试想一下这样的一种场景,在初始标记阶段有一个对象被gcroot引用被标记为非垃圾对象,到并发标记阶段由于方法执行结束了导致该对象被销毁了,那么此时这个垃圾对象是不会被清理的。这部分本应该回收但是没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只需要等到下一轮垃圾回收才被清除。(并发清理阶段也有可能出现浮动垃圾)

有人会问,不是还有一个重新标记的阶段吗?重新标记阶段CMS只会处理非可达-可达这一部分的对象。因为相比较之下,浮动垃圾是可以被容忍的,也许CMS内部机制认为由可达-不可达这样的变化需要重新从gcroot开始遍历扫描,相当于再完成一次初始标记和并发标记的工作。这样一来不仅前两个阶段变成了多余,浪费了资料开销,还会大大增加重新标记阶段的开销,同时也会大大增加STW的时间(因为重新标记阶段是STW的),这是CMS内部不被允许的。

2.漏标:漏标即非可达-可达,会导致被引用的对象被当作垃圾对象给清理掉,这样出现严重的问题,我们必须解决。有两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning, SATB)。

介绍这两种解决方案之前,我们先来看下CMS垃圾收集底层标记算法实现

三色标记

在并发标记期间,因为用户线程还在继续跑,对象间的引用有可能会发生变化,多标和漏标的情况就有可能发生。

这里我们引入“三色标记”来给大家解释下,把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色:

黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过

灰色对象) 指向某个白色对象。

灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。

白色: 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。

关于三色标记这一段,我想了很久感觉很难用文字去表述,仅做记录吧,也可以大家一起来讨论一下。

我们再回头来看漏标的两种解决方案:增量更新和原始快照

1.增量更新:增量更新就是当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色标记为根,重新扫描。这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了

2.原始快照:原始快照就是当灰色对象将要删除指向白色对象的引用关系时,将这个要删除的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的白色的对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾

以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。

现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同:比如白色/黑色集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可以是广度/深度遍历等等。

对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下:

CMS:写屏障 + 增量更新

G1,Shenandoah:写屏障 + SATB

ZGC:读屏障

工程实现中,读写屏障还有其他功能,比如写屏障可以用于记录跨代/区引用的变化,读屏障可以用于支持移动对象的并发执行等。功能之外,还有性能的考虑,所以对于选择哪种,每款垃圾回收器都有自己的想法。

为什么G1用SATB?CMS用增量更新?

我的理解:SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。

1.5 G1收集器(-XX:+UseG1GC)

G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。

前面的几个收集器都分为年轻代收集器和老年代收集器,在G1中从物理上来说,已经没有年轻代和老年代的区分了(但是在逻辑上还是有所区分)

G1将整个Java堆内存分为了多个大小相等的独立区域(Region),JVM最多可以有2048个Region。

一个Region的大小默认等于堆内存大小除以2048,G1提供了一个参数可以帮助我们手动指定Region的大小,“-XX:G1HeapRegionSize”,推荐还是用默认的计算方式。

G1中年轻代默认占比为5%,假设现在堆内存为4096M,那么一个Region的大小默认就等于2M,年轻代默认就占100个左右的Region,也可以通过参数“-XX:G1NewSizePercent”来设置年轻代初始占比。在系统的运行过程中,一个Region可能之前是年轻代,如果Region进行了垃圾回收,那么可能之后会变成老年代,也就是说Region的区域功能可能会动态的改变。JVM会不停的给年轻代增加更多的Region,但是最多年轻代Region的占比默认不会超过60%,当然也可以通过参数"-XX:G1MaxNewSizePercent"来进行调整。

年轻代中的Eden区和Survivor区占比同之前的收集器一样,默认还是8:1:1。比如现在有100个年轻代Region,Eden区占80个,From区和To区分别占10个。

G1收集器对于对象什么时候会转移到老年代同之前讲过的原则一样,唯一不同的是对大对象的处理。G1专门划分了一块叫做Humongous的区域用来对大对象做处理。在G1中,大对象的判定规则就是一个对象超过了一个Region大小的50%,就会被放入到Humongous区中。如果一个对象太大,超过了一个Region的大小,那么可以横跨多个连续的Humongous区来存放。

Humongous区是专门用来存放短期巨型对象,不用直接进入老年代,节省老年代的空间,可以避免因为老年代空间不够而导致的GC开销。当然G1进行Full GC的时候除了回收年轻代和老年代的对象之外,也会将Humongous区一并回收。但是这边有一个注意的点,G1的Full GC和之前的那些收集器的Full GC有一点不一样,这点在下面会介绍到。

G1收集器的一次GC回收大致可以分为以下几个步骤:

1.初始标记(initial mark):这一阶段会STW,会暂停其他所有的线程,并记录下gc root直接能引用的对象,速度很快。(这一步同CMS的初始标记很相似)

2.并发标记(Concurrent Marking):这一阶段同CMS的并发标记阶段。

3.最终标记(Remark):这一阶段同CMS的并发标记阶段,也会STW。

4.筛选回收(Cleanup):这一阶段也会STW,这里说明一下,G1可以通过设置参数“-XX:MaxGCPauseMillis”来设置我们所期望的GC回收停顿时间。在筛选回收阶段,G1会首先对各个Region的回收价值和成本进行排序,根据我们设置的参数来指定回收计划。比如老年代此时有1000个Region都满了,但是因为设置了回收停顿时间期望值,比如设置了本次回收只能停顿200毫秒,那么通过之前回收成本计算得知,假设可能回收其中的800个Region正好是200毫秒,那么此次回收就只会收回这800个Region,剩下的200个Region会在下一次的gc中进行回收。G1会尽量把gc停顿的时间(STW)控制在我们指定的时间之内。

筛选回收阶段是会进行STW的,其实这以阶段也可以做到和用户线程一起并行,但是因为G1内部会对各个Region的回收价值和成本进行排序,且只会收回其中的一部分区域,整个GC停顿的时间是用户可以控制的,所以在这一阶段STW将大幅度提高收集效率。而且G1内部还没有解决转移过程中准确定位对象地址的问题,这也导致了目前这一阶段是STW执行的。

G1回收主要用到了标记-复制算法,因为它内部将内存空间分为了一个个Region,那么回收的时候只需要将一个Region里面存活的对象复制到另外一个Region中,这样就不会像CMS那样回收完还需要将内存碎片再整理一次(CMS老年代是用的标记-清除算法,可以通过设置参数来进行一次碎片内存清理)。

之前有提过G1在筛选回收阶段会首先对各个Region的回收价值和成本进行排序,那么我们具体来看看它到底是怎么做的。

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花20ms就能回收10M的垃圾,在回收时间有限的情况下,G1当然会优先选择后面这个Region进行回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内尽可能高的收集效率。

我们接着来看一下G1收集器的几种GC

1.YoungGC:YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数“-XX:MaxGCPauseMillis”设置的值,那么此时G1会增加年轻代的Region,继续给新对象存放,不会马上做YoungGC,知道一下次Eden区放满了,G1计算Eden区回收时间接近参数“-XX:MaxGCPauseMillis”设置的值,才会触发YoungGC。

2.MixedGC:这个同CMS中FullGC很类似,但是不是G1内部定义的FullGC。当老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设置的值时触发,会回收所有的年轻代和部分的老年代和大Humongous区。注意,这边为什么是部分的老年代呢?因为G1内部会根据期望的GC停顿时间确定老年代垃圾收集的优先顺序回收部分老年代的Region。正常情况G1的垃圾收集是先做 MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够 的空region能够承载拷贝对象就会触发一次Full GC。

3.FullGC:停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的,而且整个过程都会STW。

 

说了这么多,我们来总结下G1收集器的特点(JDK8默认的是Parallel收集器,JDK9默认的是G1收集器):

1.并行与并发:G1能充分利用CPU,多核环境下的硬件优势,使用多个CPU或者说CPU核心来缩短STW的停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续。

2.分代收集:G1从物理上已经没有了分代的概念,它独立管理了整个堆区。但是从逻辑上看还是保留了分代的概念。

3.空间整合:与CMS的底层标记-清除算法不同,CMS回收Region是用的标记-复制算法。

4.可预测的停顿时间:这个是G1相对于CMS的另一个大优势。降低停顿时间是G1和CMS共同的关注点,但G1在最求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"- XX:MaxGCPauseMillis"指定)内完成垃圾收集,这是CMS收集器不具备的。

毫无疑问,可以由用户指定期望的停顿时间是G1收集器区别于其他收集器,很强大的一个功能。设置不同的期望停顿时间,可以使得G1在不同应用场景中取得关注吞吐量和关注停顿延迟之间的最佳平衡。不过,这里设置的“期望值”必须符合实际,不能异想天开,毕竟G1需要冻结用户线程来复制对象,这个时间再怎么低也得有个限度。G1默认的停顿时间是200毫秒,一般来说,回收阶段占到几十到一百甚至两百毫秒都很正常,但如果我们把这个时间调的非常低,比如二十毫秒,很可能出现的结果就是由于停顿时间太少,导致每次选出来的回收集只占堆内存中很小的一部分,收集器收集的速度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积。最终垃圾占满整个堆而导致FullGC,这样反而降低了性能,所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒是比较合理的。

 

C1收集器参数设置

-XX:+UseG1GC:使用G1收集器

-XX:ParallelGCThreads:指定GC工作的线程数量

-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区

-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)

-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)

-XX:G1MaxNewSizePercent:新生代内存最大空间

-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代

-XX:MaxTenuringThreshold:最大年龄阈值(默认15)

-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了

 

-XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。

-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。

-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。

G1收集器优化建议:

假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%,此时才触发年轻代的YoungGC,那么存活下来的对象就会比较多,此时就可能导致Survivor区放不下那么多对象,就会直接进入老年代,或者是年轻代YoungGC后,存活下来的对象比较多,导致进入了Survivor区后触发了动态年龄规则判断,达到了Survivor区50%的内存,也会导致一些对象快速的进入老年代。所以这里核心还是在调节-XX:MaxGCPauseMills这个参数的值,在保证它的年轻代YoungGC别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发MixedGC。

 

什么场景适合使用G1?

1.50%以上的堆被存活对象占用

2.对象分配和晋升的速度变化非常大

3.垃圾回收时间特别长,超过1秒

4.8GB以上的堆内存(G1底层Region用的复制算法,如果是小内存可能没有那么大空间,肯能会导致G1的FullGC,性能反而比较低)

5.停顿时间是500毫秒以内

 

每秒几十万并发的系统如何优化JVM?

Kafka类似的支撑高并发消息系统大家肯定不陌生,对于kafka来说,每秒处理几万甚至几十万消息时很正常的,一般 来说部署kafka需要用大内存机器(比如64G),也就是说可以给年轻代分配个三四十G的内存用来支撑高并发处理,这里就 涉及到一个问题了,我们以前常说的对于eden区的young gc是很快的,这种情况下它的执行还会很快吗?很显然,不可 能,因为内存太大,处理还是要花不少时间的,假设三四十G内存回收可能最快也要几秒钟,按kafka这个并发量放满三 四十G的eden区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会因为young gc卡顿几秒钟没法处理新消 息,显然是不行的。那么对于这种情况如何优化了,我们可以使用G1收集器,设置 -XX:MaxGCPauseMills 为50ms,假 设50ms能够回收三到四个G内存,然后50ms的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几乎无感知的情况下一边处理业务一边收集垃圾。

G1天生就适合这种大内存机器的JVM运行,可以比较完美的解决大内存垃圾回收时间过长的问题。

还是借鉴了很多别人的知识点,欢迎大家一起来讨论!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值