《深入理解Java虚拟机》笔记

《深入理解Java虚拟机》笔记

马草原 594 2021-07-14

《深入理解Java虚拟机》笔记

深入理解jvm

《深入理解Java虚拟机》是一本由周志明所著的经典Java书。该书详细地介绍了Java虚拟机(JVM)的内部工作原理,对于想要深入了解Java虚拟机及其运行时机制的软件开发者来说,是一本非常有价值的参考书。

jvmxmind

一、Java内存区域与内存溢出异常

1.运行时数据区(JVM内存模型)

jvm

  1. 程序计数器(PC 寄存器、计数器)
    程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过它主要实现跳转、循环、恢复线程等功能。
    在任何时刻,一个处理器内核只能运行一个线程,多线程是通过抢占 CPU,分配时间完成的。这时就需要有个标记,来标明线程执行到哪里,程序计数器便拥有这样的功能,所以,每个线程都已自己的程序计数器。
    可以理解为一个指针,指向方法区中的方法字节码(用来存储指向下一个指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
    倘若执行的是 native 方法,则程序计数器中为空

  2. Java 虚拟机栈(JVM Stacks)
    虚拟机栈也就是平常所称的栈内存,每个线程对应一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法在执行的同时都会创建一个栈帧,方法被执行时入栈,执行完后出栈。
    不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。

每个栈帧主要包含的内容如下:
局部变量表:存储着 java 基本数据类型(byte/boolean/char/int/long/double/float/short)以及对象的引用
注意:这里的基本数据类型指的是方法内的局部变量
局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。
操作数栈
动态连接
方法返回地址
虚拟机栈可能会抛出两种异常:
栈溢出(StackOverFlowError):
若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常
内存溢出(OutOfMemoryError):
若虚拟机栈的容量允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OOM 异常

  1. 本地方法栈(Native Method Stacks)
    本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。
    本地方法栈与虚拟机栈的作用是相似的,都是线程私有的,只不过本地方法栈是描述本地方法运行过程的内存模型。
    本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。

虚拟机栈和本地方法栈的主要区别:

  • 虚拟机栈执行的是 java 方法
  • 本地方法栈执行的是 native 方法
  1. Java 堆(Java Heap)
    Java 堆中是 JVM 管理的最大一块内存空间。主要存放对象实例。
    Java 堆是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都存放在这里,是垃圾收集器管理的主要区域。

Java 堆的分区:

  • JDK1.8 之前,分为新生代、老年代、永久代
  • JDK1.8 及之后,只分为新生代、老年代
    永久代在 jdk1.8 已经被移除,被一个称为 “元数据区”(元空间)的区域所取代

Java 堆内存大小:
堆内存大小 = 新生代 + 老年代(新生代占堆空间的1/3、老年代占堆空间2/3)
既可以是固定大小的,也可以是可扩展的(通过参数 -Xmx 和 -Xms 设定)
如果堆无法扩展或者无法分配内存时报 OOM

主要存储的内容是:

  • 对象实例
  • 类初始化生成的对象
  • 基本数据类型的数组也是对象实例
  • 字符串常量池
  • 字符串常量池原本存放在方法区,JDK1.8 开始放置于堆中,字符串常量池存储的是 String 对象的直接引用,而不是直接存放的对象,是一张 String table
  • 静态变量:static 修饰的静态变量,jdk8 时从方法区迁移至堆中
  • 线程分配缓冲区(Thread Local Allocation Buffer)
    线程私有,但是不影响 java 堆的共性,增加线程分配缓冲区是为了提升对象分配时的效率

堆和栈的区别:

  • 管理方式,堆需要GC,栈自动释放
  • 大小不同,堆比栈大
  • 碎片相关:栈产生的碎片远小于堆,因为GC不是实时的
  • 分配方式:栈支持静态分配内存和动态分配,堆只支持动态分配
  • 效率:栈的效率比堆高
  1. 方法区(逻辑上)
    方法区是 JVM 的一个规范,所有虚拟机必须要遵守的。常见的 JVM 虚拟机有 Hotspot 、 JRockit(Oracle)、J9(IBM),方法区逻辑上属于堆的一部分,但是为了与堆区分,通常又叫非堆区
    各个线程共享,主要用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。关闭 JVM 就会释放这个区域的内存。
Java8 以前是放在 JVM 内存中的,由堆空间中的永久代实现,受 JVM 内存大小参数限制
Java8 移除了永久代和方法区,引入了元空间

拓展:
JDK版本 方法区的实现 运行时常量池所在的位置
JDK6 PermGen space(永久代) PermGen space(永久代)
JDK7 PermGen space(永久代) Heap(堆)
JDK8 Metaspace(元空间) Heap(堆)

  1. 元空间(元数据区、Metaspace)
    元空间是 JDK1.8 及之后,HotSpot 虚拟机对方法区的新实现。
    元空间不在虚拟机中,而是直接用物理(本地)内存实现,不再受 JVM 内存大小参数限制,JVM 不会再出现方法区的内存溢出问题,但如果物理内存被占满了,元空间也会报 OOM

元空间和方法区不同的地方在于编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:

  • 类元信息(Class):类元信息在类编译期间放入元空间,里面放置了类的基本信息:版本、字段、方法、接口以及常量池表
  • 常量池表:主要存放了类编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中
  1. 运行时常量池(Runtime Constant Pool)
    运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些
    运行时常量池具备动态性,可以添加数据,比较多的使用就是 String 类的 intern() 方法

  2. 直接内存(Direct Memory)
    直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。常见于 NIO 操作时,用于数据缓冲区(比如 ByteBuffer 使用的就是直接内存)。分配、回收成本较高,但读写性能高。
    直接内存不受 JVM 内存回收管理(直接内存的分配和释放是 Java 会通过 UnSafe 对象来管理的),但是系统内存是有限的,物理内存不足时会报OOM。


2. 内存溢出异常 (OutOfMemoryError)

Java虚拟机在内存不足时抛出的异常。当JVM无法分配新的对象实例或调用栈需要更多内存空间时,就会发生OutOfMemoryError异常。

这种异常通常是由于以下几种情况引起的:
1. 堆内存溢出(Heap Space OutOfMemoryError):当Java堆内存不足以容纳新的对象实例时,就会抛出该异常。通常是因为应用程序创建了过多的对象,但垃圾回收器无法回收足够的空间来释放内存。
2. 栈内存溢出(Stack OverflowError):当应用程序的方法调用层次太深,导致栈空间不足时,就会抛出该异常。通常是由于递归调用过程中没有正确的终止条件,或者方法调用层次过多导致的。
3. 方法区内存溢出(Metaspace OutOfMemoryError):在Java 8及之后的版本中,常常出现该异常。当加载的类信息、常量、静态变量等元数据超过了方法区的限制,就会导致该异常。
4. 本地内存溢出(Native OutOfMemoryError):当通过JNI(Java Native Interface)调用本地方法,但本地代码中发生了内存泄漏或者申请的本地内存超过了限制时,就会抛出该异常。

处理OutOfMemoryError异常的一些解决方法:

  • 增加JVM的堆内存大小,通过-Xmx和-Xms参数调整最大堆和初始堆大小。
  • 检查代码中是否存在内存泄漏问题,确保对象在不再使用时能够被垃圾回收器正确回收。
  • 优化代码,减少不必要的对象创建和使用,尽量复用对象,避免过多的临时对象。
  • 调整递归调用的层次,确保栈空间的合理利用。
  • 对于方法区内存溢出,可以尝试增加方法区的大小,或者优化类的加载和卸载过程,减少元数据的占用。

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

Jvm的垃圾回收主要回收堆空间

堆空间结构图:
Java堆

新生代、MinorGC(Young GC)
新生代:主要是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。

新生代又分为 Eden、S0、S1(SurvivorFrom、SurvivorTo)三个区:

  • Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
  • SurvivorFrom 区:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
  • SurvivorTo 区:保留了一次 MinorGC 过程中的幸存者。

Eden 和 S0,S1 区的比例为 8 : 1 : 1
幸存者 S0,S1 区:复制之后发生交换,谁是空的,谁就是 SurvivorTo 区
JVM 每次只会使用 eden 和其中一块 survivor 来为对象服务,所以无论什么时候,都会有一块 survivor 是空的,因此新生代实际可用空间只有 90%
当 JVM 无法为新建对象分配内存空间的时候 (Eden 满了),Minor GC 被触发。因此新生代空间占用率越高,Minor GC 越频繁。

MinorGC 的过程(采用复制算法):
首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,一般是 15,则赋值到老年代区)同时把这些对象的年龄 + 1(如果 ServicorTo 不够位置了就放到老年区)然后,清空 Eden 和 ServicorFrom 中的对象;最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom 区。

