垃圾收集器与内存分配策略

一.概述

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

  在Java内存运行时的区域各个部分中,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈的操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由JIT编译器进行一些优化,但大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分的内存的分配和回收都是动态的,垃圾收集器关注的是这部分的内存。

二.对象已死吗?

1.引用计数法

  算法描述:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器值为0的对象就是不可能再被使用的

缺点: 很难解决对象之间的相互循环引用的问题。

2.可达性分析法

  通过一系列的名为“GC ROOTS”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象时不可用的。
这里写图片描述

对象object5、object6和object7虽然互相关联,但是它们到GC Roots 是不可达的,所以它们被判定是可回收的

在Java语言中可作为GC Roots的对象包括下面几种:

  • 虚拟机(栈帧中的本地变量表)中的引用对象。
  • 方法区(存储已被虚拟机加载的类信息,常量、静态变量、即时编译器(JIT)编译后的代码等数据)中的类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般所说的Native方法)的引用的对象。

补充:Just In Time编译器

解释器是一条一条的解释执行源语言。比如php,postscritp,javascript就是典型的解释性语言。

编译器是把源代码整个编译成目标代码,执行时不再需要编译器,直接在支持目标代码的平台上运行,这样执行效率比解释执行快很多。比如C语言代码被编译成二进制代码(exe程序),在windows平台上执行。

  在主流商用JVM(HotSpot、J9)中,Java程序一开始是通过解释器(Interpreter)进行解释执行的。当JVM发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“热点代码(Hot Spot Code)”,然后JVM会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为:即时编译器(Just In Time Compiler,JIT)

  我们知道,对于 Java 代码,刚开始都是被编译器编译成字节码文件,然后字节码文件会被交由 JVM 解释执行,所以可以说 Java 本身是一种半编译半解释执行的语言。Hot Spot VM 采用了 JIT compile 技术,将运行频率很高的字节码直接编译为机器指令执行以提高性能(这部分机器指令的执行不需要JVM的支持),所以当字节码被 JIT 编译为机器码的时候,要说它是编译执行的也可以。也就是说,运行时,部分代码可能由 JIT 翻译为目标机器指令(以 method 为翻译单位,还会保存起来,第二次执行就不用翻译了)直接执行。

  如果这段代码本身在将来只会被执行一次,那么从本质上看,编译就是在浪费精力。因为将代码翻译成 java 字节码相对于编译这段代码并执行代码来说,要快很多。当 然,如果一段代码频繁的调用方法,或是一个循环,也就是这段代码被多次执行,那么编译就非常值得了。因此,编译器具有的这种权衡能力会首先执行解释后的代 码,然后再去分辨哪些方法会被频繁调用来保证其本身的编译。其实说简单点,就是 JIT 在起作用。

3.再谈引用

  在JDK1.2之前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

  在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种,这四种引用强度依次逐渐减弱。

  • 强引用就是程序代码中普遍存在的,类似“Object object = new Object()”这类的引用,只要强引用存在,垃圾收集器永远不会回收掉被引用的对象
  • 软引用用来描述一些还有用,但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中,并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来试下你软引用。
  • 弱引用也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
  • 虚引用也称为幽灵引用或者幻影应引用,它是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生成时间构成影响,也无法通过虚引用来取得一个实例对象。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚应引用。

补充:OutOfMemoryError异常
   OutOfMemoryError简称OOM。“Java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。要解决这个区域的异常,一般的手段是先通过内存映像工具(如Eclipse Memory Analyzer)对dunp出来的堆转储快照进行分析,重点是确定内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory OvereFlow)。

Memory Analyzer下载地址:http://ftp.jaist.ac.jp/pub/eclipse/mat/1.8/rcp/MemoryAnalyzer-1.8.0.20180604-win32.win32.x86_64.zip

   如果是内存泄漏,可进一步通过工具查看对象到GC ROOTS的引用链。于是就能找到泄漏对象时通过怎么样的路径与GC ROOTS相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GCRoots的引用链信息,就可以比较准确的定位出泄漏代码的位置。
   如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

