写点什么

读书笔记之《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》

作者:菜农曰
  • 2022 年 7 月 10 日
  • 本文字数:9811 字

    阅读完需:约 32 分钟

读书笔记之《深入理解Java虚拟机:JVM高级特性与最佳实践》

💡 学而不思则罔,思而不学则殆。 —— 孔子

👉 微信公众号已开启,菜农曰,没关注的同学们记得关注哦!

本篇带来的是周志明老师编写的《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》,十分硬核!

全书共分为 5 部分,围绕内存管理执行子系统程序编译与优化高效并发等核心主题对 JVM 进行了全面而深入的分析,深刻揭示了 JVM 工作原理。

全书整体 5 个部分,十三章,共 358929 字。整体结构相当清晰,以至于写读书笔记的时候无从摘抄(甚至想把全书复述一遍),以下是全书第二部分的内容,望读者细细品尝!

一、第一部分 走进 Java

第一部分介绍了关于 Java 的技术体系与发展史,谈及未来。该部分内容不做摘抄,直接进入核心主题。

二、第二部分 自动内存管理机制

Java 与 C++ 之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。

第二章 Java 内存区域与内存溢出异常

对于 Java 程序员来说是幸福的也是可悲的,在虚拟机自动内存管理机制的帮助下不需要为每一个 new 操作去写配对的 delete/ free 代码,不容易出现内存泄露内存溢出问题,但是在内存管理领域中,C 或 C++,既是拥有最高权力的 "皇帝" 又是从事最基础工作的 "劳动人民"。

1)运行时数据区域

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域.

  1. 程序计数器

这是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器

为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,线程私有****。

此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

1. 虚拟机栈

与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储 局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

2. 本地方法栈

与虚拟机栈十分相似,主要的区别在于虚拟机栈是为虚拟机执行 Java 方法服务,而本地方法栈是为虚拟机使用到的 Native 方法服务。

与虚拟机栈一样的是:本地方法栈也会抛出 StackOverFlowError 和 OutOfMemoryError 异常

3. Java 堆

Java 堆是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都会在这里进行内存分配。

Java 堆是垃圾收集器管理的主要区域,因此很多时候也会称为 GC 堆 ,因此还可以细分为:新生代和老年代,而新生代中又可以细致为 Eden 空间From Survivor 空间To Survivor 空间等。

4. 方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

永久代与方法区,本质上两者并不等价

HotSpot 虚拟机设计团队会利用 永久代 来实现方法区,这样 HotSpot 的垃圾收集器就可以像 Java 堆那样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。

Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小,还可以选择不实现垃圾收集。

并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是必要的

2)扩展说明

  1. 运行时常量池

运行时常量是方法区的一部分。用于存放 编译期生成的各种字面量和符号引用,这部分内存将在类加载后进入方法区的运行时常量池中存放。

运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备 动态性(并不要求常量一定只有编译期才能产生,运行期间也可以将新的常量存入池中)

运行时常量池是 方法区 的一部分,当常量池无法再申请到内存时也会抛出 OutOfMemoryError 异常。

  1. 直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但这部分也会导致 OutOfMemoryError 异常。

在 Java1.4 引入的 NIO,是一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,可以使用 Native 函数库直接分配 堆外内存,避免了在 Java 堆和 Native 堆中来回复制数据,可以在一些场景中显著提高性能。

直接内存的分配不会受到 Java 堆大小的限制,但还是会受到本机总内存大小以及处理器寻址空间的限制。

3)HotSpot 虚拟机对象探秘

  1. 对象的创建

众所周知,对象的创建通常是使用 new 关键字生成的

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的 符号引用,并且检查该符号引用是否已经被加载解析初始化(类加载的过程)。如果没有,则必须先执行相应的类加载过程。

在类加载检查通过后,虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。

