目录

一:jvm运行时数据区的划分

二:堆区的划分(where)

三:什么情况下发生GC(when)

四:哪些内存需要回收(垃圾对象who)

五:GC如何回收(how)

六:jvm的内存回收过程

七:垃圾收集器

八:GC执行机制

九:内存调优

十:JVM常见配置选项


一:jvm运行时数据区的划分

        (1)程序计数器(Program Conuter Register)

程序计数器是一块较小的内存空间,它是当前线程执行字节码的行号指示器,字节码解释工作器就是通过改变这个计数器的值来选取下一条需要执行的指令。它是线程私有的内存,也是唯一一个没有OOM异常的区域。

        (2)Java虚拟机栈区(Java Virtual Machine Stacks)

栈区是Java方法执行的内存模型,每个方法被执行的时候都创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法被调用到完成,相当于一个栈帧在虚拟机栈中从入栈到出栈的过程。此区域也是线程私有的内存,可能抛出两种异常:如果线程请求的栈深度大于虚拟机允许的深度将抛出StackOverflowError;如果虚拟机栈可以动态的扩展,扩展到无法动态的申请到足够的内存时会抛出OOM异常。

        (3)本地方法栈(Native Method Stacks)

本地方法栈与虚拟机栈发挥的作用相似,区别就是虚拟机栈为虚拟机执行Java方法,本地方法栈则是为虚拟机使用到的Native方法服务。

        (4)堆区(Heap)

所有对象实例和数组都在堆区上分配,堆区是GC主要管理的区域。堆区还可以细分为新生代、老年代,新生代还分为一个Eden区和两个Survivor区。此块内存为所有线程共享区域,当堆中没有足够内存完成实例分配时会抛出OOM异常。

        (5)方法区(Method Area)

方法区也是所有线程共享区,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。GC在这个区域很少出现,这个区域内存回收的目标主要是对常量池的回收和类型的卸载,回收的内存比较少,所以也有称这个区域为永久代(Permanent Generation)的。当方法区无法满足内存分配时抛出OOM异常。

        (6)运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

二:堆区的划分(where)

堆内存划分为 Eden、Survivor 和 Tenured/Old 空间,如下图所示:

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生代,即方法区的回收(JDK8中无永生代),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。Major GC的速度一般会比Minor GC慢10倍以上。

三:什么情况下发生GC(when)

(1)Minor GC触发的条件

1)新生代中Eden空间不足,对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快;

2)发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则进行一次Full GC,如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC。

(2)Full GC触发的条件

1)System.gc()方法的调用,此方法是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。一般情况下不使用此方法,让虚拟机自己去管理它的内存,可通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc()。

2) 老年代的内存空间不足,发生Full GC一般都会有一次Minor GC。大对象直接进入老年代,如很长的字符串数组,虚拟机提供一个-XX:PretenureSizeThreadhold参数,令大于这个参数值的对象直接在老年代中分配,避免在Eden区和两个Survivor区发生大量的内存拷贝;

3)方法区内存空间不足,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

4)当Minor GC时,老年代的剩余空间小于历次从新生代往老年代中移的对象的平均内存空间大小时,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
      例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
      当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
     对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可在启动时通过- java -

Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

5)堆中分配很大的对象,大对象是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。

四:哪些内存需要回收(垃圾对象who)

(1)引用计数法,每个对象创建的时候,会分配一个引用计数器,当这个对象被引用的时候计数器就加1,当不被引用或者引用失效的时候计数器就会减1。任何时候,对象的引用计数器值为0就说明这个对象不被使用了,就认为是“垃圾”,可以被GC处理掉。

优点:算法实现简单。

缺点:不能解决对象之间循环引用的问题。有垃圾对象不能被正确识别,这对垃圾回收来说是很致命的,所以GC并没有使用这种搜索算法。

(2)根搜索算法,以一些特定的对象作为基础原始对象,或者称作“根”,不断往下搜索,到达某一个对象的路径称为引用链。如果一个对象和根对象之间有引用链,即根对象到这个对象是可到达的,则这个对象是活着的,不是垃圾,还不能回收。例如,假设有根对象O,O引用了A对象,同时A对象引用了B对象,B对象又引用了C对象,那么对象C和根对象O之间的路径的可达的,C对象就不能当做垃圾对象。引用链为O->A->B->C。反之,如果一个对象和根对象之间没有引用链,根对象到这个对象的路径是不可达的,那么这个对象就是可回收的垃圾对象。