1.内存泄漏memory leak :是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
2、内存溢出 out of memory :指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。

  • java -verbose:gc :在虚拟机发生内存回收时在输出设备显示信息,格式如下: [Full GC 268K->168K(1984K), 0.0187390 secs] 该参数用来监视虚拟机内存回收的情况。箭头前后的数据168K和97K分别表示垃圾收集GC前后所有存活对象使用的内存容量,说明有168K-97K=71K的对象容量被回收,括号内的数据1984K为堆内存的总容量,收集所需要的时间是0.0253873秒(这个时间在每次执行的时候会有所不同)
  • java -verbose:class 在程序运行的时候究竟会有多少类被加载呢,一个简单程序会加载上百个类的!你可以用verbose:class来监视,在命令行输入java -verbose:class XXX (XXX为程序名)你会在控制台看到加载的类的情况
  • java –verbose:jni : -verbose:jni输出native方法调用的相关情况,一般用于诊断jni调用错误信息
  • -XX:+/-HeapDumpOnOutOfMemoryError :可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。
  • -XX:+/-PrintGCDetails :打印GC的详细信息到控制台

A.虚拟机栈和本地方法栈溢出

   由于在HotSpot中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

  • 如果线程请求的最大深度大于虚拟机栈所允许的最大深度,将抛出StackOverFlowError异常。
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
    操作系统分配给每个进程的内存是由限制的,譬如32位的windows限制为2GB。虚拟机提供了参数来控制Java堆和方法区这两部分内存的最大值。剩余的内存为2GB(操作系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗的内存很小,可以忽略。如果虚拟机本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。每个线程分配的栈容量越大,可以建立的线程数量自然就越少,建立线程时就月容易把剩下的内存耗尽,就可能建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

B.运行时常量池溢出

   如果要向运行时常量池中添加额外内容,最简单的做法就是使用Stirng.intern()这个Native()方法。该方法的作用就是:如果池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的应用。由于常量池分配在方法区内,我们可以通过-XX : PermSize和-XX : MaxPermSize限制方法区的大小(这个参数在Java 8中没用了,不用配置了),从而间接限制其中常量池的容量。

C.方法区溢出

   方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,所以大量的类可能导致方法区溢出。

D.本机直接内存溢出

  DirectMemory容量通过-XX: MaxDirectMemorySize指定,如果不指定则默认为与Java堆的最大值(-Xmx指定)一样。 内存不足时会抛出OutofMemoryError或者OutOfMemoryError:Direct buffer memory

4.回收方法区

根据Java虚拟机规范的规定,方法区无法满足内存分配需求时,也会抛出OutOfMemoryError异常,虽然规范规定虚拟机可以不实现垃圾收集,因为和堆的垃圾回收效率相比,方法区的回收效率实在太低,但是此部分内存区域也是可以被回收的。

方法区的垃圾回收主要有两种,分别是对废弃常量的回收和对无用类的回收。

当一个常量对象不再任何地方被引用的时候,则被标记为废弃常量,这个常量可以被回收。

方法区中的类需要同时满足以下三个条件才能被标记为无用的类:

1.Java堆中不存在该类的任何实例对象;

2.加载该类的类加载器已经被回收;

3.该类对应的java.lang.Class对象不在任何地方被引用,且无法在任何地方通过反射访问该类的方法。

当满足上述三个条件的类才可以被回收,但是并不是一定会被回收,需要参数进行控制,例如HotSpot虚拟机提供了-Xnoclassgc参数进行控制是否回收。

三.垃圾收集算法

1. 标记-清除算法

两个阶段:首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足: 一个是效率问题,两个阶段的效率都不高,另一个时空间问题,标记清除后悔留下大量的不连续的内存碎片,空间碎片太多,可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2.复制算法

将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另一块内存中,然后再把已使用过的内存一起清空,这样使得每次都是对整个半区就行内存回收。内存分配时也不需要考虑内存碎片等复杂问题,只需要移动堆顶指针,按顺序分配内存即可,实现简单,云心高效。这种算法的代价是将内存缩小为原来的一半。

现在的 商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间。而是将内存分为一块较大的Eden空间和两块较小的Survivior空间,每次使用Eden和其中一块Survivior。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间
HotSpot虚拟机默认Eden和Survivor的大小是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%。只有10%的内存会被“浪费”(即10%的对象没有被清空)。如果另外一块survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

3.标记-整理算法

标记(同标记-清除)后让所有的存活对象都移向一端,然后直接清理掉端边界以外的内存。

4.分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并没有什么新的意思,只是根据对象存活周期的不同将内存划分为几块。一般是将Java堆分为新生代和老年代,这样根据各个年代的特点采用适当的收集算法。在新生代由于每次都有大批的对象死去,只有少量存活,那就选用复制算法。而老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理“算法进行回收。

四.HotSpot的算法实现

上面试从理论上介绍了对象存活判定算法和垃圾收集算法,而在Hotspot虚拟机上实现这些算法时,必须对算法的执行效率有严格的考量,才能保证虚拟机高效运行。

1.枚举根节点

从可达性分析中从GC Roots节点找引用链这个操作实例,可作为 GCRoots 的节点主要在全局性的引用(例如常量和静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法去就有数百兆,如果要逐个检查者里面的引用,那么必然会消耗很多时间。

另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行——这里“一致性”的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断发生变化的情况,该点不满足的话分析结果的准确性就无法保证。这点是导致GC进行时必须停顿所有Java执行线程(Sun称这件事为“Stop The World”)的其中一个重要原因,即使在号称(几乎)不会发生停顿的CMS收集器上,枚举根节点时也是必须要停顿的。

由于目前的主流Java虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在HotSpot 的实现中,是使用一组称为OopMap的数据结构来达到这个目的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。

2.安全点

在OopMap的协助下,HotSpot可以快速准确的完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用问题关系变化,或者说OopMap内容变化的指令非常多。如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本会变得很高。

实际上,HotSpot也确实没有为每条指令都生成OopMap,前面已经提到过,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有的地方都能停顿下来开始GC,只有在达到安全点时才能暂停。Safepoint的选定太少以至于让GC等待时间太长,也不能过于频繁以至于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint

对于Safepoint,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先试中断主动式中断,其中抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。

而主动式中断思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志位真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

3.安全区域

使用Safepoint似乎已经完美的解决了如何进入GC的问题,但实际情况却并不一定。Safepoint机制保证了程序在执行时,在不太长的时间内就会遇到可进入GC的Safepoint,但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况就需要安全区(Safe Region)域来解决。

安全区域是指一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,我们可以把Safe Region看着是被扩展了的Safepoint。

在线程执行到SafeRegion中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者时整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

五.垃圾收集器

这里讨论的收集器基于JDK1.7Update 14之后的HotSpot虚拟机(在这个版本中正式提供了商用的G1收集器,之前的G1仍处于试验状态),这个虚拟机包含的所有收集器如图:
这里写图片描述

上图展示了7种用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。

1.serial

单线程收集器,“单线程”的意义不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在进行垃圾收集时必须暂停所有的工作线程。Serial对于运行在client模式下的虚拟机来说是一个很好的选择。
这里写图片描述

serial垃圾收集器是虚拟机运行在client模式下的默认新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程的收集效率。在用户的桌面应用场景中应用。所以Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

补充JVM client模式和Server模式的区别

我们把jdk安装完成后,在命名行输入java -version

不仅可以看到jdk版本相关信息,还会看到类似与 Java HotSpot(TM) 64-Bit Server VM (build 25.31-b07, mixed mode) 这样的信息。

其中有个Server VM (build 25.31-b07, mixed mode)其实代表了JVM的Server模式了。

当然JVM还有一个Client模式。

JVM Server模式与client模式启动的差别?

最主要的差别在于:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升.原因是:

当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,,服务起来之后,性能更高.

所以通常用于做服务器的时候我们用服务端模式,如果你的电脑只是运行一下java程序,就客户端模式就可以了。当然这些都是我们做程序优化程序才需要这些东西的,普通人并不关注这些专业的东西了。其实服务器模式即使编译更彻底,然后垃圾回收优化更好,这当然吃的内存要多点相对于客户端模式。

怎么修改JVM的启动模式呢?

64位系统默认在 JAVA_HOME/jre/lib/amd64/jvm.cfg

32在目录JAVA_HOME/jre/lib/i386/jvm.cfg

我的配置是这样的,所以是已服务器模式启动的,当然,你想换成client模式的话,把两个对调一下就可以了。

#
-server KNOWN
-client IGNORE

参考: https://www.cnblogs.com/huzi007/p/6728328.html

2.ParNew 收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRation-XX:PretenureSizeThreshold( 令大于这个设置值的对象直接在老年代中分配)、 -XX:+HandlePromotionFailure(是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留)等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。
这里写图片描述

ParNew收集器除了多线程收集之外,其他与Serial收集器比并没有太多创新,但他却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器工作。

不幸的是,CMS作为老年代收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器的一个。ParNew收集器也是使用-XX:+UseConcSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。


注意 从ParNew 收集器开始,后面还会接触到几款并发和并行的收集器。在大家可能产生疑惑之前,有必要先解释两个名词:并发和并行,这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,可以解释如下:

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
  • 并发 (Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

3.Parallel Scavenge

Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)。虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRation参数。

4.Serial old

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

5.Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK1.6中才开始提供的。在吞吐量以及资源敏感的场合,都可以优先考虑Parallel ScavengeParallel Old收集器。Parallel Old收集器的工作过程如下图:
这里写图片描述

6.CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,以给用户带来较好的体验。CMG收集器就非常符合这类应用的需求。

从名字(包含“Mark Sweep”)就可以看出,CMS收集器是基于“标记——清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程为4个步骤,包括:

  • 初始标记 (CMS initial Mark)
  • 并发标记(CMS concurrent Mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS Concurrent Sweep)

    其中初始标记、重新标记这两个步骤仍需要“Stop The World”,初始标记仅仅只是标记一下GC Roots能直接关联的对象,速度很快,并发标记阶段就是GC Roots Tracing的过程,而重新标记则是为了修正并发标记期间用户程序继续运作而导致标记产生变动的那一部分兑现的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程收集器都可以与用户线程一起工作,所以,从总体上来说,CMS的内存回收过程是与用户线程一起并发执行的。
这里写图片描述

CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。但是CMS还远达不到完美的程度,缺点:

  • CMS收集器对CPU资源非常敏感。其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数量是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时,CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然下降了50%,其实也让人无法接受。为了应付这种情况,虚拟机提供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep / i-CMS)的CMS收集器变种,所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占资源的时间,这样整个垃圾收集的时间会更长,但对用户程序的影响就会显得少一些,也就是速度下降没那么明显。实践证明,增量时的CMS收集器效果很一般,在目前的版本中,i-CMS已经被声明为“deprecated”,即不在提倡用户使用。
  • CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时时程序运作使用。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获得更好的性能,在JDK1.6中,CMS收集器的启动阈值已经提升至92%,要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备方案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
  • 因为CMS是一款基于“标记——清除”算法实现的收集器,那就意味着收集结束时会由大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的)用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理工程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时多进行碎片整理)。