分配内存仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配的方式称为 指针碰撞。(虚拟机必须维护一个列表,记录哪些内存块是可用的,在分配的时候就从列表中找到一块足够大的空间划分给对象实例,这种分配方式称为空闲列表

当然,创建对象还需要考虑 并发问题,有可能给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存。

解决该问题有两种方案:

  • 对分配内存空间的动作进行同步处理——实际上虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性

  • 把内存分配的动作按照线程划分在不同的空间之中,即每个线程在 Java 堆中预先分配一小块内存,称为 本地线程分配缓冲(TLAB)。

内存分配完毕后,虚拟机需要将分配到内存空间都初始化为零值(不包括对象头),如果使用 TLAB,这一工作过程也可以提前至 TLAB 分配时进行。

到此,一个新的对象产生了,但是由于还没有执行**** 方法,这个时候对象的所有字段都是 0。

  1. 对象的内存布局

对象内存中存储的布局可以分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头:

  • 用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32 bit 和 64bit,官方称为 Mark Word。

  • 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据:

是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。这部分存储顺序会受到虚拟机分配策略参数和字段在 Java 源码中定义顺序的影响。

对齐填充:

这部分并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

  1. 对象的访问定位

建立对象是为了使用对象,我们的 Java 程序需要通过栈上的 reference 数据来操作堆上的具体对象

目前主流的对象访问方式有两种

  • 句柄访问。Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。

  • 直接指针访问。Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的就是对象地址。

这两种对象访问方式各有优势:

  • 句柄访问:reference 中存储的最稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要改变。

  • 直接指针访问:速度更快,节省了一次指针定位的时间开销

4)OutOfMemoryError 异常

除了程序计数器之外的其他几个运行时区域都有发生 OOM 异常的可能。

  1. 堆内存溢出

Java 堆用于存储对象实例,只要不断创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制来清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生 内存溢出异常。

具体情况具体分析,可以借助内存映像分析工具(如 Eclipse Memory Analyzer)对 Dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

如果是内存泄露,那么需要找到泄露对象是通过怎样的路径与 GC Roots 相关联并导致垃圾收集器无法自动回收它们的。

如果是内存溢出,那就需要检查虚拟机的堆参数(-Xms 和 -Xmx),与机器物理内存对比看看是否还可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

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

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常

  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常

  1. 方法区和运行时常量池溢出

在运行时常量池溢出,那么在 OutOfMemoryError 后面跟随的提示信息是 PermGen space。

方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。

在经常动态生成大量 Class 的应用中,需要特别注意类的回收状况。

这类场景除了使用了 CGLib 字节码增强和动态语言之外,常见的还有:大量 JSP 或动态产生 JSP 文件的应

用(JSP 第一次运行时需要编译为 Java 类)、基于 OSGi 的应用(即使是同一个类文件,被不同的加载器

加载也会视为不同的类)等。

  1. 本机直接内存溢出

DirectMemory 容量可以通过 -XX: MaxDirectMemorySize 指定。如果不指定,则默认与 Java 堆最大值(-Xmx)一样。

在 DirectByteBuffer 分配内存时也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算机得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是:unsafe.allocateMemory()

由 DirectMemory 导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显的异常,如果读者发现 OOM 之后 Dump 的文件很小,而程序中又直接或间接使用了 NIO,那就可以考虑检查是不是这方面的问题

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

1)对象已死吗

在垃圾收集器工作之前需要确定这些对象中哪些是 存活 的,哪些已经 死去

  1. 引用计数法

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

缺陷:两个无用对象出现相互引用而无法回收。

  1. 可达性分析法

定义:通过一系列称为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为 引用链 ,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

可作为 GC Roots 对象:

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

  • 方法区种类静态属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中 JNI(Native 方法)引用的对象

  1. 引用种类

JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为 强引用、软引用、弱引用、虚引用 4 种

  • 强引用:普遍存在的引用(通过 new 方式),只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象

  • 软引用:一些还有用但非必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常

  • 弱引用:强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否够用,都会回收掉只被弱引用关联的对象

  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是在这个对象被收集器回收时收到一个系统通知

  1. 生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是 非死不可

宣告一个对象死亡,至少要经过两次标记过程:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize()方法,当对象没有覆盖 finalize()方法,或已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。

如果这个对象判定为有必要执行 finalize() 方法,那么这个对象会被放置在一个叫做 F-Queue 的队列中,并在稍后由一个虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在**finalize()**中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this 关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合。