从gc root开始搜索找不到的对象,而且经过一次标记、清理,仍然没有复活的对象,当一个对象到GC Roots没有任何引用相连接,用图论的来说就是从GC Roots到这个对象不可达,则证明此对象是不可用的,说明此对象可以被GC。对于这些不可达对象,也不是一下子就被GC,而是至少要经历两次标记过程:如果对象在进行根搜索算法(GC Root Tracing)后发现没有与GC Roots相连接的引用链,那它将会第一次标记并且进行一次筛选,筛选条件是此对象有没有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用执行过一次,这两种情况都被视为没有必要执行finalize()方法,对于没有必要执行finalize()方法的将会被GC,对于有必要有必要执行的,对象在finalize()方法中可能会自救,也就是重新与引用链上的任何一个对象建立关联即可。

优点:可找到所以得垃圾对象,并且完美解决对象之间循环引用的问题。

缺点:不可避免地要遍历全局所有对象,导致搜索效率不高。

根搜索算法是现在GC使用的搜索算法。

可以当做GC roots的对象有以下几种:

1)虚拟机栈中的引用的对象。(java栈的栈帧本地变量表)

2)方法区中的类静态属性引用的对象。

3)方法区中的常量引用的对象。(声明为final的常量对象)

4)本地方法栈中JNI的引用的对象。(本地方法栈的栈帧本地变量表)

下图可以看到:GC ROOTS就是根对象节点,蓝色的是可达的引用链,引用链上的对象是活着的,不能被当做垃圾对象回收。相反暗灰色的路径表示不可达的路径,这些对象将会被回收。每个圈圈里面的数字,表示其被引用的次数,就是上面说到的引用计数法的计数值。

当对象被判定为不可达对象后,是不是就一定非清除不可呢

答案:不是,不可达对象可以通过覆写finalize()方法,来自救。

判定一个类是否可以被移除,需要满足下面几个条件:

  • 该类所有的实例都已被回收

  • 加载该类的ClassLoader已被回收

  • 该类的Class对象没有被任何地方引用,无法在任何地方通过反射访问该类的方法。

满足上述条件,表明这个类可以被回收,而不是必然会被回收。是否回收,Hotspot使用-Xnoclassgc控制。

五:GC如何回收(how)

选择不同的垃圾收集器,所使用的收集算法也不同。

在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,则使用复制算法,新生代内存被分为一个较大的Eden区和两个较小的Survivor区,每次只使用Eden区和一个Survivor区,当回收时将Eden区和Survivor还存活着的对象一次性的拷贝到另一个Survivor区上,最后清理掉Eden区和刚才使用过的Survivor区,Eden和Survivor的默认比例是8:1,可以使用-XX:SurvivorRatio来设置该比例。

而老年代中对象存活率高,没有额外的空间对它进行分配担保,必须使用“标记-清理”或“标记-整理”算法。

(1)标记-清除算法(Mark-Sweep)

该算法分两步执行:

1) 标记Mark:从GC ROOTS开始,遍历堆内存区域的所有根对象,对在引用链上的对象都进行标记。这样下来,如果是存活的对象就会被做了标记,反之如果是垃圾对象,则没做有标记。GC很容易根据有没有被做标记就完成了垃圾对象回收。

2) 清除Sweep:遍历堆中的所有的对象(标记阶段遍历的是所有根节点),找到未被标记的对象,直接回收所占的内存,释放空间。

优点:没有产生额外的内存空间消耗,内存利用率高。

缺点:体现在效率和空间,从效率的角度讲,标记和清除两个过程的效率都不高;从空间的角度讲,标记清除后会产生大量不连续的内存碎片, 内存碎片太多可能会导致以后程序运行过程中在需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。

标记-清除算法操作的对象是【垃圾对象】,对于活着的对象(被标记的对象),它则直接不理睬。

标记-清除算法执行过程如图:

(2)复制算法(Copying)

复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。这样每次只需要对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等复杂情况,只需要移动指针,按照顺序分配即可。

优点:GC后的内存齐整,不产生内存碎片。