7.G1收集器

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果。

G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备以下特点。

  • 并行与并发:能充分利用多CPU来缩短Stop-The-World停顿的时间,原本需要停顿Java线程执行GC的动作,G1收集器可以通过并发的方式让Java程序继续执行。
  • 分代收集:G1可以不要其他收集器的配合就能独立管理整个GC堆,它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。G1从整体上看是基于“标记——整理”算法实现的收集器,从局部(两个Region)上来看是基于“复制”算法实现的。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:G1除了最求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java的垃圾收集器的特征了。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值较大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking)
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)
  • 筛选回收(Live Data Counting and Evacuation)

对CMS收集器运作过程熟悉的读者,一定已经发现G1的前几个步骤的运作过程和CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,从Sun公司透露出来的信息来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。通过图3-11可以比较清楚地看到G1收集器的运作步骤中并发和需要停顿的阶段。

这里写图片描述

六.阅读GC日志
这里写图片描述

  • 最前面的数字: “33.125, 100.667” 代表了GC发生的时间,是从虚拟机启动以来经过的秒数。
  • “GC”、”FULL GC”说明了这次垃圾收集停顿的类型,而不是用来区分新生代GC还是老年代GC,如果有“FULL” ,则说明这次GC是发生了stop the world的。
  • 接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的。例如Serial收集器新生代名为“Default New Generation”,所以显示的是“[DefNew”,如果是Parallel,新生代名称为为“[ParNew”。如果采用Parallel Scavenge,则新生代为“PSYangGen”,老年代和永久代同理,名称也是由收集器决定的。
  • 后面方括号内的“3324K->152K(3712K)” 含义是“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”,而在方括号之外的“3324K->152K(11904K)”表示”GC前Java堆已使用容量->GC后Java已使用容量(Java堆总容量)”。
  • 再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。有的收集器会给出更具体的时间数据,如“[Times :user=0.01 sys=0.00, real=0.02 secs]”。这里面的user、sys和real与Linux的命令所输出的时间含义一致,分别代表用户态消耗的CPU、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间。CPU时间与墙钟时间的区别是:墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核时,多线程操作会叠加这些CPU时间,所以读者会看到user或者sys时间会超过real时间是很正常的。

七.垃圾收集器参数总结
这里写图片描述
这里写图片描述

八.内存分配与回收策略

Java 技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。关于回收这一点,我们已经使用了大量的篇幅去介绍虚拟机中的垃圾收集器体系以及运作原理,现在来讨论一下给对象分配内存的事。

对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓存,将按线程优先在TLAB(Thread Local Allocation Buffer)上分配。少数情况下也可能直接分配在老年代中,分配的规则并不是百分百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

1.对象优先在Eden区分配

大多数情况下,对象在新生代Eden区分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC

虚拟机提供了-XX:+PrintGCDetails 这个收集器日志参数,在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前内存各区域分配情况,主要是打印到控制台。

在实际应用中,内存回收日志一般是打印到文件后通过日志分析工具进行分析。通过-Xloggc:D:/gc.log可指定将gc的信息输出到gc.log中。

以下代码清单尝试分配3个2MB大小和1个4MB大小的对象,在运行时通过-Xms20M-Xmx20M-Xmn10M(设置年轻代大小为10m,整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小)这3个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1。

执行testAllocation()中分配allocation4对象的语句时会发生一次Minor GC。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩余空间已不足以分配allocation4所需的4MB内存,因此发生Minor GC。GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制提前转移到老年代区。

这次GC结束后,4MB的allocation4 对象顺利分配在Eden中,因此程序执行完的结果是Eden占用4MB(被allocation4 占用),survivor空闲,老年代被占用6MB(被allocation1、allocation2、allocation3占用)。


注意:Minor GC和Full GC的不同:

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上

package com.allocation;

/**
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 * @author Administrator
 *
 */
public class TestAllocation {
    private static final int _1MB = 1024 * 1024;
    public static void testAllocation() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];
    }

    public static void main(String[] args) {
        testAllocation();
    }
}