注意:任何一个对象的 finalize() 方法都只会被系统自动调用一次,如果对象面临下一次回收,它的 finallize() 方法不会被再次执行。

  1. 回收方法区

Java 虚拟机规范中对方法区的要求比较宽松,可以不要求虚拟机在方法区中实现垃圾回收,但并不表示方法区没有垃圾回收。

永久代的垃圾收集主要回收两部分内容: 废弃常量 和 无用的类。

类需要同时满足下面 3 个条件才算是 无用的类:

  • 该类的所有实例都已经被回收,也就是 Java 堆中不存在该类的任何实例

  • 加载该类的 ClassLoader 已经被回收

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

2)垃圾收集算法

  1. 标记 - 清除算法

标记 - 清除算法是最基础的收集算法。算法分为 标记清除 两个阶段。

不足之处:效率问题空间问题

  1. 复制算法

该算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。

不足之处:将内存缩小为了原来的一半,代价有点高了

现在商业虚拟机都采用这种收集算法来回收新生代。将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。(默认 Eden 和 Survivor 的大小比例是 8∶1)

  1. 标记-整理算法

该算法分为 标记整理 算法,标记过程仍然与 标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  1. 分代收集算法

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

3)HotSpot 的算法实现

  1. 枚举根节点

可达性分析对执行时间的敏感还体现在 GC 停顿上,因为这项分析工作必须在一个能确保一致性的快照中

进行——这里“一致性”的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不

可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保

证。这点是导致 GC 进行时必须停顿所有 Java 执行线程(Sun 将这件事情称为“Stop The World”)的其

中一个重要原因,即使是在号称(几乎)不会发生停顿的 CMS 收集器中,枚举根节点时也是必须要停顿的。

虚拟机应当是有办法直接得知哪些地方存放着对象引用。在 HotSpot 的实现中,是使用一组称 OopMap

的数据结构来达到这个目的。

  1. 安全点

在 OopMap 的协助下,HotSpot 可以快速且准确地完成 GC Roots 枚举,但如果为每一条指令都生成对应

的 OopMap,那将会需要大量的额外空间,这样 GC 的空间成本将会变得很高。

因此 HotSpot 会在 特定的位置 记录这些信息,这些位置称为 安全点 程序执行时并非在所有地方都能停顿下来开始 GC ,只有在到达安全点才能暂停。

安全点的选定基本上是以是否具备让程序长时间执行的特征选定的。

长时间执行最明显的特征就是指令序列复用(方法调用、循环跳转、异常跳转等)

在 GC 发生时要让所有线程都跑到安全点上再停顿下来有两种方式:

  • 抢先式中断:不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程都中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。

  • 主动式中断:当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。

  1. 安全区域

安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint。

但是在程序不执行的时候(线程 Sleep 或 Blocked 状态)这时候线程就无法响应 JVM 的中断请求,这个时候就需要 安全区域 来解决。安全区域是扩展了的 Safepoint。

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

4)垃圾收集器

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

两个概念:

  • 并行(Parallel):指多条垃圾收集线程并行工作,此时用户线程仍然处于等待状态

  • 并发(Concurrent):用户线程与垃圾收集线程同时执行(不一定是并行的,也可以是交替执行),用户程序继续运行,而垃圾收集程序则运行在另一个 CPU 上

在 HotSpot 中大致存在着作用于不同分代的收集器,如果两个收集器之间存在连线,说明可以搭配使用。

  1. Serial 收集器

这个收集器是一个单线程的收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

  1. ParNew 收集器

这个收集器就是 Serial 收集器的多线程版本(也就是说除了多条线程一起处理之外,其余行为与 Serial 收集器一致)。除了 Serial 收集器之外,只有 ParNew 才能与 CMS 配合使用

  1. Parallel Scavenge 收集器

该收集器也是使用 复制算法。该收集器与其他收集器的关注点不同 - 吞吐量优先收集器

  • CMS 等收集器的关注点在于 尽可能地缩短垃圾收集时用户线程的停顿时间

  • Parallel Scanvenge 则 尽可能达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 /(运行用户代码时间 +垃圾收集时间)