缺点:内存缩小为了原来的一半,GC要使用两倍的内存,或者说导致堆只能使用被分配到的内存的一半,这个算法对空间要求太高!如果存活的对象较多,则意味着要复制很多对象并且要维护大量对象的内存地址,所以存活的对象数量不能太多,否则效率也会很低。

现在的商用虚拟机都采用这种算法来回收新生代,不过研究表明1:1的比例非常不科学,因此新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)。

复制算法复制移动的对象是【活着的对象】,对于垃圾对象(不被标记的对象)则直接回收。

复制算法的执行过程如图:

(3)标记-整理算法(Mark-Compact)

这个算法则是对上面两个算法的综合结果。也分为两个阶段:

1)标记:这个阶段和标记-清除Mark-Sweep算法一样,遍历GC ROOTS并标记存活的对象。

2)整理:移动所有活着的对象到内存区域的一侧(具体在哪一侧则由GC实现),严格按照内存地址次序依次排列活着的对象,然后将最后一个活着的对象地址以后的空间全部回收。

优点:内存空间利用率高,消除了复制算法内存减半的情况;GC后不会产生内存碎片。

缺点:需要遍历标记活着的对象,效率较低;复制移动对象后,还要维护这些活着对象的引用地址列表。

复制算法在对象存活率较高的场景下要进行大量的复制操作,效率很低。万一对象100%存活,那么需要有额外的空间进行分配担保。老年代都是不易被回收的对象,对象存活率高,因此一般不能直接选用复制算法。根据老年代的特点,有人提出了另外一种标记-整理算法,过程与标记-清除算法一样,不过不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。

标记-整理算法的工作过程如图:

(4)分代回收算法(Generational Collecting)

分代回收算法就是现在JVM使用的GC回收算法。

现代商用虚拟机基本都采用分代收集算法来进行垃圾回收,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的,使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的,采用标记-清理算法或者标记-整理算法。

1)简要说明

1】先来看看简单化后的堆的内存结构:

Java堆 = 年老代 + 年轻代
(空间大小比例一般是3:1)年轻代 = Eden区 + From Space区 + To Space区
(空间大小比例一般是8:1:1)

2】按照对象存活时间长短,我们可以把对象简单分为三类:

  • 短命对象:存活时间较短的对象,如中间变量对象、临时对象、循环体创建的对象等。这也是产生最多数量的对象,GC回收的关注重点。

  • 长命对象:存活时间较长的对象,如单例模式产生的单例对象、数据库连接对象、缓存对象等。

  • 长生对象:一旦创建则一直存活,几乎不死的对象。

3】对象分配区域
短命对象存在于年轻代,长命对象存在于年老代,而长生对象则存在于方法区中。
由于GC的主要内存区域是堆,所以GC的对象主要就是短命对象和长命对象这类寿命“有限”的对象。

2)分代回收的GC类型

针对HotSpot VM的的GC其实准确分类只有两大种:

1】Partial GC:部分回收模式

  • Young GC:只收集young gen的GC。和Minor GC一样。
  • Old GC:只收集old gen的GC。只有CMS的concurrent - collection是这个模式
  • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式

2】Full GC:收集整个堆,包括young gen、old gen,还有永久代perm gen(如果存在的话)等所有部分的模式。同Major GC。

3】触发时机
HotSpot VM的串行GC的触发条件是:
young GC:当young gen中的eden区分配满的时候触发。

full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC;或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。

并发GC的触发条件就不太一样。以CMS GC为例,它主要是定时去检查old gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对old gen做并发收集。

3)年轻代GC过程

当需要在堆中创建一个新的对象,而年轻代内存不足时触发一次GC,在年轻代触发的GC称为普通GC,Minor GC。注意到年轻代中的对象都是存活时间较短的对象,所以适合使用复制算法。这里肯定不会使用两倍的内存来实现复制算法了,牛人们是这样解决的,把年轻代内存组成是80%的Eden、10%的From Space和10%的To Space,然后在这些内存区域直接进行复制。

刚开始创建的对象是在Eden中,此时Eden中有对象,而两个survivor区没有对象,都是空闲区间。第一次Minor GC后,存活的对象被放到其中一个survivor,Eden中的内存空间直接被回收。在下一次GC到来时,Eden和一个survivor中又创建满了对象,这个时候GC清除的就是Eden和这个放满对象的survivor组成的大区域(占90%),Minor GC使用复制算法把活的对象复制到另一个空闲的survivor区间,然后直接回收之前90%的内存。周而复始。始终会有一个10%空闲的survivor区间,作为下一次Minor GC存放对象的准备空间。