这里写图片描述

常见配置总结:

1.堆设置
  -Xms:初始堆大小
  -Xmx:最大堆大小
  -XX:NewSize=n:设置年轻代初始值大小
  -XX:MaxnewSize:表示新生代可被分配的内存的最大上限;当然这个值应该小于-Xmx的值;
  -Xmn:至于这个参数则是对 -XX:newSize、-XX:MaxnewSize两个参数的同时配置,也就是说如果通过-Xmn来配置新生代的内存大小,那么-XX:newSize = -XX:MaxnewSize = -Xmn,虽然会很方便,但需要注意的是这个参数是在JDK1.4版本以后才使用的
  -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:设置持久代大小

2.收集器设置
  -XX:+UseSerialGC:设置串行收集器
  -XX:+UseParallelGC:设置并行收集器
  -XX:+UseParalledlOldGC:设置并行年老代收集器
  -XX:+UseConcMarkSweepGC:设置并发收集器

3.垃圾回收统计信息
  -XX:+PrintGC
  -XX:+PrintGCDetails
  -XX:+PrintGCTimeStamps
  -Xloggc:filename
  -verbose:gc :在虚拟机发生内存回收时在输出设备显示信息

4.并行收集器设置
  -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
  -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
  -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

5.并发收集器设置
  -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
  -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