Minor GC 触发机制:
当年轻代满(指的是 Eden 满,Survivor 满不会引发 GC)时就会触发 Minor GC(通过复制算法回收垃圾)
对象年龄(Age)计数器:虚拟机给每个对象定义了一个对象年龄(Age)计数器。

如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold (阈值) 来设置。

老年代、MajorGC(Old GC)
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

MajorGC 采用标记-清除算法:

  • 首先扫描一次所有老年代,标记出存活的对象
  • 然后回收没有标记的对象。

MajorGC 的耗时比较长(速度一般会比 Minor GC 慢10倍以上,STW 的时间更长),因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,一般需要进行合并或者标记出来方便下次直接分配。
当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

永久代、元数据区(元空间)、常量池
是 JDK7 及之前, HotSpot 虚拟机基于 JVM 规范对方法区的一个落地实现,其他虚拟机如 JRockit(Oracle)、J9(IBM) 有方法区 ,但是没有永久代。在 JDK1.8 已经被移除,取而代之的是元数据区(元空间)

内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域。和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。
所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

元数据区(元空间、Metaspace)
元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

  • -XX:MetaspaceSize (初始空间大小):达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整,如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过 MaxMetaspaceSize时,适当提高该值。
  • -XX:MaxMetaspaceSize(最大空间)默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

  • -XX:MinMetaspaceFreeRatio :在 GC 之后,最小的 Metaspace 剩余空间容量的百分比,减少为分配空间所导致的垃圾收集;
  • -XX:MaxMetaspaceFreeRatio :在GC之后,最大的 Metaspace 剩余空间容量的百分比,减少为释放空间所导致的垃圾收集;