Parallel Scavenge 收集器无法与 CMS 收集器配合工作,如果新生代选择了 Parallel Scavenge 收集器,老年代只能选择 Serial Old 收集器

  1. Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。

  1. Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法

  1. CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

CMS 收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为 4 个步骤,包括:

  • 初始标记(需 Stop The World)

  • 并发标记

  • 重新标记(需 Stop The World)

  • 并发清除

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

CMS 收集器明显的 3 个缺陷:

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

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

  • CMS 收集器会产生大量的内存碎片

浮动垃圾:由于 CMS 并发清理阶段用户线程还在运行这,伴随程序运行自然就还有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理,只好在下次 GC 时在清理掉。

  1. G1 收集器

G1 是一款面向服务端应用的垃圾收集器。具备以下特点:

  • 并行与并发

  • 分代收集

  • 空间整合:G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存

  • 可预测停顿:G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒

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

  • 初始标记

  • 并发标记

  • 最终标记(需要 Stop The World)

  • 筛选回收

5)内存分配与回收策略

  1. 对象优先在 Eden 分配

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

Minor GC 和 Full GC 的区别:

  • 新生代 GC(Minor GC):发生在新生代的垃圾收集动作,因为 Java 对象太多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快

  • 老年代 GC(Major GC/ Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scanvenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。Major GC 的速度一般会比 Minor GC 慢 10 倍以上

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

所谓的大对象是指,需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组

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

在虚拟机中会为每个对象定义一个对象年龄(Age)计数器。对象每熬过一次 Minor GC 年龄就增加 1 岁,当它年龄增加到一定程度(默认 15 岁),就会进入老年代。

  1. 动态对象年龄判定

如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

  1. 空间分配担保

在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,则可以确保 Minor GC 是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。

第四章 虚拟机性能监控与故障处理工具

1)JDK 的命令行工具

这些工具都异常小巧,因为这些命令行工具大多数是 jdk/lib/tools.jar 类库的一层薄包装,主要功能代码是在 tools 类库中实现的。

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

功能:列出正在运行的虚拟机进程,并显示虚拟机执行主类(main()函数所在的类)名称以及这些进程的本地虚拟机唯一 ID

主要选项:

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

功能:用于监视虚拟机各种运行状态信息的命令行工具。

主要选项:

  1. jinfo:Java 配置信息工具

功能:实时地查看和调整虚拟机各项参数

  1. jmap:Java 内存映像工具

功能:用于生成堆转储快照(heapdump 或 dump 文件)。还可以查询 finalize 执行队列,Java 堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等

主要选项:

  1. jhat:虚拟机堆转储快照分析工具

功能:与 jmap 搭配使用,来分析 jmap 生成的堆转储快照。

  1. jstack:Java 堆栈跟踪工具

功能:用于生成虚拟机当前时刻的线程快照(threaddump 或 javacore 文件)

线程快照就是当前虚拟机中每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。

主要选项:

2)JDK 的可视化工具

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

基于 JMX 的可视化监视、管理工具。它管理部分的功能是针对 JMX MBean 进行管理。

  1. VisualVM:多合一故障处理工具

是目前为止随 JDK 发布的功能最强大的运行监视和故障处理程序。处理运行监视、故障处理外,还提供了性能分析。


这篇咱们主要是针对 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》 第二部分做了相关的读书笔记,一方面是因为篇幅原因,接下来会针对每部分分别发布。另一方面是为了让读者有充足的时间进行消化,毕竟一口吃不成大胖子 ~ 请读者慢慢阅读,静待下部分读书笔记的出炉!👨💻

不要空谈,不要贪懒,和小菜一起做个 吹着牛 X 做架构 的程序猿吧~点个关注做个伴,让小菜不再孤单。咱们下文见!

👀 今天的你多努力一点,明天的你就能少说一句求人的话!

👉🏻 微信公众号:菜农曰,没关注的同学们记得关注哦!

发布于: 刚刚阅读数: 3
用户头像

菜农曰

关注

还未添加个人签名 2021.11.24 加入

还未添加个人简介

评论

发布
暂无评论
读书笔记之《深入理解Java虚拟机:JVM高级特性与最佳实践》_Java_菜农曰_InfoQ写作社区