2.大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(例子中的byte[]数组就是典型的大对象)。大对象堆虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。以下代码中testPretenureSizeThreshold()方法后,可以看到Eden空间几乎没有被使用,而老年代的10MB空间被使用了40%,也就是4MB的allocation对象直接就分配在老年代中,这是因为PretenureSizeThreshold被设置为3MB(就是3145728,这个参数不能像-Xmx之类的参数一样直接写3MB)。


PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般并不需要设置。如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的收集器组合。


package com.pretenure;

/**
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 * -XX:PretenureSizeThreshold=3145728 -XX:+UseSerialGC
 * @author Administrator
 *
 */
public class PretenureSizeThreshold {
    private static final int _1MB = 1024 * 1024;
    public static void testPretenureSizeThreshold() {
        byte[] allocation;
        allocation = new byte[4 * _1MB];
    }

    public static void main(String[] args) {
        testPretenureSizeThreshold();
    }
}

运行结果 :

Heap
def new generation total 9216K, used 1312K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 16% used [0x00000000fec00000, 0x00000000fed48198, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
Metaspace used 2665K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K

3.长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代。为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

以下代码清单中,在allocation3分配内存时,发生第一次Minor GC,allocation1对象需要256KB内存,Survivor空间可以容纳,allocation1的年龄为1。allocation2对象需要4MB内存,Survivor空间容纳不了就要放在老年代中。第一次GC之后,Eden中存储4MB的allocation3对象,Survivor中存储256KB的allocation1对象,老年代存储4MB的allocation2对象。在执行最后一条语句给allocation3分配内存时,发生第二次Minor GC。当虚拟机参数-XX:MaxTenuringThreshold=1时,allocation1进入老年代,当虚拟机参数-XX:MaxTenuringThreshold=15时,allocation1仍然在新生代中。验证这个例子时,使用1.6的jdk版本才生效。

package com.tenuringdistribution;

/**
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
 * -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
 * @author Administrator
 *
 */
public class TenuringThreshold {
    private static final int _1MB = 1024 * 1024;
    public static void testTenuringThreshold() {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        allocation3 = null;
        allocation3 = new byte[4 * _1MB];
    }

    public static void main(String[] args) {
        testTenuringThreshold();
    }
}

当 MaxTenuringThreshold=1时:

运行结果:

Heap
PSYoungGen total 9216K, used 5664K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 69% used [0x00000000ff600000,0x00000000ffb881b8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400020,0x00000000ff600000)
Metaspace used 2665K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K

这里写图片描述
上图参考:https://blog.csdn.net/dam454450872/article/details/79827973

当MaxTenuringThreshold=15时,allocation1在第二次gc发生时就不会进入老年代。

运行结果如下:

Heap
def new generation total 9216K, used 4665K [0x33060000, 0x33a60000, 0x33a60000)
eden space 8192K, 52% used [0x33060000, 0x33488fe0, 0x33860000)
from space 1024K, 39% used [0x33860000, 0x338c5788, 0x33960000)
to space 1024K, 0% used [0x33960000, 0x33960000, 0x33a60000)
tenured generation total 10240K, used 4096K [0x33a60000, 0x34460000, 0x34460000)
the space 10240K, 40% used [0x33a60000, 0x33e60010, 0x33e60200, 0x34460000)
compacting perm gen total 12288K, used 375K [0x34460000, 0x35060000, 0x38460000)
the space 12288K, 3% used [0x34460000, 0x344bdc90, 0x344bde00, 0x35060000)
ro space 10240K, 51% used [0x38460000, 0x38993000, 0x38993000, 0x38e60000)
rw space 12288K, 55% used [0x38e60000, 0x394fe4f8, 0x394fe600, 0x39a60000)

4.动态对象年龄判定

为了能更好第适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

package com.tenuringdistribution;

/**
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
 * -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
 * @author Administrator
 *
 */
public class TenuringThreshold2 {
    private static final int _1MB = 1024 * 1024;
    public static void testTenuringThreshold() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }

    public static void main(String[] args) {
        testTenuringThreshold();
    }
}

