今でもあなたは私の光丶

JVM优化(3)

垃圾回收机制及算法

垃圾回收概述

什么是垃圾回收

说起垃圾收集(Garbage Collection, 下文简称GC) , 有不少人把这项技术当作Java语言的伴生产物。 事实上, 垃圾收集的历史远远比Java久远, 在1960年诞生于麻省理工学院的Lisp是第一门开始使 用内存动态分配和垃圾收集 技术的语言。垃圾收集需要完成的三件事情:

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

java垃圾回收的优缺点:

优点:

  1. 不需要考虑内存管理,
  2. 可以有效的防止内存泄漏,有效的利用可使用的内存,
  3. 由于有垃圾回收机制,Java中的对象不再有"作用域"的概念,只有对象的引用才有"作用域"

缺点:

java开发人员不了解自动内存管理, 内存管理就像一个黑匣子,过度依赖就会降低我们解决内存溢出/内存泄漏等问题 的能力。

垃圾回收-对象是否已死

判断对象是否存活 - 引用计数算法

引用计数算法可以这样实现:给每个创建的对象添加一个引用计数器,每当此对象被某个地方引用时,计数值+1, 引用失效时-1,所以当计数值为0时表示对象已经不能被使用。引用计数算法大多数情况下是个比较不错的算法, 简单直接,也有一些著名的应用案例但是对于Java虚拟机来说,并不是一个好的选择,因为它很难解决对象直接相 互循环引用的问题。

优点:

实现简单,执行效率高,很好的和程序交织。

缺点:

无法检测出循环引用。

譬如有A和B两个对象,他们都互相引用,除此之外都没有任何对外的引用,那么理论上A和B都可以被作为垃 圾回收掉,但实际如果采用引用计数算法,则A、B的引用计数都是1,并不满足被回收的条件,如果A和B之 间的引用一直存在,那么就永远无法被回收了

public class App {
public static void main(String[] args) {
Test object1 = new Test();
Test object2 = new Test();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
}
}
class Test {
public Test object = null;
}

这两个对象再无任何引用, 实际上这两个对象已 经不可能再被访问, 但是它们因为互相引用着对方, 导致它们的 引用计数都不为零, 引用计数算法也 就无法回收它们 。
但是在java程序中这两个对象仍然会被回收,因为java中并没有使用引用计数算法。

判断对象是否存活-可达性分析算法

可达性分析算法

在主流的商用程序语言如Java、C#等的主流实现中,都是通过可达性分析(Reachability Analysis)来判断对象是否存 活的。此算法的基本思路就是通过一系列的“GC Roots”的对象作为起始点,从起始点开始向下搜索到对象的路径。 搜索所经过的路径称为引用链(Reference Chain),当一个对象到任何GC Roots都没有引用链时,则表明对象“不可 达”,即该对象是不可用的。

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

  • 栈帧中的局部变量表中的reference引用所引用的对象
  • 方法区中static静态引用的对象
  • 方法区中final常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象
  • Java虚拟机内部的引用, 如基本数据类型对应的Class对象, 一些常驻的异常对象(比如 NullPointExcepiton、 OutOfMemoryError) 等, 还有系统类加载器。
  • 所有被同步锁(synchronized关键字) 持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、 JVMTI中注册的回调、 本地代码缓存等。
JVM之判断对象是否存活

finalize()方法最终判定对象是否存活:

即使在可达性分析算法中判定为不可达的对象, 也不是“非死不可”的, 这时候它们暂时还处于“缓 刑”阶段, 要真 正宣告一个对象死亡, 至少要经历两次标记过程:

第一次标记:

如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链, 那它将会被第一次标记, 随后进行一次筛 选, 筛选的条件是此对象是否有必要执行finalize()方法。

没有必要:

假如对象没有覆盖finalize()方法, 或者finalize()方法已经被虚拟机调用过, 那么虚拟机将这两种情况都视为“没有必 要执行”。

有必要:

如果这个对象被判定为确有必要执行finalize()方法, 那么该对象将会被放置在一个名为F-Queue的 队列之中, 并在 稍后由一条由虚拟机自动建立的、 低调度优先级的Finalizer线程去执行它们的finalize() 方法。 finalize()方法是对 象 逃脱死亡命运的最后一次机会, 稍后收集器将对F-Queue中的对象进行第二次小规模的标记, 如果对 象要在 finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可, 譬如把自己 (this关键字) 赋值 给某个类变量或者对象的成员变量, 那在第二次标记时它将被移出“即将回收”的集 合; 如果对象这时候还没有逃 脱, 那基本上它就真的要被回收了。

一次对象自我拯救的演示