类的元数据放入本地内存中,字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由虚拟机的 MaxPermSize 控制,而由系统的实际可用空间来控制。

元空间替换永久代的原因分析:

字符串存在永久代中,容易出现性能问题和内存溢出。通常会使用 PermSize 和 MaxPermSize 设置永久代的大小就决定了永久代的上限,但是类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。当使用元空间时,可以加载多少类的元数据就不再由 MaxPermSize 控制,而由系统的实际可用空间来控制。

永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
Oracle 可能会将HotSpot 与 JRockit 合二为一。

类常量池、运行时常量池、字符串常量池

  • 类常量池:在类编译过程中,会把类元信息存放到元空间(方法区),类元信息其中一部分便是类常量池,主要存放字面量(字面量一部分便是文本字符)和符号引用

  • 运行时常量池:在类加载时,会将字面量和符号引用解析为直接引用存储在运行时常量池

文本字符会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池
在 JDK6,运行时常量池 存在于 方法区
在 JDK7,运行时常量池 存在于 Java 堆

  • 字符串常量池:存储的是字符串对象的引用,而不是字符串本身

字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中(JDK8 时,方法区就是元空间)

  • 字面量:Java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示:
int a=1; // 这个1便是字面量
String b="iloveu"; // iloveu便是字面量
  • 符号引用:由于在编译过程中并不知道每个类的地址,因为可能这个类还未加载,所以如果在一个类中引用了另一个类,被引用的类的全限定类名会作为符号引用,在类加载完后用这个符号引用去获取它的内存地址。

比如:com.javabc.Solution 类中引用了 com.javabc.Quest,那么 com.javabc.Quest 作为符号引用就会存到类常量池,等类加载完后,就可以拿着这个引用去元空间找此类的内存地址

Minor GC、Major GC、Full GC 的区别

  • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
  • 老年代收集(Major GC/Old GC ):只是老年代的垃圾收集
  • 整堆收集(Full GC):收集整个 java 堆(young gen + old gen)和方法区的垃圾收集