运行结果:

Heap
def new generation total 9216K, used 4260K [0x33060000, 0x33a60000, 0x33a60000)
eden space 8192K, 52% used [0x33060000, 0x33488fe0, 0x33860000)
from space 1024K, 0% used [0x33860000, 0x33860088, 0x33960000)
to space 1024K, 0% used [0x33960000, 0x33960000, 0x33a60000)
tenured generation total 10240K, used 4757K [0x33a60000, 0x34460000, 0x34460000)
the space 10240K, 46% used [0x33a60000, 0x33f05738, 0x33f05800, 0x34460000)
compacting perm gen total 12288K, used 375K [0x34460000, 0x35060000, 0x38460000)
the space 12288K, 3% used [0x34460000, 0x344bdcc0, 0x344bde00, 0x35060000)
ro space 10240K, 51% used [0x38460000, 0x38993000, 0x38993000, 0x38e60000)
rw space 12288K, 55% used [0x38e60000, 0x394fe4f8, 0x394fe600, 0x39a60000)

5.空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure(-XX:+HandlePromotionFailure 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留)设置值是否允许担保失败。如果允许,那么会继续查看老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

下面解释一下“冒险”是冒了什么风险,前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在 Minor GC后任然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保。把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会存活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

取平均值进行比较其实任然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