要完成上面的算法,每次Minor GC过程都要满足:
存活的对象大小都不能超过survivor那10%的内存空间,不然就没有空间复制剩下的对象了。但是,万一超过了呢?前面我们提到过年老代,对,就是把这些大对象放到年老代。

4)年老代GC

什么样的对象可以进入年老代呢?如下:

  • 在年轻代中,如果一个对象的年龄(GC一次后还存活的对象年岁加1)达到一个阈值(可以配置),就会被移动到年老代。
  • Survivor中相同年龄的对象大小总和超过survivor空间的一半,则不小于这个年龄的对象都会直接进入年老代。
  • 创建的对象的大小超过设定阈值,这个对象会被直接存进年老代。
  • 年轻代中大于survivor空间的对象,Minor GC时会被移进年老代。

年老代中的对象特点就是存活时间较长,而且没有备用的空闲空间,所以显然不适合使用复制算法了,这个时候使用标记-清除算法或者标记-整理算法来实现GC。负责年老代中GC操作的是全局GC,Major GC,Full GC。

什么时候触发Major GC呢?
       在Minor GC时,先检测JVM的统计数据,查看历史上进入老年代的对象平均大小是否大于目前年老代中的剩余空间,如果大于则触发Full GC。

六:jvm的内存回收过程

对象在Eden Space创建,当Eden Space满了的时候,gc就把所有在Eden Space中的对象扫描一次,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象所占用的空间释放。当Eden Space再次变满了的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中的有效对象复制到第二个Survivor Space。如果填充到第二个Survivor Space中的有效对象被第一个Survivor Space或Eden Space中的对象引用,那么这些对象就是长期存在的,此时这些对象将被复制到Old Generation。

若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC,此时jvm gc停止所有在堆中运行的线程并执行清除动作。

七:垃圾收集器

垃圾收集器就是上面讲的理论知识的具体实现了。不同虚拟机所提供的垃圾收集器可能会有很大差别,我们使用的是HotSpot,HotSpot这个虚拟机所包含的所有收集器如图:

上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,那说明它们可以搭配使用。虚拟机所处的区域说明它是属于新生代收集器还是老年代收集器。多说一句,我们必须要明白一个道理:没有最好的垃圾收集器,更加没有万能的收集器,只能选择对具体应用最合适的收集器。这也是HotSpot为什么要实现这么多收集器的原因。

(1)Serial收集器

最基本、发展历史最久的收集器,这个收集器是一个采用复制算法的单线程的收集器,单线程一方面意味着它只会使用一个CPU或一条线程去完成垃圾收集工作,另一方面也意味着它进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止。后者意味着,在用户不可见的情况下要把用户正常工作的线程全部停掉,这对很多应用是难以接受的。不过实际上到目前为止,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器,因为它简单而高效。用户桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代停顿时间在几十毫秒最多一百毫秒,只要不是频繁发生,这点停顿是完全可以接受的。

(2)ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余行为和Serial收集器完全一样,包括使用的也是复制算法。ParNew收集器除了多线程以外和Serial收集器并没有太多创新的地方,但是它却是Server模式下的虚拟机首选的新生代收集器,其中有一个很重要的和性能无关的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作(看图)。CMS收集器是一款几乎可以认为有划时代意义的垃圾收集器,因为它第一次实现了让垃圾收集线程与用户线程基本上同时工作。ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于线程交互的开销,该收集器在两个CPU的环境中都不能百分之百保证可以超越Serial收集器。当然,随着可用CPU数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU数量相同,在CPU数量非常多的情况下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

(3)Parallel收集器

Parallel收集器也是一个新生代收集器,也是用复制算法的收集器,也是并行的多线程收集器,但是它的特点是它的关注点和其他收集器不同。介绍这个收集器主要还是介绍吞吐量的概念。CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel收集器的目标则是打到一个可控制的吞吐量。所谓吞吐量的意思就是CPU用于运行用户代码时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总运行100分钟,垃圾收集1分钟,那吞吐量就是99%。另外,Parallel收集器是虚拟机运行在Server模式下的默认垃圾收集器。