Full GC 触发机制:

  • 调用 System.gc 时,系统建议执行 Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存
  • 由 Eden 区、survivor space1(From Space)区向 survivor space2(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
  • 当永久代满时也会引发 Full GC,会导致 Class、Method 元信息的卸载

堆空间分为新生代和老年代的原因:

根据对象存活的时间,有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。

新生代分为了 eden、Survivor 区的原因:

为了更好的管理堆内存中的对象,方便GC算法(复制算法)来进行垃圾回收。
如果没有 Survivor 区,那么 Eden 每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发 Full GC,而 Full GC 是非常耗时的。
将 Eden 区满了的对象,添加到 Survivor 区,等对象反复清理几遍之后都没清理掉,再放到老年区,这样老年区的压力就会小很多。即 Survivor 相当于一个筛子,筛掉生命周期短的,将生命周期长的放到老年代区,减少老年代被清理的次数。

新生代的 Survivor 区又分为 S0 和 S1 区的原因:

分两个区的好处就是解决内存碎片化。

为什么一个 Survivor 区不行?
假设现在只有一个survivor区,模拟一下流程:
新建的对象在 Eden 中,一旦 Eden 满了,触发一次 Minor GC,Eden 中的存活对象就会被移动到 Survivor 区。这样继续循环下去,下一次 Eden 满了的时候,问题来了,此时进行 Minor GC,Eden和 Survivor 各有一些存活对象,如果此时把 Eden 区的存活对象硬放到 Survivor 区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。

GC 优化的本质,也是为什么分代的原因:减少GC次数和GC时间,避免全区扫描。

对象不一定就存在堆中(逃逸分析):
如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样无需在堆上分配内存。也无须进行垃圾回收了。

逃逸分析概述: 一种可以有效减少 Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

逃逸分析的基本行为就是分析对象动态作用域:

  • 当一个对象在方法中被定义后,对象只在方法内部引用,则认为没有发生逃逸
  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。


GC 常用算法

  • 分代收集算法(现在的虚拟机垃圾收集大多采用这种方式):
    它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。
    新生代中,由于对象生存期短,每次回收都会有大量对象死去,所以使用的是复制算法。
    老年代里的对象存活率较高,没有额外的空间进行分配担保,所以使用的是标记-整理 或者 标记-清除。

  • 标记-清除算法:
    每个对象都会存储一个标记位,记录对象的状态(活着或是死亡)。
    标记-清除算法分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。优点是可以避免内存碎片。

  • 标记-压缩(标记-整理)算法:
    标记-压缩法是标记-清除法的一个改进版,和标记清除算法基本相同。不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩(整理),然后把剩下的所有对象全部清除,这样就可以解决内存碎片问题。

  • 复制算法:
    复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。当有效内存空间耗尽时,JVM 将暂停程序运行,开启复制算法 GC 线程。接下来 GC 线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC 线程将更新存活对象的内存引用地址指向新的内存地址。此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。复制算法不会产生内存碎片。

垃圾收集器

  • Serial收集器:

    • 优点:简单高效,单线程执行,适用于单核或较小规模的应用。
    • 缺点:垃圾回收时会暂停所有应用线程。
    • 回收算法:标记-压缩算法。
    • 适用代:新生代。
    • 适用场景:客户端应用、小型应用。
  • Parallel收集器:

    • 优点:多线程执行,适用于多核处理器和大规模应用,减少垃圾回收暂停时间。
    • 缺点:垃圾回收时会暂停所有应用线程。
    • 回收算法:标记-复制算法。
    • 适用代:新生代。
    • 适用场景:服务器应用、多核处理器环境。
  • CMS收集器:

    • 优点:并发执行,减少垃圾回收暂停时间,适用于对响应时间要求较高的应用。
    • 缺点:并发执行可能会降低应用的吞吐量,无法处理碎片化问题。
    • 回收算法:标记-清除算法。
    • 适用代:老年代。
    • 适用场景:Web服务器、中小型应用。
  • G1收集器:

    • 优点:并发执行,适用于大堆内存和较大的Java堆,可以控制暂停时间。
    • 缺点:与CMS相比,吞吐量略低。
    • 回收算法:标记-复制算法。
    • 适用代:新生代和老年代。
    • 适用场景:大内存应用、需要低停顿时间的应用。
  • ZGC收集器:

    • 优点:低延迟,几乎不会暂停应用线程,适用于大堆内存和超大规模应用。
    • 缺点:吞吐量较低。
    • 回收算法:标记-整理算法。
    • 适用代:新生代和老年代。
    • 适用场景:超大规模应用、对低延迟有严格要求的应用。
  • Shenandoah收集器:

    • 优点:低延迟,几乎不会暂停应用线程,适用于大堆内存和超大规模应用。
    • 缺点:较复杂,与G1相比吞吐量较低。
    • 回收算法:标记-压缩算法。
    • 适用代:新生代和老年代。
    • 适用场景:超大规模应用、对低延迟有严格要求的应用。

三、虚拟机性能监控与故障处理工具

JDK命令行工具

  1. jps:进程状况工具

jps是Java虚拟机(JVM)进程状况工具,它是JDK中自带的一个命令行工具。jps可以用于快速查看正在运行的Java进程及其相关信息,方便开发者进行进程监控和调试。

  • 查看Java进程列表:运行jps命令,可以列出当前系统中正在运行的所有Java进程的进程ID和主类名。

  • 查看Java进程详细信息:使用jps -l命令,可以查看Java进程的进程ID和完整的主类名,包括包名。

  • 查看Java进程的JVM参数:通过jps -m命令,可以查看Java进程启动时的JVM参数,包括-Xmx、-Xms等设置。

  • 查看Java进程的JVM参数和传递给main方法的参数:使用jps -mlv命令,可以查看Java进程的JVM参数、传递给main方法的参数,以及Java进程的进程ID和完整的主类名。

  • 查看Java进程的JVM启动命令:通过jps -v命令,可以查看Java进程启动时的完整命令行参数,包括JVM参数和传递给main方法的参数。

  • 查看Java进程的系统属性:使用jps -q -m -l命令,可以查看Java进程的系统属性,包括JVM参数、传递给main方法的参数和系统属性。


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

jstat是Java虚拟机(JVM)统计信息监视工具,它也是JDK中自带的一个命令行工具。jstat可以用于监视JVM的各种运行时统计信息,包括堆内存、垃圾回收、类加载、线程等方面的数据。通过jstat命令,可以实时查看这些统计信息,帮助开发者进行性能分析、优化和故障排查。

  • 查看垃圾回收统计信息:使用jstat -gc <PID> <间隔时间(ms)>命令,可以查看指定Java进程的垃圾回收统计信息。该命令会显示堆内存的使用情况,包括Eden区、Survivor区、老年代的使用情况,以及垃圾回收的次数和耗时等。

  • 查看类加载统计信息:通过jstat -class <PID> <间隔时间(ms)>命令,可以查看指定Java进程的类加载统计信息。该命令会显示已加载的类数量、已卸载的类数量等。

  • 查看线程统计信息:使用jstat -gcutil <PID> <间隔时间(ms)>命令,可以查看指定Java进程的线程统计信息。该命令会显示各个垃圾回收器线程的CPU占用率和垃圾回收的执行情况。

  • 查看编译统计信息:通过jstat -compiler <PID> <间隔时间(ms)>命令,可以查看指定Java进程的编译统计信息。该命令会显示JIT编译器的编译任务数量、编译成功率等。

  • 查看JIT编译器统计信息:使用jstat -printcompilation <PID> <间隔时间(ms)>命令,可以查看指定Java进程的JIT编译器统计信息。该命令会显示JIT编译器的编译任务、编译时间、编译方法等。

  • jstat工具提供了丰富的运行时统计信息,可以帮助开发者了解Java虚拟机的运行状态,进行性能分析和优化。通过实时监控,开发者可以及时发现潜在的性能问题,并进行相应的调整和优化。


  1. jinfo:Java配置信息工具

jinfo是Java虚拟机(JVM)配置信息工具,它也是JDK中自带的一个命令行工具。jinfo可以用于查看和修改正在运行的Java进程的Java虚拟机配置信息,包括系统属性、JVM参数、环境变量等。通过jinfo命令,可以方便地查看Java进程的配置信息,以及进行一些简单的配置调整。

  • 查看Java进程的配置信息:使用jinfo <PID>命令,可以查看指定Java进程的Java虚拟机配置信息。该命令会显示Java进程的JVM参数、系统属性、环境变量等。

  • 查看Java进程的JVM参数:通过jinfo -flags <PID>命令,可以查看指定Java进程的JVM参数,包括启动时设置的-Xmx、-Xms等参数。

  • 修改Java进程的JVM参数:使用jinfo -flag <参数名>=<参数值> <PID>命令,可以动态修改指定Java进程的JVM参数。注意:不是所有的JVM参数都支持动态修改,只有部分参数可以在运行时进行调整。

  • 查看Java进程的动态链接库信息:通过jinfo -sysprops <PID>命令,可以查看指定Java进程加载的动态链接库信息。

  • 查看Java进程的VM内部参数:使用jinfo -sysprops <PID>命令,可以查看指定Java进程的VM内部参数信息。


  1. jmap:Java内存映像工具

jmap是Java虚拟机(JVM)的内存映像工具,它也是JDK中自带的一个命令行工具。jmap用于生成Java进程的堆转储快照(heap dump)和获取Java进程的内存相关信息,方便开发者进行内存分析和故障排查。

  • 生成堆转储快照:使用jmap -dump:<选项> <PID> <文件名>命令,可以生成指定Java进程的堆转储快照,并保存到指定的文件中。堆转储快照是Java进程堆内存的快照,可以用于分析内存使用情况、查找内存泄漏等问题。

  • 查看堆内存使用情况:通过jmap -heap <PID>命令,可以查看指定Java进程的堆内存使用情况,包括堆内存大小、已使用内存、垃圾回收器信息等。

  • 查看类实例数量统计:使用jmap -histo <PID>命令,可以查看指定Java进程中各个类的实例数量统计信息,可以帮助开发者了解各个类的内存占用情况。

  • 查看共享对象统计:通过jmap -clstats <PID>命令,可以查看指定Java进程的共享对象统计信息,包括共享类加载器和共享对象的使用情况。

  • 查看内存映射信息:使用jmap -finalizerinfo <PID>命令,可以查看指定Java进程的内存映射信息,包括内存地址范围、映射文件路径等。


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

jhat是一个在JDK 6和JDK 7中提供的虚拟机堆转储快照分析工具,用于分析由jmap生成的堆转储快照文件(heap dump file)。但是,从JDK 9开始,jhat被宣布为已弃用并且从JDK中移除。

通过jmap命令生成堆转储快照文件:

jmap -dump:format=b,file=heapdump.hprof <PID>
其中,<PID>是Java进程的进程ID,heapdump.hprof是生成的堆转储快照文件名。

使用jhat命令加载堆转储快照文件:

jhat heapdump.hprof

jhat会解析堆转储快照文件,并启动一个本地HTTP服务器。
在浏览器中访问jhat的服务器地址,默认情况下是http://localhost:7000
然后,您可以使用浏览器查看和分析堆转储快照的内容。

jhat的服务器界面提供了许多功能,包括查看对象实例、查找内存泄漏、查看类加载信息、查看垃圾回收器信息等。通过这些功能,您可以深入了解Java应用程序的内存使用情况,帮助您分析内存问题和优化应用程序的性能。但是要注意jhat转储文件时非常消耗内存。


  1. jstack:Java堆栈跟踪工具

jstack是Java虚拟机(JVM)的堆栈跟踪工具,它也是JDK中自带的一个命令行工具。jstack用于查看Java进程的线程堆栈信息,它可以帮助开发者分析Java应用程序的线程状态和调用栈,用于性能分析、故障排查和死锁检测。

  • 查看Java进程的线程堆栈信息:运行jstack <PID>命令,可以查看指定Java进程的所有线程的堆栈信息。<PID>是Java进程的进程ID

  • 查看死锁信息:通过jstack -l <PID>命令,可以查看指定Java进程的线程堆栈信息,并且在输出中标记死锁情况。这对于发现和解决死锁问题非常有帮助。

  • 线程分析:jstack输出的线程堆栈信息可以用于分析Java应用程序的线程状态和运行情况,查找线程阻塞、等待和运行状态等。

  • 检测线程问题:通过分析线程堆栈信息,可以帮助发现线程执行慢、阻塞或死循环等问题,帮助进行性能优化和故障排查。

  • 监控线程状态:可以使用jstack命令定期输出Java进程的线程堆栈信息,从而监控Java应用程序的线程状态,了解应用程序在不同时间点的线程运行情况。

  • 查找CPU占用高的线程:通过jstack查看线程堆栈信息,可以找到CPU占用率高的线程,帮助定位性能问题。

在运行jstack命令时,Java进程会被暂停一小段时间,因为jstack需要获取线程信息和堆栈信息。在生产环境中,如果对性能造成较大影响,建议在低峰期或非生产环境中使用。同时,建议将jstack的输出结果保存到文件,方便后续的分析和查看。


JDK的可视化工具

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

Java监视与管理控制台(JConsole)是JDK自带的一个图形化监控和管理工具,它提供了对Java应用程序运行时的性能和资源使用情况进行实时监控和管理的能力。使用JConsole,开发者可以监控Java虚拟机的内存、线程、垃圾回收等信息,帮助进行性能分析、故障排查和优化调整。

  • 启动JConsole:JConsole是一个Java应用程序,可以在JDK的bin目录下找到它。可以通过在命令行中输入jconsole命令来启动JConsole。

  • 连接到Java进程:启动JConsole后,会弹出一个图形界面。在界面中,您可以选择要监控的Java进程。可以选择本地Java进程,也可以通过远程连接方式监控远程运行的Java进程。

  • 监控JVM的性能:在JConsole中,您可以查看Java进程的各种性能指标,如堆内存使用情况、线程数、垃圾回收统计信息、类加载信息等。可以通过不同的标签页切换查看不同的监控数据。

  • 分析堆转储快照:JConsole可以生成并分析Java堆转储快照。在JConsole的"内存"标签页中,您可以生成堆转储快照,并通过MAT(Eclipse Memory Analyzer Tool)进行进一步的内存分析。

  • 线程监控和分析:在JConsole的"线程"标签页中,您可以查看Java进程中的所有线程以及各个线程的状态、CPU使用情况等信息。这对于分析线程问题和死锁很有帮助。

  • MBean监控:JConsole可以查看Java应用程序中注册的MBean(管理Bean)以及它们的属性和操作。MBean可以帮助监控和管理Java应用程序的各个方面。


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

VisualVM是一款多合一的故障处理工具,它也是JDK中自带的一款图形化监控、分析和故障处理工具。VisualVM可以用于实时监控和分析Java应用程序的性能、内存使用情况以及线程状态等。

  • 多合一工具:VisualVM整合了多个工具和插件,包括JConsole、jstack、jmap、JVM监控和分析、内存分析、线程分析等。这使得VisualVM成为一个强大的多合一故障处理工具,可以满足各种故障排查和性能分析的需求。

  • 图形化界面:VisualVM提供了直观的图形界面,使得监控和分析Java应用程序变得更加易于操作。它可以显示各种监控数据和分析结果,包括内存使用情况、线程状态、垃圾回收情况、类加载情况等。

  • 实时监控:VisualVM可以实时监控Java应用程序的性能和运行状态。您可以查看实时的CPU占用率、堆内存使用情况、线程状态等信息。

  • 堆转储分析:VisualVM可以生成Java堆转储快照,并提供内存分析功能。您可以在VisualVM中对堆转储快照进行分析,查找内存泄漏和内存占用情况。

  • 线程分析:VisualVM提供了线程状态的监控和分析功能。您可以查看Java应用程序的所有线程,查找阻塞、等待、死锁等问题。

  • 插件支持:VisualVM支持通过插件来扩展功能。开发者可以编写自定义插件,满足特定的监控和分析需求。

四、虚拟机执行子系统

虚拟机执行子系统:

虚拟机执行子系统是JVM的核心组成部分,负责将Java字节码转换成可执行的机器码,并执行程序。执行子系统包括类加载器字节码解释器即时编译器(JIT编译器)以及执行引擎

  • 类加载器负责加载类的字节码到JVM中,分为启动类加载器、扩展类加载器和应用程序类加载器。
  • 字节码解释器逐条解释字节码并执行,效率较低,但跨平台性好。
  • 即时编译器将热点代码(经常执行的代码)编译成本地机器码,提高执行效率。
  • 执行引擎负责执行机器码,其中包括解释执行和编译执行两种模式。

类文件结构:

Java类文件是一种二进制文件,以.class为扩展名,包含了Java源代码编译后的字节码。
类文件结构由常量池访问标志类索引父类索引接口索引集合字段表方法表属性表组成。

  • 常量池存储了各种字面量和符号引用,为其他部分提供数据支持。
  • 访问标志包含了类或接口的修饰符信息,如public、final、abstract等。
  • 类索引、父类索引和接口索引集合指向其他类或接口,构成类的继承和实现关系。
  • 字段表描述了类中声明的成员变量。
  • 方法表描述了类中声明的方法。
  • 属性表用于存储额外的类、字段、方法的附加信息,例如源文件名、代码行号等。

虚拟机类加载机制