在JDk 6 Update 24之后,这个测试结果会有差异,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略。JDk 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则进行Full GC。

九.jdk的命令行工具

   如果在工作中需要监控于JDK1.5的虚拟机之上的程序,在程序启动时需添加参数“-Dcom.sun.management.jmxremote”开启JMX管理功能,jdk1.6以上的虚拟机,jmx默认是开启的。本节所介绍的工具全部基于Windows平台下的JDK1.6 Update 21。
  

名称主要作用
jpsJVM Process Status Tool 显示指定系统内所有的hotSpot虚拟机进程
jstatJVM Statistics Monitoring Tool, 用于收集HotSpot虚拟机各方面的运行数据
jinfoConfiguration Info for Java 显示虚拟机配置信息
jmapMemory Map for Java 生成虚拟机的内存转储快照(heapdump文件)
jhatJVM Heap Dump Browser 用户分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果
jstackStack Trace for Java 显示虚拟机的线程快照

1.jps:虚拟机进程状况工具

jps命令格式
jps [options] [hostid]

执行路径: ${JAVA_HOME}\bin\jps -l

选项作用
-q只输出当前进程的本地虚拟机的唯一ID(LVMID),省略主类的名称
-m输出虚拟机进程启动时传递给主类main()函数的参数
-l输出主类的全名,如果进程执行的而是jar包,输出jar路径
-v输出虚拟机进程启动时JVM参数

2.jstat:虚拟机统计信息监视工具

jstat的格式
jstat [option vmid [interval[s|ms] [count] ]]