停顿时间短适合需要与用户交互的程序,良好的响应速度能提升用户体验;高吞吐量则可以高效率利用CPU时间,尽快完成运算任务,主要适合在后台运算而不需要太多交互的任务。

虚拟机提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio两个参数来精确控制最大垃圾收集停顿时间和吞吐量大小。不过不要以为前者越小越好,GC停顿时间的缩短是以牺牲吞吐量和新生代空间换取的。由于与吞吐量关系密切,Parallel收集器也被称为“吞吐量优先收集器”。Parallel收集器有一个-XX:+UseAdaptiveSizePolicy参数,这是一个开关参数,这个参数打开之后,就不需要手动指定新生代大小、Eden区和Survivor参数等细节参数了,虚拟机会根据当亲系统的运行情况手机性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。如果对于垃圾收集器运作原理不太了解,以至于在优化比较困难的时候,使用Parallel收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个不错的选择。

(4)Serial Old收集器

Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理算法”,这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

(5)Parallel Old收集器

Parallel收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在JDK 1.6之后的出现,“吞吐量优先收集器”终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel收集器+Parallel Old收集器的组合。

(6)CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的老年代收集器。目前很大一部分Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其注重服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,CMS收集器就非常符合这类应用的需求。CMS收集器从名字就能看出是基于“标记-清除”算法实现的。

(7)G1收集器

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,JDK 7 Update 4后开始进入商用。在G1收集器之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1收集器不再是这样,使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region的集合。G1收集器跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也是Garbage-First名称的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

垃圾收集器总结

来看一下对垃圾收集器的总结,列了一张表

GC组合

Minor GC

Full GC

描述
-XX:+UseSerialGC Serial收集器串行回收 Serial Old收集器串行回收 该选项可以手动指定Serial收集器+Serial Old收集器组合执行内存回收
-XX:+UseParNewGC ParNew收集器并行回收 Serial Old收集器串行回收 该选项可以手动指定ParNew收集器+Serilal Old组合执行内存回收
-XX:+UseParallelGC Parallel收集器并行回收 Serial Old收集器串行回收 该选项可以手动指定Parallel收集器+Serial Old收集器组合执行内存回收
-XX:+UseParallelOldGC Parallel收集器并行回收 Parallel Old收集器并行回收 该选项可以手动指定Parallel收集器+Parallel Old收集器组合执行内存回收
-XX:+UseConcMarkSweepGC ParNew收集器并行回收  缺省使用CMS收集器并发回收,备用采用Serial Old收集器串行回收
 
该选项可以手动指定ParNew收集器+CMS收集器+Serial Old收集器组合执行内存回收。优先使用ParNew收集器+CMS收集器的组合,当出现ConcurrentMode Fail或者Promotion Failed时,则采用ParNew收集器+Serial Old收集器的组合

-XX:+UseConcMarkSweepGC

-XX:-UseParNewGC

Serial收集器串行回收
-XX:+UseG1GC G1收集器并发、并行执行内存回收 暂无

GC日志

每种收集器的日志形式都是由它们自身的实现所决定的,换言之,每种收集器的日志格式都可以不一样。不过虚拟机为了方便用户阅读,将各个收集器的日志都维持了一定的共性,就以最前面的对象间相互引用的那个类ReferenceCountingGC的代码为例:

虚拟机参数为“-XX:+PrintGCDetails -XX:+UseSerialGC”,使用Serial+Serial Old组合进行垃圾回收的日志

[GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
[GC [DefNew: 2242K->0K(2368K), 0.0018814 secs] 2242K->2241K(7680K), 0.0019172 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System) [Tenured: 2241K->193K(5312K), 0.0056517 secs] 4289K->193K(7680K), [Perm : 2950K->2950K(21248K)], 0.0057094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heapdef new generation   total 2432K, used 43K [0x00000000052a0000, 0x0000000005540000, 0x0000000006ea0000)eden space 2176K,   2% used [0x00000000052a0000, 0x00000000052aaeb8, 0x00000000054c0000)from space 256K,   0% used [0x00000000054c0000, 0x00000000054c0000, 0x0000000005500000)to   space 256K,   0% used [0x0000000005500000, 0x0000000005500000, 0x0000000005540000)tenured generation   total 5312K, used 193K [0x0000000006ea0000, 0x00000000073d0000, 0x000000000a6a0000)the space 5312K,   3% used [0x0000000006ea0000, 0x0000000006ed0730, 0x0000000006ed0800, 0x00000000073d0000)compacting perm gen  total 21248K, used 2982K [0x000000000a6a0000, 0x000000000bb60000, 0x000000000faa0000)the space 21248K,  14% used [0x000000000a6a0000, 0x000000000a989980, 0x000000000a989a00, 0x000000000bb60000)
No shared spaces configured.

虚拟机参数为“-XX:+PrintGCDetails -XX:+UseParNewGC”,使用ParNew+Serial Old组合进行垃圾回收的日志

[GC [ParNew: 310K->205K(2368K), 0.0006664 secs] 310K->205K(7680K), 0.0007043 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [ParNew: 2253K->31K(2368K), 0.0032525 secs] 2253K->2295K(7680K), 0.0032911 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System) [Tenured: 2264K->194K(5312K), 0.0054415 secs] 4343K->194K(7680K), [Perm : 2950K->2950K(21248K)], 0.0055105 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heappar new generation   total 2432K, used 43K [0x0000000005550000, 0x00000000057f0000, 0x0000000007150000)eden space 2176K,   2% used [0x0000000005550000, 0x000000000555aeb8, 0x0000000005770000)from space 256K,   0% used [0x0000000005770000, 0x0000000005770000, 0x00000000057b0000)to   space 256K,   0% used [0x00000000057b0000, 0x00000000057b0000, 0x00000000057f0000)tenured generation   total 5312K, used 194K [0x0000000007150000, 0x0000000007680000, 0x000000000a950000)the space 5312K,   3% used [0x0000000007150000, 0x0000000007180940, 0x0000000007180a00, 0x0000000007680000)compacting perm gen  total 21248K, used 2982K [0x000000000a950000, 0x000000000be10000, 0x000000000fd50000)the space 21248K,  14% used [0x000000000a950000, 0x000000000ac39980, 0x000000000ac39a00, 0x000000000be10000)
No shared spaces configured.

虚拟机参数为“-XX:+PrintGCDetails -XX:+UseParallelGC”,使用Parallel+Serial Old组合进行垃圾回收的日志

[GC [PSYoungGen: 4417K->288K(18688K)] 4417K->288K(61440K), 0.0007910 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System) [PSYoungGen: 288K->0K(18688K)] [PSOldGen: 0K->194K(42752K)] 288K->194K(61440K) [PSPermGen: 2941K->2941K(21248K)], 0.0032663 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
HeapPSYoungGen      total 18688K, used 321K [0x0000000034190000, 0x0000000035660000, 0x0000000048f90000)eden space 16064K, 2% used [0x0000000034190000,0x00000000341e05c0,0x0000000035140000)from space 2624K, 0% used [0x0000000035140000,0x0000000035140000,0x00000000353d0000)to   space 2624K, 0% used [0x00000000353d0000,0x00000000353d0000,0x0000000035660000)PSOldGen        total 42752K, used 194K [0x000000000a590000, 0x000000000cf50000, 0x0000000034190000)object space 42752K, 0% used [0x000000000a590000,0x000000000a5c0810,0x000000000cf50000)PSPermGen       total 21248K, used 2982K [0x0000000005190000, 0x0000000006650000, 0x000000000a590000)object space 21248K, 14% used [0x0000000005190000,0x0000000005479980,0x0000000006650000)

虚拟机参数为“-XX:+PrintGCDetails -XX:+UseConcMarkSweepGC”,使用ParNew+CMS+Serial Old组合进行垃圾回收的日志

[Full GC (System) [CMS: 0K->194K(62656K), 0.0080796 secs] 4436K->194K(81792K), [CMS Perm : 2941K->2940K(21248K)], 0.0081589 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Heappar new generation   total 19136K, used 340K [0x0000000005540000, 0x0000000006a00000, 0x0000000006a00000)eden space 17024K,   2% used [0x0000000005540000, 0x0000000005595290, 0x00000000065e0000)from space 2112K,   0% used [0x00000000065e0000, 0x00000000065e0000, 0x00000000067f0000)to   space 2112K,   0% used [0x00000000067f0000, 0x00000000067f0000, 0x0000000006a00000)concurrent mark-sweep generation total 62656K, used 194K [0x0000000006a00000, 0x000000000a730000, 0x000000000a940000)concurrent-mark-sweep perm gen total 21248K, used 2981K [0x000000000a940000, 0x000000000be00000, 0x000000000fd40000)

这四段GC日志中提炼出一些共性:

1、日志的开头“GC”、“Full GC”表示这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有Full,则说明本次GC停止了其他所有工作线程。看到Full GC的写法是“Full GC(System)”,这说明是调用System.gc()方法所触发的GC。

2、“GC”中接下来的“DefNew”、“ParNew”、“PSYoungGen”、“CMS”表示的是老年代垃圾收集器的名称,“PSYoungGen”中的“PS”指的是“Parallel Scavenge”,它是Parallel收集器的全称。

3、以第一个为例,方括号内部的“320K->194K(2368K)”、“2242K->0K(2368K)”,指的是该区域已使用的容量->GC后该内存区域已使用的容量(该内存区总容量)。方括号外面的“310K->194K(7680K)”、“2242K->2241K(7680K)”则指的是GC前Java堆已使用的容量->GC后Java堆已使用的容量(Java堆总容量)。

4、还以第一个为例,再往后“0.0269163 secs”表示该内存区域GC所占用的时间,单位是秒。最后的“[Times: user=0.00 sys=0.00 real=0.03 secs]”则更具体了,user表示用户态消耗的CPU时间、内核态消耗的CPU时间、操作从开始到结束经过的钟墙时间。后面两个的区别是,钟墙时间包括各种非运算的等待消耗,比如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间所以如果user或sys超过real是完全正常的。

5、“Heap”后面就列举出堆内存目前各个年代的区域的内存情况

八:GC执行机制

(1)串行GC

在搜索扫描和复制过程都是采用单线程实现,适用于单CPU、新生代空间较小或者要求GC暂停时间要求不高的地方。是client级别的默认方式。

(2)并行GC

在搜索扫描和复制过程都是采用多线程实现,适用于多CPU、或者要求GC暂停时间要求高的地方。是server级别的默认方式。

(3)同步GC

同时允许多个GC任务,减少GC暂停时间。主要应用在实时性要求重于总体吞吐量要求的中大型应用,即使如此,降低中断时间的技术还是会导致应用程序性能的降低。

九:内存调优

JVM内存调优,主要是减少GC的频率和减少Full GC的次数,Full GC的时候会极大地影响系统的性能。所以在此基础上,更加要关注会导致Full GC的情况。

(1)容易导致Full GC的情况

  • 年老代空间不足
    1)分配足够大空间给old gen。
    2)避免直接创建过大对象或者数组,否则会绕过年轻代直接进入年老代。
    3)应该使对象尽量在年轻代就被回收,或待得时间尽量久,避免过早的把对象移进年老代。

  • 方法区的永久代空间不足
    1)分配足够大空间给。
    2)避免创建过多的静态对象。

  • 被显示调用System.gc()
    通常情况下不要显示地触发GC,让JVM根据自己的机制实现。