package com.lagou.unit3;
/**
* 此代码演示了两点:
* 1.对象可以在被GC时自我拯救。
* 2.这种自救的机会只有一次, 因为一个对象的finalize()方法最多只会被系统自动调用一次
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低, 暂停0.5秒, 以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
//下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低, 暂停0.5秒, 以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}

注意:

Finalizer线程去执行它们的finalize() 方法, 这里所说的“执行”是指虚拟机会触发这个方法开始运行, 但并不承诺一定 会等待它运行结束。 这样做的原因是, 如果某个对象的finalize()方法执行缓慢, 或者更极端地发生了死循环, 将 很可能导 致F-Queue队列中的其他对象永久处于等待, 甚至导致整个内存回收子系统的崩溃。

再谈引用

在JDK1.2以前,Java中引用的定义很传统: 如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就 称这块内存代表着一个引用。这种定义有些狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态。 我 们希望能描述这一类对象: 当内存空间还足够时,则能保存在内存中;如果内存空间在进行垃圾回收后还是非常紧 张,则可以抛弃这些对象。很多系统中的缓存对象都符合这样的场景。 在JDK1.2之后,Java对引用的概念做了扩 充,将引用分为 强引用(Strong Reference) 、 软引用(Soft Reference) 、 弱引用(Weak Reference) 和 虚引 用(Phantom Reference) 四种,这四种引用的强度依次递减。

⑴强引用(StrongReference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟 机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问 题。 ps:强引用其实也就是我们平时A a = new A()这个意思。

⑵软引用(SoReference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对 象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。 软引用可以和一个引用队列 (ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到 与之关联的引用队列中。

⑶弱引用(WeakReference)

用来描述那些非必须对象, 但是它的强度比软引用更弱一些, 被弱引用关联的对象只能生存到下一次垃圾收集发 生为止。 当垃圾收集器开始工作, 无论当前内存是否足够, 都会回收掉只 被弱引用关联的对象。 在JDK 1.2版之 后提供了WeakReference类来实现弱引用。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用 所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

弱引用与软引用的区别在于:

①更短暂的生命周期
②一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

⑷虚引用(PhantomReference)

“虚引用”顾名思义,它是最弱的一种引用关系。如果一个对象仅持有虚引用,在任何时候都可能被垃圾回收器回 收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。

虚引用与软引用和弱引用的一个区别在于:

①虚引用必须和引用队列 (ReferenceQueue)联合使用。
②当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到 与之 关联的引用队列中。

垃圾收集算法

分代收集理论

思想也很简单,就是根据对象的生命周期将内存划分,然后进行分区管理。 当前商业虚拟机的垃圾收集器, 大多 数都遵循了“分代收集”(Generational Collection)的理论进 行设计, 分代收集名为理论, 实质是一套符合大多数 程序运行实际情况的经验法则, 它建立在两个分代假说之上:

1) 弱分代假说(Weak Generational Hypothesis) : 绝大多数对象都是朝生夕灭的。
2) 强分代假说(Strong Generational Hypothesis) : 熬过越多次垃圾收集过程的对象就越难以消亡。

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则: 收集器应该将Java堆划分 出不同的区域, 然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数) 分配到不同的区 域之中存储。 显而易见, 如 果一个区域中大多数对象都是朝生夕灭, 难以熬过垃圾收集过程的话, 那 么把它们集中放在一起, 每次回收时只 关注如何保留少量存活而不是去标记那些大量将要被回收的对 象, 就能以较低代价回收到大量的空间; 如果剩下 的都是难以消亡的对象, 那把它们集中放在一块, 虚拟机便可以使用较低的频率来回收这个区域, 这就同时兼顾 了垃圾收集的时间开销和内存的空间有 效利用。

在Java堆划分出不同的区域之后, 垃圾收集器才可以每次只回收其中某一个或者某些部分的区域 ——因而才有 了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分; 也才能够针对不同的区域安 排与里面存储对象存亡特征 相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算 法”“标记-整理算法”等针对性的垃圾收集算 法。

他针对不同分代的类似名词, 为避免产生混淆, 在这里统一定义 :

  • 部分收集(Partial GC) : 指目标不是完整收集整个Java堆的垃圾收集, 其中又分为:
    • 新生代收集(Minor GC/Young GC): 指目标只是新生代的垃圾收集。
    • 老年代收集(Major GC/Old GC): 指目标只是老年代的垃圾收集,目前只有CMS收集器会有单 独收集老年代的行为。
    • 混合收集(Mixed GC): 指目标是收集整个新生代以及部分老年代的垃圾收集。 目前只有G1收集器会有这种行为。
  • 整堆收集(Full GC) : 收集整个Java堆和方法区的垃圾收集

标记-清除算法

什么是标记-清除算法?

最早出现也是最基础的垃圾收集算法是“标记-清除”(Mark-Sweep) 算法, 在1960年由Lisp之父 John McCarthy所 提出。 如它的名字一样, 算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回 收的对象, 在标记完成后, 统一回收掉所有被标记的对象, 也可以反过来, 标记存活的对象, 统一回 收所有未被标记的对象。

标记过程就是对象是否属于垃圾的判定过程, 这在前一节讲述垃圾对象标记 判定算法时其实已经介绍过了。 之所 以说它是最基础的收集算法, 是因为后续的收集算法大多都是以标记-清除算法为基础, 对其 缺点进行改进而得到 的。

标记-清除算法有两个不足之处:

第一个是执行效率不稳定, 如果Java堆中包含大量对 象, 而且其中大部分是需要被回收的, 这时必须进行大量标 记和清除的动作, 导致标记和清除两个过 程的执行效率都随对象数量增长而降低;

第二个是内存空间的碎片化问题, 标记、 清除之后会产生大 量不连续的内存碎片, 空间碎片太多可能会导致当以 后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-复制算法

什么是标记-复制算法

标记-复制算法常被简称为复制算法。

为了解决标记-清除算法面对大量可回收对象时执行效率低的问题, 1969年Fenichel提出了一种称为“半区复 制”(Semispace Copying) 的垃圾收集算法, 它将可用 内存按容量划分为大小相等的两块, 每次只使用其中的一 块。 当这一块的内存用完了, 就将还存活着 的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理 掉。 如果内存中多数对象都是存活的, 这种算法将会产生大量的内存间复制的开销, 但对于多数对象都是可回收 的情况, 算法需要复制的就是占少数的存活对象, 而且每次都是针对整个半区进行内存回收, 分配内存时也就不 用考虑有空间碎片的复杂情况, 只要移动堆顶指针, 按顺序分配即可.

但是这种算法也有缺点:

  1. 需要提前预留一半的内存区域用来存放存活的对象(经过垃圾收集后还存活的对象),这样导致可用的对 象区域减小一半,总体的GC更加频繁了
  2. 如果出现存活对象数量比较多的时候,需要复制较多的对象,成本上升,效率降低
  3. 如果99%的对象都是存活的(老年代),那么老年代是无法使用这种算法的。

注意事项:

现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代, IBM公司曾有一项专门研 究对新生代“朝生 夕灭”的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。 因此 并不需要按照1∶1的比例来划分 新生代的内存空间。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的 Survivor空间, 每 次分配内存只使用Eden和其中一块Survivor。 发生垃圾搜集时, 将Eden和Survivor中仍 然存活的对象一次性复制到 另外一块Survivor空间上, 然后直接清理掉Eden和已用过的那块Survivor空 间。 HotSpot虚拟机默认Eden和Survivor 的大小比例是8∶1, 也即每次新生代中可用内存空间为整个新 生代容量的90%(Eden的80%加上一个Survivor的 10%) , 只有一个Survivor空间, 即10%的新生代是会 被“浪费”的。

标记-整理算法

标记-复制算法在对象存活率较高时就要进行较多的复制操作, 效率将会降低。 更关键的是, 如果不想浪费50%的 空间, 就需要有额外的空间进行分配担保, 以应对被使用的内存中所有对象都100%存活的极端情况, 所以在老年 代一般不能直接选用这种算法

针对老年代对象的存亡特征, 1974年Edward Lueders提出了另外一种有针对性的“标记-整 理”(Mark-Compact) 算 法, 其中的标记过程仍然与“标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活 的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存 。

标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法, 而后者是移动式的。 是否移动 回收后的存活对象是一项优缺点并存的风险决策:

是否移动对象都存在弊端, 移动则内存回收时会更复杂, 不移动则内存分配时会更复杂。 从垃圾收集的停顿时间 来看, 不移动对象停顿时间会更短, 甚至可以不需要停顿, 但是从整个程序的吞吐量来看, 移动对象会更划算。

垃圾收集器

垃圾收集器概述

垃圾回收器与垃圾回收算法

垃圾回收算法分类两类,第一类算法判断对象生死算法,如引用计数法、可达性分析算法 ;第二类收集死亡对象方 法有四种,如标记-清除算法、标记-复制算法、标记-整理算法。一般的实现采用分代回收算法,根据不同代的特点应 用不同的算法。垃圾回收算法是内存回收的方法论。垃圾收集器是算法的落地实现。和回收算法一样,目前还没有 出现完美的收集器,而是要根据具体的应用场景选择最合适的收集器,进行分代收集。

垃圾收集器分类

串行垃圾回收(Serial)

串行垃圾回收是为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程,不适合交互性强的服 务器环境

并行垃圾回收(Parallel)

多个垃圾收集器线程并行工作,同样会暂停用户线程,适用于科学计算、大数据后台处理等多交互场景。

并发垃圾回收(CMS)

用户线程和垃圾回收线程同时执行,不一定是并行的,可能是交替执行,可能一边垃圾回收,一边运行应用线程, 不需要停顿用户线程,互联网应用程序中经常使用,适用对响应时间有要求的场景。

G1垃圾回收

G1垃圾回收器将堆内存分割成不同的区域然后并发地对其进行垃圾回收。

七种垃圾收集器及其组合关系

根据分代思想,我们有7种主流的垃圾回收器

新生代垃圾收集器:Serial 、 ParNew 、Parallel Scavenge
老年代垃圾收集器:Serial Old 、 Parallel Old 、CMS
整理收集器:G1

垃圾收集器的组合关系

JDK8中默认使用组合是: Parallel Scavenge GC 、ParallelOld GC

JDK9默认是用G1为垃圾收集器
JDK14 弃用了: Parallel Scavenge GC 、Parallel OldGC
JDK14 移除了 CMS GC

GC性能指标

吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时 间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
暂停时间:执行垃圾回收时,程序的工作线程被暂停的时间
内存占用:java堆所占内存的大小
收集频率:垃圾收集的频次

Serial收集器

单线程收集器,“单线程”的意义不仅仅说明它只会使用一个CPU或一个收集线程去完成垃圾收集工作;
更重要的是它在垃圾收集的时候,必须暂停其他工作线程,直到垃圾收集完毕;

这个收集器是一个单线程工作的收集器, 但它的“单线 程”的意义并不仅仅是说明它只会使用一个处理器或一条收 集线程去完成垃圾收集工作, 更重要的是强 调在它进行垃圾收集时, 必须暂停其他所有工作线程, 直到它收集结 束。 “Stop The World”这个词语也 许听起来很酷, 但这项工作是由虚拟机在后台自动发起和自动完成的, 在用户 不可知、 不可控的情况 下把用户的正常工作的线程全部停掉, 这对很多应用来说都是不能接受的。

示意了Serial/Serial Old收 集器的运行过程

Serial收集器也并不是只有缺点;Serial收集器由于简单并且高效;
对于单CPU环境来说,由于Serial收集器没有线程间的交互,专心做垃圾收集自然可以做获得最高的垃圾收集效率
使用方式:-XX:+UseSerialGC

ParNew 收集器

ParNew收集器实质上是Serial收集器的多线程并行版本, 除了同时使用多条线程进行垃圾收集之 外, 其余的行为 包括Serial收集器可用的所有控制参数 、 收集算法、 Stop The World、 对象分配规则、 回收策略等都与Serial收集 器完全一致, 在实现上这两种收集器也共用了相当多的代码.

ParNew收 集器的工作过程

ParNew收集器在单CPU服务器上的垃圾收集效率绝对不会比Serial收集器高;
但是在多CPU服务器上,效果会明显比Serial好
使用方式:-XX:+UseParNewGC
设置线程数: XX:ParllGCThreads

Parallel Scavenge收集器

什么是Parallel Scanvenge

又称为吞吐量优先收集器,和ParNew收集器类似,是一个新生代收集器。使用复制算法的并行多线程收集器。 Parallel Scavenge是Java1.8默认的收集器,特点是并行的多线程回收,以吞吐量优先。

特点

  • Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput);
    吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
    (虚拟机总共运行100分钟,垃圾收集时间为1分钟,那么吞吐量就是99%)
  • 自适应调节策略,自动指定年轻代、Eden、Suvisor区的比例。

适用场景

适合后台运算,交互不多的任务,如批量处理,订单处理,科学计算等。

参数

  • 使用方式:-XX:+UseParallelGC
  • 分别是控制:最大垃圾收集停顿时间-XX:MaxGCPauseMillis
    -XX: MaxGCPauseMillis参数允许的值是一个大于0的毫秒数, 收集器将尽力保证内存回收花费的 时间不超过用户设定值。 不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得 系统的垃圾收集速度变得更快, 垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的: 系统把新生代调得小一些, 收集300MB新生代肯定比收集500MB快, 但这也直接导致垃圾收集发生得 更频繁, 原来10秒收集一次、 每次停顿100毫秒, 现在变成5秒收集一次、 每次停顿70毫秒。 停顿时间 的确在下降, 但吞吐量也降下来了。
  • 吞吐量大小-XX:GCTimeRatio
    -XX: GCTimeRatio参数的值则应当是一个大于0小于100的整数, 也就是垃圾收集时间占总时间的 比率, 相当于吞吐量的倒数。 假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。譬如把此 参数设置为19, 那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)) , 默认值为99, 即允许最大1%(即 1/(1+99)) 的垃圾收集时间
  • 设置年轻代线程数 XX:ParllGCThreads
    当cpu合数小于等于8,默认cpu核数相同; 当cpu核数超过8, ParllGCThreads设置为 3+(5*CPU_COUNT)/8
  • 与Parallel Scavenge收集器有关的还有一个参数:-XX:+UseAdaptiveSizePolicy(有了这个参数之后,就不要手工 指定年轻代、Eden、Suvisor区的比例,晋升老年代的对象年龄等,因为虚拟机会根据系统运行情况进行自适 应调节)

Serial Old收集器

Serial Old是Serial收集器的老年代版本, 它同样是一个单线程收集器, 使用标记-整理算法。 这个收集器的主要意 义也是供客户端模式下的HotSpot虚拟机使用。

特点:

  • 针对老年代;
  • 采用"标记-整理"算法;
  • 单线程收集;

执行流程:

应用场景: 主要用于Client模式

1)在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);
2)作为 CMS收集器的后备预案 ,在并发收集发生Concurrent Mode Failure时使用

参数设置:

使用方式:-XX:+UseSerialGC

注意事项:

需要说明一下, Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集, 并非 直接调用 Serial Old收集器, 但是这个PS MarkSweep收集器与Serial Old的实现几乎是一样的, 所以在官方的许多资料中都是 直接以Serial Old代替PS MarkSweep进行讲解.

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本, 支持多线程并发收集, 基于标记-整理算法实现。 这个收集器 是直到JDK 6时才开始提供的, 在此之前, 新生代的Parallel Scavenge收集器一直处于相 当尴尬的状态, 原因是如 果新生代选择了Parallel Scavenge收集器, 老年代除了Serial Old(PS MarkSweep) 收集器以外别无选择, 其他表 现良好的老年代收集器, 如CMS无法与它配合工作。

Parallel Old收集器的工作过程:

应用场景

JDK1.6及之后用来代替老年代的Serial Old收集器;
特别是在Server模式,多CPU的情况下;
这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合;

设置参数

"-XX:+UseParallelOldGC":指定使用Parallel Old收集器;

CMS 收集器

CMS垃圾回收器

CMS(concurrent mark sweep)是以获取最短垃圾收集停顿时间为目标的收集器,CMS收集器的关注点尽可能缩短 垃圾收集时用户线程的停顿时间,停顿时间越短就越适合与用户交互的程序,目前很大一部分的java应用几种在互联网 的B/S系统服务器上,这类应用尤其注重服务器的响应速度,系统停顿时间最短,给用户带来良好的体验,CMS收 集器使用的算法是标记-清除算法实现的;

CMS垃圾收集过程

整个过程分4个步骤:

1)初始标记
2) 并发标记
3) 重新标记
4) 并发清除

其中 初始标记 和 重新标记 都需要stopTheWorld

CMS整个过程比之前的收集器要复杂,整个过程分为4个阶段即、初始标记 并发标记 、重新标记、并发清除

  • 初始标记(Initial-Mark)阶段:这个阶段程序所有的工作线程都将会因为"Stop-the-Wold"机制而出现短暂的的 暂停,这个阶段的主要任务标记处GC Roots 能够关联到的对象.一旦标记完成后就恢复之前被暂停的的所有应 用。 由于直接关联对象比较小,所以这里的操作速度非常快。
  • 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较 长,但是不需要暂停用户线程, 用户线程可以与垃圾回收器一起运行。
  • 重新标记(Remark)阶段:由于并发标记阶段,程序的工作线程会和垃圾收集线程同时运行或者交叉运行, 因此,为了修正并发标记期间因为用户继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段 的停顿时间通常比初始标记阶段长一些,但也远比并发标记阶段时间短。
  • 清除并发(Concurrent-Sweep)阶段: 此阶段清理删除掉标记判断已经死亡的对象,并释放内存空间。由于不需 要移动存活对象,所以这个阶段可以与用户线程同时并发运行。

由于最消耗事件的并发标记与并发清除阶段都不需要暂停工作,因为整个回收阶段是低停顿(低延迟)的。

并发可达性分析

当前主流编程语言的垃圾收集器基本上都是依靠可达性分析算法来判定对象 是否存活的, 可达性分析算法理论上 要求全过程都基于一个能保障一致性的快照中才能够进行分析。

垃圾回收器的工作流程大体如下:

  1. 标记出哪些对象是存活的,哪些是垃圾(可回收);
  2. 进行回收(清除/复制/整理),如果有移动过对象(复制/整理),还需要更新引用。

三色标记

三色标记(Tri-color Marking)作为工具来辅助推导, 把遍历对象图过程中遇到的对象, 按照“是否访问过”这个条 件标记成以下三种颜色:

要找出存活对象,根据可达性分析,从GC Roots开始进行遍历访问,可达的则为存活对象:

我们把遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色:

  • 白色:尚未访问过。
  • 黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
  • 灰色:本对象已访问过,但是本对象 引用到 的其他对象尚未全部访问完。全部访问后,会转换为黑色。

假设现在有白、灰、黑三个集合(表示当前对象的颜色),其遍历访问过程为:

  1. 初始时,所有对象都在 【白色集合】中;
  2. 将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
  3. 从灰色集合中获取对象:
    1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
    2. 将本对象 挪到 【黑色集合】里面。
  4. 重复步骤3,直至【灰色集合】为空时结束。
  5. 结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收。

注:如果标记结束后对象仍为白色,意味着已经“找不到”该对象在哪了,不可能会再被重新引用。

当Stop The World (以下简称 STW)时,对象间的引用 是不会发生变化的,可以轻松完成标记。 而当需要支持并 发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。

多标-浮动垃圾

假设已经遍历到E(变为灰色了),此时应用执行了 objD.fieldE = null :

此刻之后,对象E/F/G是“应该”被回收的。然而因为E已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终 的结果是:这部分对象仍会被标记为存活,即本轮GC不会回收这部分内存。

这部分本应该回收 但是 没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响应用程序的正确性,只是 需要等到下一轮垃圾回收中才被清除。

漏标

假设GC线程已经遍历到E(变为灰色了),此时应用线程先执行了:

var G = objE.fieldG;
objE.fieldG = null; // 灰色E 断开引用 白色G
objD.fieldG = G; // 黑色D 引用 白色G

此时切回GC线程继续跑,因为E已经没有对G的引用了,所以不会将G放到灰色集合;尽管因为D重新引用了G,但 因为D已经是黑色了,不会再重新做遍历处理。 最终导致的结果是:G会一直停留在白色集合中,最后被当作垃圾 进行清除。这直接影响到了应用程序的正确性,是不可接受的。

不难分析,漏标只有同时满足以下两个条件时才会发生: 条件一:灰色对象 断开了 白色对象的引用;即灰色对象 原来成员变量的引用 发生了变化。 条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的 引用。

从代码的角度看:

var G = objE.fieldG; // 1.读
objE.fieldG = null; // 2.写
objD.fieldG = G; // 3.写
  1. 读取 对象E的成员变量fieldG的引用值,即对象G;
  2. 对象E 往其成员变量fieldG,写入 null值。
  3. 对象D 往其成员变量fieldG,写入 对象G ;

我们只要在上面这三步中的任意一步中做一些“手脚”,将对象G记录起来,然后作为灰色对象再进行遍历即可。比 如放到一个特定的集合,等初始的GC Roots遍历完(并发标记),该集合的对象 遍历即可(重新标记)。

重新标记是需要STW的,因为应用程序一直在跑的话,该集合可能会一直增加新的对象,导致永远都跑不 完。当然,并发标记期间也可以将该集合中的大部分先跑了,从而缩短重新标记STW的时间,这个是优化问 题了。

CMS收集器三个缺点

CMS收集器对CPU资源非常敏感。

其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用 了一部分线程而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(处理器核心数量 +3) /4, 也就是说, 如果处理器核心数在四个或以上, 并发回收时垃圾收集线程只占用不超过25%的 处理器运算资源, 并 且会随着处理器核心数量的增加而下降。 但是当处理器核心数量不足四个时, CMS对用户程序的影响就可能变得 很大。 如果应用本来的处理器负载就很高, 还要分出一半的运算能 力去执行收集器线程, 就可能导致用户程序的 执行速度忽然大幅降低。

CMS收集器无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生。

由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在 标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为"浮动垃 圾"。同样也是由于在垃圾收集阶段用户线程还需要持续运 行, 那就还需要预留足够内存空间提供给用户线程使 用, 因此CMS收集器不能像其他收集器那样等待 到老年代几乎完全被填满了再进行收集, 必须预留一部分空间供 并发收集时的程序运作使用。

在JDK 5的默认设置下, CMS收集器当老年代使用了68%的空间后就会被激活, 这是一个偏保守的设置, 如果 在实 际应用中老年代增长并不是太快, 可以适当调高参数-XX: CMSInitiatingOccu-pancyFraction的值 来提高CMS的触发 百分比, 降低内存回收频率, 获取更好的性能。 到了JDK 6时, CMS收集器的启动 阈值就已经默认提升至92%。 但这又会更容易面临另一种风险: 要是CMS运行期间预留的内存无法满 足程序分配新对象的需要, 就会出现一 次“并发失败”(Concurrent Mode Failure) , 这时候虚拟机将不 得不启动后备预案: 冻结用户线程的执行, 临时 启用Serial Old收集器来重新进行老年代的垃圾收集, 但这样停顿时间就很长了。

空间碎片:CMS是一款基于标记-清除算法实现的收集器,所有会有空间碎片的现象。

当空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大 的连续空间来分配当前对象,不得不提前触发一次Full GC。

为了解决这个问题, CMS收集器提供了一个-XX: +UseCMS-CompactAtFullCollection开关参数(默认是开启的, 此 参数从 JDK 9开始废弃) , 用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程, 由于这个 内存 整理必须移动存活对象, 是无法并发的。 这样空间碎片问题是解 决了, 但停顿时间又会变长, 因此虚拟机设计者 们还提供了另外一个参数-XX: CMSFullGCsBeforeCompaction(此参数从JDK 9开始废弃) , 这个参数的作用是要 求CMS收集器在执行过若干次(数量 由参数值决定) 不整理空间的Full GC之后, 下一次进入Full GC前会先进行碎 片整理(默认值为0, 表 示每次进入Full GC时都进行碎片整理)。

G1收集器

G1垃圾收集器简介

Garbage First是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足 GC停顿时间的同时,还兼具高吞吐量的性能特征。

G1收集器特点
  1. G1把内存划分为多个独立的区域Region
  2. G1仍然保留分代思想,保留了新生代和老年代,但他们不再是物理隔离,而是一部分Region的集合
  3. G1能够充分利用多CPU、多核环境硬件优势,尽量缩短STW
  4. G1整体整体采用标记整理算法,局部是采用复制算法,不会产生内存碎片
  5. G1的停顿可预测,能够明确指定在一个时间段内,消耗在垃圾收集上的时间不超过设置时间
  6. G1跟踪各个Region里面垃圾的价值大小,会维护一个优先列表,每次根据允许的时间来回收价值最大的区域,从 而保证在有限事件内高效的收集垃圾

Region区域

G1不再坚持固定大小以及固定数量的 分代区域划分, 而是把连续的Java堆划分为多个独立区域(Region) , 每一 个Region都可以 根据需要, 扮演新生代的Eden空间、 Survivor空间, 或者老年代空间。

将整个堆空间细分为若干个小的区域

①使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实 际大小而定,为2的N次幂,即1MB, 2MB, 4MB, 8MB, 16MB,32MB。

② 虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region (不需要连 续)的集合。通过Region的动态分配方式实现逻辑上的连续。

③ G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。主要用于存储大对象,如 果超过1 .5个region,就放到H。一般被视为老年代.

G1 GC过程

G1提供了两种GC模式,Young GC和Mixed GC,两种均是完全Stop The World的。

  • Young GC:选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC 的时间开销。
  • Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代 Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。

在G1 GC垃圾回收的过程一个有四个阶段:

初始标记 :和CMS一样只标记GC Roots直接关联的对象
并发标记 :进行GC Roots Traceing过程
最终标记 :修正并发标记期间,因程序运行导致发生变化的那一部分对象
筛选回收 :根据时间来进行价值最大化收集

下面是G1收集的示意图

G1 YoungGC

YoungGC执行前

堆分为大约2000个区域。最小大小为1Mb,最大大小为32Mb。蓝色区域保存老年代对象,绿色区域保存年轻对象。

执行YoungGC

将存活的对象(即复制或移动)到一个或多个幸存者区域。如果满足老化阈值,则某些对象将被提升到老年代区 域。

G1的年轻GC结束

最近升级的对象以深蓝色显示。幸存者区域为绿色。
总而言之,关于G1的年轻一代,可以说以下几点:

  • 堆是单个内存空间,分为多个区域。
  • 年轻代内存由一组非连续区域组成。
  • 年轻一代的垃圾收集器或年轻的GC出现STW。将停止所有应用程序线程以进行操作。
  • 年轻的GC使用多个线程并行完成。
  • 将活动对象复制到新的幸存者或老年代的地区。

G1 Mix GC

初始标记阶段(initial mark,STW)

存活的对象的初始标记背负在年轻的垃圾收集器上。在日志中,此标记为 GC pause (young)(inital-mark) 。

并发标记阶段(Concurrent Marking)

如果找到空白区域(如“ X”所示),则在Remark阶段将其立即删除。另外,计算确定活跃度的信息。

最终标记阶段(Remark,STW)

空区域将被删除并回收。现在可以计算所有区域的区域活跃度。

筛选回收阶段/复制清理阶段(Cleanup,STW)

G1选择“活度”最低的区域,这些区域可以被最快地收集。然后与年轻的GC同时收集这些区域。这在日志中表示为 [GC pause (mixed)] 。因此,年轻代和老年代都是同时收集的。

筛选回收阶段-(复制/清理)阶段之后

选定的区域已被收集并压缩为图中所示的深蓝色区域和深绿色区域。

总结:

  • 并发标记阶段
    • 活动信息是在应用程序运行时同时计算的。
    • 该活动信息标识在疏散暂停期间最适合回收的区域。
    • 像CMS中没有清扫阶段。
  • 最终标记阶段
    • 使用开始快照(SATB)算法,该算法比CMS使用的算法快得多。
    • 完全回收空区域。
  • 筛选回收阶段
    • 同时回收年轻一代和老一代。
    • 老年代地区是根据其活跃度来选择的。
G1常用参数

常用指令与可视化调优工具

常用指令

jps

jps 是(java process Status Tool), Java版的ps命令,查看java进程及其相关的信息,如果你想找到一个java进程的 pid,那可以用jps命令替代linux中的ps命令了,简单而方便。

命令格式:

jps [options] [hostid]

options参数解释:

  • -l : 显示进程id,显示主类全名或jar路径
  • -q : 显示进程id
  • -m : 显示进程id, 显示JVM启动时传递给main()的参数
  • -v : 显示进程id,显示JVM启动时显示指定的JVM参数

hostid : 主机或其他服务器ip

最常用示例:

jps -l 输出jar包路径,类全名
jps -m 输出main参数
jps -v 输出JVM参数

参考代码:

package com.lagou.unit4;
/**
* jps -q 显示进程id
* jps -l 输出jar包路径,类全名
* jps -m 输出主类名,及传入main方法的参数
* jps -v 输出主类名,及输出JVM参数
*/
public class Demo1_Jps {
public static void main(String[] args) {
System.out.println("jps 指令");
try {
Thread.sleep(200000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

jinfo

jinfo是用来查看JVM参数和动态修改部分JVM参数的命令

命令格式:

 jinfo [option]  <pid>

options参数解释:

  • no options 输出所有的系统属性和参数
  • -flag 打印指定名称的参数
  • -flag [+|-] 打开或关闭参数
  • -flag = 设置参数
  • -flags 打印所有参数
  • -sysprops 打印系统配置

最常用示例:
其中11666为pid

package com.lagou.unit4;
/**
jinfo [option] <pid>
options参数解释:
- no options 输出所有的系统属性和参数
- -flag <name> 打印指定名称的参数
- -flag [+|-]<name> 打开或关闭参数
- -flag <name>=<value> 设置参数
- -flags 打印所有参数
- -sysprops 打印系统配置
*
*/
public class Demo2_jinfo {
public static void main(String[] args) {
System.out.println("jinfo 指令");
try {
Thread.sleep(2000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

查看JVM参数和系统配置

jinfo 11666
jinfo -flags 11666
jinfo -sysprops 11666

查看打印GC日志参数

jinfo -flag PrintGC 11666
jinfo -flag PrintGCDetails 11666

打开GC日志参数

jinfo -flag +PrintGC 11666
jinfo -flag +PrintGCDetails 11666

关闭GC日志参数

jinfo -flag -PrintGC 11666
jinfo -flag -PrintGCDetails 11666

还可以使用下面的命令查看那些参数可以使用jinfo命令来管理:
常用JVM参数:

  • -Xms:初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增 大堆直到-Xmx的最大限制
  • -Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
  • -Xmn:新生代的内存空间大小,注意:此处的大小是(eden+ 2 survivor space)。与jmap -heap中显示的New gen是不同 的。整个堆大小=新生代大小 + 老生代大小 + 永久代大小。
    在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
  • -XX:SurvivorRatio:新生代中Eden区域与Survivor区域的容量比值,默认值为8。两个Survivor区与一个Eden区的比值为 2:8,一个Survivor区占整个年轻代的1/10。
  • -Xss:每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。应根据应用的线程所需内存大小 进行适当调整。在相同物理内存下, 减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。一般 小的应用, 如果栈不是很深, 应该是128k够用的, 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。和threadstacksize选项解释很类似,官方文档似乎没有 解释, 在论坛中有这样一句话:"-Xss ``is translated ``in a VM flag named ThreadStackSize”一般设置这个值就可以了。
  • -XX:PermSize:设置永久代(perm gen)初始值。默认值为物理内存的1/64。
  • -XX:MaxPermSize:设置持久代最大值。物理内存的1/4。

jstat

jstat命令是使用频率比较高的命令,主要用来查看JVM运行时的状态信息,包括内存状态、垃圾回收等。

命令格式:

jstat [option] VMID [interval] [count ]

其中VMID是进程id,interval是打印间隔时间(毫秒),count是打印次数(默认一直打印)

option参数解释:

  • -class class loader的行为统计
  • -compiler HotSpt JIT编译器行为统计
  • -gc 垃圾回收堆的行为统计
  • -gccapacity 各个垃圾回收代容量(young,old,perm)和他们相应的空间统计
  • -gcutil 垃圾回收统计概述
  • -gccause 垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因
  • -gcnew 新生代行为统计
  • -gcnewcapacity 新生代与其相应的内存空间的统计
  • -gcold 年老代和永生代行为统计
  • -gcoldcapacity 年老代行为统计
  • -printcompilation HotSpot编译方法统计

常用示例及打印字段解释:

jstat -gcutil 11666 1000 3

字段解释:

  • S0 survivor0使用百分比
  • S1 survivor1使用百分比
  • O 老年代使用百分比
  • M 元数据区使用百分比
  • CCS 压缩使用百分比
  • YGC 年轻代垃圾回收次数
  • YGCT 年轻代垃圾回收消耗时间
  • FGC Full GC垃圾回收次数
  • FGCT Full GC垃圾回收消耗时间
  • GCT 垃圾回收消耗总时间
jstat -gc 11666 1000 3

-gc和-gcutil参数类似,只不过输出字段不是百分比,而是实际的值。
输出

字段解释:

  • S0C survivor0大小
  • S1C survivor1大小
  • S0U survivor0已使用大小
  • S1U survivor1已使用大小
  • EC Eden区大小
  • EU Eden区已使用大小
  • OC 老年代大小
  • OU 老年代已使用大小
  • MC 方法区大小
  • MU 方法区已使用大小
  • CCSC 压缩类空间大小
  • CCSU 压缩类空间已使用大小
  • YGC 年轻代垃圾回收次数
  • YGCT 年轻代垃圾回收消耗时间
  • FGC Full GC垃圾回收次数
  • FGCT Full GC垃圾回收消耗时间
  • GCT 垃圾回收消耗总时间

jstack

jstack是用来查看JVM线程快照的命令,线程快照是当前JVM线程正在执行的方法堆栈集合。使用jstack命令可以定 位线程出现长时间卡顿的原因,例如死锁,死循环等。jstack还可以查看程序崩溃时生成的core文件中的stack信 息。

命令格式:

jstack [options]

option参数解释:

  • -F 当使用jstack 无响应时,强制输出线程堆栈。
  • -m 同时输出java堆栈和c/c++堆栈信息(混合模式)
  • -l 除了输出堆栈信息外,还显示关于锁的附加信息

cpu占用过高问题

  1. 使用Process Explorer工具找到cpu占用率较高的线程
  2. 在thread卡中找到cpu占用高的线程id
  3. 线程id转换成16进制
  4. 使用jstack -l 查看进程的线程快照
  5. 线程快照中找到指定线程,并分析代码

jstack检查死锁问题

public class DeadLock {
private static Object obj1 = new Object();
private static Object obj2 = new Object();
public static void main(String[] args) {
new Thread(new Thread1()).start();
new Thread(new Thread2()).start();
}
private static class Thread1 implements Runnable{
public void run() {
synchronized (obj1){
System.out.println("Thread1 拿到了 obj1 的锁!");
try {
// 停顿2秒的意义在于,让Thread2线程拿到obj2的锁
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2){
System.out.println("Thread1 拿到了 obj2 的锁!");
}
}
}
}
private static class Thread2 implements Runnable{
public void run() {
synchronized (obj2){
System.out.println("Thread2 拿到了 obj2 的锁!");
try {
// 停顿2秒的意义在于,让Thread1线程拿到obj1的锁
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1){
System.out.println("Thread2 拿到了 obj1 的锁!");
}
}
}
}
}

执行指令:

jstack -l 11666

打印结果

Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007efc880062c8 (object 0x00000000ec1dc5c8, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007efc88004e28 (object 0x00000000ec1dc5d8, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at DeadLock$Thread2.run(DeadLock.java:35)
- waiting to lock <0x00000000ec1dc5c8> (a java.lang.Object)
- locked <0x00000000ec1dc5d8> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at DeadLock$Thread1.run(DeadLock.java:19)
- waiting to lock <0x00000000ec1dc5d8> (a java.lang.Object)
- locked <0x00000000ec1dc5c8> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock

jmap

jmap可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列

命令格式:

jmap [option] (连接正在执行的进程)

option参数解释:

  • 如果使用不带选项参数的jmap打印共享对象映射,将会打印目标虚拟机中加载的每个共享对象的起始 地址、 映射大小以及共享对象文件的路径全称。
  • -heap 打印java heap摘要
  • -histo[:live] 打印堆中的java对象统计信息
  • -clstats 打印类加载器统计信息
  • -finalizerinfo 打印在f-queue中等待执行finalizer方法的对象
  • -dump: 生成java堆的dump文件
    • dump-options:
      • live 只转储存活的对象,如果没有指定则转储所有对象
      • format=b 二进制格式
      • file= 转储文件到

常用示例:

jmap -dump:live,format=b,file=dump.bin 11666

输出:

Dumping heap to /dump.bin ...
Heap dump file created

这个命令是要把java堆中的存活对象信息转储到dump.bin文件

jmap -finalizerinfo 11666

输出:

Attaching to process ID 11666, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 24.71-b01
Number of objects pending for finalization: 0

输出结果的含义为当前没有在等待执行finalizer方法的对象

jmap -heap 11666

输出堆的详细信息

Attaching to process ID 11666, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.25-b02
using thread-local object allocation.
Parallel GC with 4 thread(s)
Heap Configuration: //堆内存初始化配置
MinHeapFreeRatio = 0 //对应jvm启动参数-XX:MinHeapFreeRatio设置JVM堆最小空闲比率(default 40)
MaxHeapFreeRatio = 100 //对应jvm启动参数 -XX:MaxHeapFreeRatio设置JVM堆最大空闲比率(default 70)
MaxHeapSize= 1073741824 (1024.0MB) //对应jvm启动参数-XX:MaxHeapSize=设置JVM堆的最大大小
NewSize = 22020096 (21.0MB) //对应jvm启动参数-XX:NewSize=设置JVM堆的新生代的默认大小
MaxNewSize = 357564416 (341.0MB) //对应jvm启动参数-XX:MaxNewSize=设置JVM堆的新生代的最大大小
OldSize = 45088768 (43.0MB) //对应jvm启动参数-XX:OldSize=<value>:设置JVM堆的老年代的大小
NewRatio = 2 //对应jvm启动参数-XX:NewRatio=:新生代和老生代的大小比率
SurvivorRatio = 8 //对应jvm启动参数-XX:SurvivorRatio=设置新生代中Eden区与Survivor区的大小比值
MetaspaceSize = 21807104 (20.796875MB) // 元数据区大小
CompressedClassSpaceSize = 1073741824 (1024.0MB) //类压缩空间大小
MaxMetaspaceSize = 17592186044415 MB //元数据区最大大小
G1HeapRegionSize = 0 (0.0MB) //G1垃圾收集器每个Region大小
Heap Usage: //堆内存使用情况
PS Young Generation
Eden Space: //Eden区内存分布
capacity = 17825792 (17.0MB) //Eden区总容量
used = 12704088 (12.115562438964844MB) //Eden区已使用
free = 5121704 (4.884437561035156MB) //Eden区剩余容量
71.26801434685203% used //Eden区使用比率
From Space: //其中一个Survivor区的内存分布
capacity = 2097152 (2.0MB)
used = 1703936 (1.625MB)
free = 393216 (0.375MB)
81.25% used
To Space: //另一个Survivor区的内存分布
capacity = 2097152 (2.0MB)
used = 0 (0.0MB)
free = 2097152 (2.0MB)
0.0% used
PS Old Generation
capacity = 52428800 (50.0MB) //老年代容量
used = 28325712 (27.013504028320312MB) //老年代已使用
free = 24103088 (22.986495971679688MB) //老年代空闲
54.027008056640625% used //老年代使用比率
15884 interned Strings occupying 2075304 bytes.
jmap -histo:live 11666 | more

输出存活对象统计信息

jhat

jhat是用来分析jmap生成dump文件的命令,jhat内置了应用服务器,可以通过网页查看dump文件分析结果,jhat一 般是用在离线分析上。

命令格式

jhat [option][dumpfile]

option参数解释:

  • -stack false: 关闭对象分配调用堆栈的跟踪
  • -refs false: 关闭对象引用的跟踪
  • -port : HTTP服务器端口,默认是7000
  • -debug : debug级别
  • -version 分析报告版本

常用实例

jhat dump.bin

JVM常用工具

Jconsole 监控管理工具

Jconsole(Java Monitoring and Management Console)是从java5开始,在JDK中自带的java监控和管理控制台,用于 对JVM中内存,线程和类等的监控,是一个基于JMX(java management extensions)的GUI性能监测工具。jconsole 使用jvm的扩展机制获取并展示虚拟机中运行的应用程序的性能和资源消耗等信息。
直接在jdk/bin目录下点击jconsole.exe即可启动

内存监控

代码准备

import java.util.*;
public class JConsoleDemo {
static class OOMObject {
public byte[] placeholder = new byte[64 * 1024];
}
public static void fillHeap(int num) throws InterruptedException {
List<OOMObject> list = new ArrayList<OOMObject>();
for (int i = 0; i < num; i++) {
Thread.sleep(50);
list.add(new OOMObject());
}
System.gc();
}
public static void main(String[] args) throws Exception {
fillHeap(1000);
//System.gc();
Thread.sleep(10000);
}
}

编译运行JConsoleDemo类, 运行时设置的虚拟机参数为 -Xms100m -Xmx100m -XX:+UseSerialGC , 在%JAVA_HOME%\bin目录下, 启动jconsole.exe , 将自动搜索出本机运行的所有虚拟机进程, 这里我们选择 JConsoleDemo对应的进程2464。

启动后主界面如下:

概述”页签显示的是整个虚拟机主要运行数据的概览,其中包括“堆内存使用情况”、“线程”、“类”、“CPU使用情况”4 种信息的曲线图,这些曲线图是后面“内存” 、“线程”、 ‘类”页签的信息汇总,具体内容将在后面介绍。

在"内存"页签, 查看堆内存Eden区的运行趋势如下:

从图中详细信息可以看出, Eden区的内存大小为27.328KB, 所以折线图中显示每次到27Mb左右时系统就会进行一次 GC。当1000次循环结束后, 执行System.gc(), 柱状图中显示Eden区和Survivor区基本被清空, 但老年代的对应柱状图仍 保持峰值状态, 这是因为System.gc()是在fillHeap()方法内执行, 所以list对象在System.gc()执行时仍然是存活的( 处于 作用域之内、被引用)。如果将System.gc()移动到fillHeap()方法外执行, 如下柱状图所示, 则会回收包括老年代的所有 内存。

线程监控

查看CPU使用率及活锁阻塞线程

代码准备

package com.lagou.unit4;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Demo7_JConsole02 {
/**
* 线程死循环演示
*/
public static void createBusyThread() {
Thread thread = new Thread(new Runnable() {
public void run() {
while (true);
}
}, "testBusyThread");
System.out.println("启动testBusyThread 线程完毕..");
thread.start();
}
/**
* 线程锁等待演示
*/
public static void createLockThread(final Object lock) {
Thread thread = new Thread(new Runnable() {
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "testLockThread");
thread.start();
System.out.println("启动testLockThread 线程完毕..");
}
public static void main(String[] args) throws Exception {
System.out.println("main 线程..");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.println("redLine阻塞");
br.readLine();
createBusyThread();
System.out.println("redLine阻塞");
br.readLine();
Object obj = new Object();
createLockThread(obj);
System.out.println("main 线程结束..");
}
}

查看死锁线程

package com.lagou.unit4;
public class Demo7_JConsole03 {
/**
* 线程死锁等待演示
*/
static class SynAddRunalbe implements Runnable {
int a, b;
public SynAddRunalbe(int a, int b) {
this.a = a;
this.b = b;
}
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 SynAddRunalbe(1, 2)).start();
new Thread(new SynAddRunalbe(2, 1)).start();
}
}
}

编译运行, 在"线程"页签可查看"死锁"描述。这是因为1、2两个数值在Integer类的缓存常量池[-128, 127]范围内, 这样 当多次调用Integer.valueOf()方法时, 不会再每次都创建对象, 而是直接返回缓存常量池中的对象。所以上面两个线程 的同步代码块中实际上只创建了两个锁对象, 且在某一时刻互相持有对方的锁, 即"死锁"现象。

VisualVM 可视化优化工具

简介

VisualVM 是一个工具,它提供了一个可视界面,用于查看 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的基于 Java 技术的应用程序(Java 应用程序)的详细信息。VisualVM 对 Java Development Kit (JDK) 工具所检索的 JVM 软 件相关数据进行组织,并通过一种使您可以快速查看有关多个 Java 应用程序的数据的方式提供该信息。您可以查 看本地应用程序以及远程主机上运行的应用程序的相关数据。此外,还可以捕获有关 JVM 软件实例的数据,并将该 数据保存到本地系统,以供后期查看或与其他用户共享。

概述与插件安装

VisualVM基于NetBeans平台开发, 因此它一开始就具备了插件扩展的特性, 通过插件支持, VisualVM可以做许多事情, 例如: 显示虚拟机进程和进程的配置、环境信息(jps、jinfo)

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

在%JAVA_HOME%\bin目录下, 启动jvisualvm.exe进入主界面, 点击"工具"→"插件"→"可用插件"选项, 选择所需的插件 安装。

安装好插件后, 选择一个正在运行的java程序就可以查看程序监控的主界面了

堆转储快照

两种方式生成堆dump文件:

  • 在"应用程序"窗口中右键单击应用程序节点, 选择"堆 Dump"
  • 在"监视"页签中选择"堆 Dump"
分析程序性能

在Profiler页签中, 可以对程序运行期间方法级的CPU和内存进行分析, 这个操作会对程序运行性能有很大影响, 所以 一般不再生产环境使用。CPU分析将会统计每个方法的执行次数、执行耗时; 内存分析则会统计每个方法关联的对 象数及对象所占空间。