如果是本地虚拟机进程,则vmid=LVMID,如果是远程虚拟机进程,则vmid格式:
[protocal:] [//]vmid[@hostname[:port]/servername]

interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次,假设需要每250毫秒查询一次进程2764垃圾收集情况,一共查询20次,则: jstat -gc 2764 250 20
这里写图片描述
这里写图片描述

3.jinfo:Java 配置信息工具

jinfo (Configuration info for Java )的作用是实时实地的查看和调整虚拟机各项参数。使用jps命令-v参数可以查看虚拟机启动时显示指定的参数列表,但如果想知道未被显示指定的参数的系统默认值,可以使用jinfo的-flag选项进行查询了(如果只限于JDK1.6或以上版本的话,使用java -XX:+PrintFlagsFinal查看默认值也是一个很好的选择),jinfo 还可以使用-sysprops选项把虚拟机进程的System.getProperties()的内容打印出来。这个命令在JDK1.5时期已经随着Linux版的JDK发布,当时只提供了信息查询的功能,JDK1.6之后,jinfo在windows平台和Linux平台都有提供,并且加入了运行期修改参数的能力,可以使用-flag[+|-]name 或者-flag name=value修改一部分运行期可写的虚拟机参数值。JDK1.6中,jinfo对于Windows平台功能任然有较大限制,只提供了基本的-flag选项。

jinfo 命令格式:
jinfo [option] pid

执行样例:查询CMSInitiatingOccupancyFraction参数值
这里写图片描述

4.jmap java 内存映像工具

格式: jmap [option] vmid
这里写图片描述
这里写图片描述

对于生成的dump文件eclipse.bin,可以使用jhat命令查看: jhat eclipse.bin。
jhat(java heap analysis tool)是虚拟机堆转储快照分析工具,jhat内置了一个HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看。

5.jstack: Java堆栈追踪工具

jstact(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照。线程快照是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生产线程快照的主要目的是定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的主要原因,线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者等待着什么资源。

格式:
jstack [option] vmid

选项作用
-F当正常输出的请求不被响应时,强制输出线程堆栈
-l除堆栈外显示关于锁的附加信息
-m如果调用到本地方法的话,可以显示C/C++的堆栈

十.JDK的可视化工具

1.JConsole:java监视与管理控制台

启动JConsole,通过JDK/bin目录下的“jconsole.exe”

这里写图片描述

这里写图片描述

  • 内存监控页签相当于可视化的jstat命令
  • 线程监控页签相当于可视化的jstack命令,遇到线程停顿时可以使用这个页签进行监控分析。线程长时间停顿主要原因有:等待外部资源(数据库连接、网络资源、设备资源)、死循环、锁等待(活锁和死锁)
public class Test {

    static class SynAddRunnable implements Runnable {

        int a, b;

        public SynAddRunnable(int a, int b) {
            this.a = a;
            this.b = b;
        }

        @Override
        public void run() {

            synchronized (Integer.valueOf(a)) {
                synchronized (Integer.valueOf(b)) {
                    System.out.println(a + b);
                }
            }
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new SynAddRunnable(1, 2)).start();
            new Thread(new SynAddRunnable(2, 1)).start();
        }
    }

}

这里写图片描述
这里写图片描述

造成死锁的原因是Integer.valueof()方法基于减少对象创建次数和节省内存的考虑,[-128,127]之间的数字会被缓存,当valueof()方法传入参数在这个范围之内,将直接返回缓存中的对象

2. jVisualVM:多合一故障处理工具

是到目前为止随JDK发布的功能强大的运行监视和故障处理工具。

VisualVM的功能:

  • 显示虚拟机进程以及进程的配置、环境信息(jps、jinfo)
  • 监视应用程序的CPU、GC、堆、方法区以及线程的信息(jstat、jstack)
  • dump以及分析堆转储快照(jmap、jhat)
  • 方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法
  • 离线程序快照:收集程序运行配置、线程dump、内存dump等信息建立一个快照

visumalVM的远程配置:
https://www.cnblogs.com/Pierre-de-Ronsard/p/6771522.html
http://blog.csdn.net/chwshuang/article/details/44202561

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值