(2)JVM堆内存分配问题讨论

1)年轻代过小(年老代过大)

  • 导致频繁发生GC,增大系统消耗
  • 容易让普通大文件直接进入年老代,从而更容易诱发Full GC。

2)年轻代过大(年老大过小)

  • 导致年老代过小,从而更容易诱发Full GC。
  • GC耗时增加,降低GC的效率。

3) Eden过大(survivor过小)

Minor GC时容易让普通大文件直接绕过survivor进入年老代,从而更容易诱发Full GC。

4)Eden过小(survivor过大)

导致GC频率升高,影响系统性能。

(3)调优策略

  • 保证系统吞吐量优先
  • 减少GC暂停时间优先

十:JVM常见配置选项

堆设置-Xms:初始堆大小-Xmx:最大堆大小-XX:NewSize=n:设置年轻代大小-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5-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数。并行收集线程数。

1.什么情况下发生GC相关推荐

  1. java在gc正常工作的情况下_Java GC的工作原理

    一 ,GC的概要: JVM主要管理两种类型内存:堆和非堆,堆内存(Heap Memory)是在 Java 虚拟机启动时创建,非堆内存(Non-heap Memory)是在JVM堆之外的内存. 简单来说 ...

  2. redo synch writes在什么情况下发生

    在用户commit或rollback下(如果是异步commit或rollback则不会有这个事件) update scott.dept set deptno=deptno; commit; or ro ...

  3. 什么情况下会发生full Gc?如何排查频繁发生full Gc的原因?

    GC就是Java的垃圾回收机制,要了解什么情况下会发生GC(即GC得触发条件),我们需要先了解JVM的内存模型结构,之前一篇文章已经详细讲解了Jvm的内存模型结构,而通常来说,GC主要针对的是堆(ja ...

  4. java在gc正常工作的情况下_最新JVM面试题小结,程序猿直呼内行

    这篇文章主要介绍了JVM面试题小结(2020最新版),觉得挺不错的,现在分享给大家,也给大家做个参考. Java内存区域 说一下 JVM 的主要组成部分及其作用? JVM包含两个子系统和两个组件,两个 ...

  5. 什么情况下会发生Full GC?

    1.调用System.gc()时 当调用System.gc()时,是建议JVM进行Full GC,只是建议,不是一定会发生,但一般情况下,JVM也会进行Full GC,进行Full GC时会让用户线程 ...

  6. 什么情况下会调用到session_destroy()

    https://segmentfault.com/q/1010000000191102 首先 ... session_destory() 是一个函数 ... 这个函数在任何情况下都不会被 php 引擎 ...

  7. JVM内存模型、逃逸分析以及发生GC的时机

    文章目录 1. 整体内存模型图 ①:堆 ②:栈与栈帧 ③:方法区 ④:程序计数器 ⑤:本地方法栈 2. JVM参数设置 GC日志打印参数设置 3. JVM内部的对象创建流程(new 对象流程) 4. ...

  8. 没有发生GC也进入了安全点?这段关于安全点的JVM源码有点意思!

    文末 JVM 思维导图,有需要的自取 熟知并发编程的你认为下面这段代码的执行结果是怎么样的? 我如果说,执行流程是: t1 线程和 t2 线程一直执行 num 的累加操作 主线程睡眠 1 秒,1 秒之 ...

  9. 什么情况下JVM内存中的一个对象会被垃圾回收

    问题1:  哪些对象能回收,哪些对象不能被回收 1.引用计数算法 (ref count) 2.可达性算法  (root searching) JVM 使用可达性算法     判断哪些对象可以被回收.也 ...

最新文章

  1. JavaScript数据结构与算法——字典
  2. oracle10gr2 x86,Oracle10gR2 On RHEL5 x86安装技术文档(英文原版)
  3. C语言中extern关键字的使用
  4. 【django】创建django项目工程
  5. appium java 点击坐标_appium定位元素java篇【转】
  6. 手把手教你将H5游戏打包为手游
  7. Facebook 开启元宇宙模式!豪砸百亿是为什么?
  8. 可变大小、颜色边框、样式的UISwitch
  9. qt动画效果_Qt编写自定义控件44-天气仪表盘
  10. day10 java的this关键字
  11. selenium切换窗口
  12. servlet-mapping_浅谈servletmapping的机制(二)
  13. cadence菜鸟之旅
  14. HTTP请求的完全过程
  15. 黑群晖DSM7.1.0物理机安装教程
  16. 基于Multisim的波形发生器
  17. 【ACWing】1123. 铲雪车
  18. ceph 集群报 mds cluster is degraded 故障排查
  19. Python语音基础操作--4.3共振峰估计
  20. 【电影推荐】风吹麦浪

热门文章

  1. 读《Scratch 2.0 少儿游戏趣味编程》
  2. KONG 之 rate-limiting
  3. 【CSDN表情包大全】
  4. 【UOJ 454】打雪仗(通信题)(分块)
  5. 图像质量评价指标FID、LPIPS、NIQE及其代码
  6. SQL每日一题 牛客17 10月的新客户单价和获客成本
  7. 修复Windows 10远程桌面复制粘贴不了的5种方法
  8. 手机5g什么时候普及_5G到底什么时候普及,明年发布的手机都会支持5G?
  9. 从特斯拉人形机器人亮相看AI人工智能模型落地面临的两个难题
  10. Apache Curator操作zookeeper的API使用