深入探索 JVM:原理、机制与实战
一、JVM 概述
JVM(Java Virtual Machine)是 Java 程序运行的核心组件,它提供了一个独立于硬件和操作系统的执行环境,使得 Java 程序能够在不同平台上具有跨平台的特性。
JVM 主要由以下几部分组成:
- 类装载器(Class Loader):负责从文件系统或者网络加载 Java 类,转换成 Java 字节码,然后加载到运行时数据区。
- 运行时数据区:这是 Java 虚拟机执行 Java 程序时使用的主要内存空间,主要包括以下几部分:
- 方法区(Method Area):存储已被加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
- 堆(Heap):这是 JVM 所管理的最大的一块内存空间,几乎所有的对象实例都在这里分配内存。
- Java 栈(Java Stacks):每个线程都有一个私有的 Java 栈,用于存储局部变量,操作数栈,动态链接和方法出口等信息。
- 本地方法栈(Native Method Stacks):对于执行 Native 方法服务的栈,每个线程都会有一个对应的本地方法栈。
- 程序计数器(Program Counter Register):它是当前线程所执行的字节码的行号指示器。
- 执行引擎(Execution Engine):负责执行字节码,主要包括解释器、即时编译器(JIT)以及垃圾回收器。
- 本地接口库(Native Interface):Java Native Interface,可以支持 Java 调用其他语言的程序。
- 本地方法库(Native Method Library):存储了所有的本地方法,由 JVM 调用。
Java 虚拟机屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java 虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
Java 语言的跨平台特性是由 JVM 实现的,我们编写的程序运行在 JVM 上,JVM 运行在操作系统上。Java 编译将.java 文件转化成.class 文件,.class 文件中并不是机器码,而是字节码。这种字节码是不依赖于计算机硬件架构而存在的,不管是什么操作系统,只要有对应的 JVM,字节码就可以在任何平台运行,真正实现了一次编译,到处运行。
JVM 的整体结构架构模型有基于栈式和基于寄存器两种。基于栈式的指令集架构设计和实现更简单,适用于资源受限的系统,避开了寄存器的分配难题,不需要硬件支持,可移植性更好,更好实现跨平台;基于寄存器的指令集架构则完全依赖硬件,可移植性差,但性能优秀和执行更高效,花费更少的指令去完成一项操作。由于 Java 要支持跨平台,所以 Java 的指令都是针对栈来设计的。不同平台 CPU 架构不同所以不能设计成基于寄存器的。优点是跨平台指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
二、JVM 的组成部分
1. Class Loader(类加载器)
类加载器是 Java 运行时环境(JRE)的一部分,负责在运行时动态地加载 Java 类到 Java 虚拟机(JVM)中。其主要作用包括加载类、链接类以及初始化类。具体来说,它会根据类的全名找到对应的.class文件,并将其加载到 JVM 中,然后进行链接操作,包括验证、准备和解析,最后为类的静态变量赋予正确的初始值完成初始化。
Java 中有三种主要的类加载器:启动类加载器负责加载 Java 的核心类库;扩展类加载器负责加载 Java 的扩展类库;系统类加载器负责加载应用程序的类路径下的所有类。此外,开发者还可以自定义类加载器,以满足特殊需求,如热部署、代码加密等。
Java 的类加载器采用双亲委派模型,当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。这种模型保证了 Java 核心类库的类型安全,避免了类的重复加载,并且使得 Java 应用更加稳定。
类加载器的隔离性确保了不同应用程序或库之间的类不会相互干扰,从而避免了潜在的类冲突和不安全行为。例如,两个不同的应用程序可能都使用了一个名为com.example.Utils的类,但这两个类实际上可能是完全不同的。通过为每个应用程序使用不同的类加载器,可以确保每个应用程序加载和使用它自己的com.example.Utils类版本,而不会与其他应用程序的类发生冲突。
自定义类加载器允许开发者扩展 Java 的类加载机制,以满足特定的需求。通过继承ClassLoader类并重写其中的方法,开发者可以控制类的加载过程,实现如加密类的加载、从特定位置(如数据库或网络)加载类等高级功能。
2. Execution Engine(执行引擎)
执行引擎是 Java 虚拟机核心的组成部分之一,它的任务是将字节码指令解释或编译为对应平台上的本地机器指令才可以执行。可以把执行引擎看作是将高级语言翻译为机器语言的译者。
执行引擎在执行的过程中,其行为有解释执行和编译执行两种。解释执行是当 Java 虚拟机启动时,根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容 “翻译” 为对应平台的本地机器指令执行。而编译执行主要是通过 JIT(Just In Time Compiler)编译器实现,虚拟机将源代码一次性直接编译成和本地机器平台相关的机器语言,但并不是马上执行。
Java 属于半编译半解释型语言,JDK1.0 时代,Java 语言定位为 “解释执行”,后来发展出可以直接生成本地代码的编译器。现在 JVM 在执行 Java 代码的时候,通常都会将解释执行与编译执行二者结合起来进行。JIT 编译器将字节码翻译成本地代码后,可以做一个缓存操作,存储在方法区的 JIT 代码缓存中,并且在翻译成本地代码的过程中可以做优化。
3. Native Interface(本地接口)
本地接口是 Java 虚拟机(JVM)提供给开发者的一种机制,用于在 Java 程序中调用本地方法。本地接口允许 Java 代码与底层的本地代码进行交互,使得 Java 程序可以调用 C、C++ 等本地语言编写的函数库或操作系统提供的功能。
本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++ 程序。本地方法栈中登记 native 方法,在执行引擎执行时加载本地方法库。有声明,无实现。
本地接口允许 Java 程序调用本地方法,从而实现与底层本地代码的交互。Java 程序可以通过本地接口调用本地方法,进而调用本地函数库提供的功能,如操作系统的系统调用、硬件设备的访问等。本地接口为 Java 程序提供了与底层系统的无缝连接,极大地扩展了 Java 的应用范围。
4. Runtime data area(运行数据区)
运行时数据区是整个 JVM 的重点,所有写的程序都被加载到这里,之后才开始运行。它是 JVM 在执行 Java 程序时,用于存储和管理各种数据的内存区域,包括方法区、堆、虚拟机栈、本地方法栈和程序计数器等五大区域。
方法区存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,在 JDK 1.8 及以后版本中称为元空间。堆是所有线程共享的一块内存区域,主要用于分配和回收 Java 对象实例,几乎所有的对象都在堆上分配内存,并且是垃圾回收的主要区域。虚拟机栈是每个线程私有的,用于存储方法调用和局部变量,描述的是 Java 方法执行的内存模型。本地方法栈与虚拟机栈的作用类似,是为虚拟机调用 Native 方法服务的。程序计数器是当前线程所执行的字节码的行号指示器,是线程私有的,用于记录当前线程执行的字节码指令地址,是唯一一个不会出现内存溢出错误的区域。
三、JVM 的内存管理
JVM 的内存管理是其核心功能之一,它直接影响着 Java 程序的性能和稳定性。以下将详细介绍 JVM 的内存管理机制。
1. 内存区域划分
内存区域 | 内存区域描述 | 是否线程私有 | 主要存储内容 |
---|---|---|---|
程序计数器 | 当前线程所执行的字节码的行号指示器,JVM 中唯一不会出现内存溢出错误的区域 | 是 | 作为行号指示器独立存储各线程执行位置,互不影响 |
虚拟机栈 | 每个方法被执行时 Java 虚拟机同步创建栈帧,包含局部变量表、操作数栈等信息,可能抛出 StackOverflowError 和 OutOfMemoryError | 是 | 局部变量表存放方法中的局部变量, 操作数栈用于字节码执行时的运算, 动态链接用于在运行时找到被调用方法的实际地址, 方法出口表示方法该如何结束 |
本地方法栈 | 作用与虚拟机栈类似,为 Native 方法服务,可能抛出 StackOverflowError 和 OutOfMemoryError | 是 | |
堆 | 整个 Java 应用程序共享的区域,用于存放和管理对象和数组,是垃圾回收的主要区域,可动态扩展,扩展失败抛出 OutOfMemoryError 异常,采用分代收集算法分为新生代和老年代等区域 | 否 | 对象、数组 |
方法区(JDK 1.8 后为元空间) | 存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,垃圾回收主要目标是对常量池回收和类卸载,较难实现,动态扩展失败抛出 OutOfMemoryError 异常,运行时常量池是其一部分用于存放编译器生成的各种字面量和符号引用 | 否 | 类信息、 常量、 静态变量、 即时编译器编译后的代码 |
2. 内存分配
- 对象在堆中的分配:当一个对象被创建时,它首先进入新生代的 Eden 区。如果 Eden 区满了,就会触发 Minor GC,将 Eden 区中仍然存活的对象复制到 Survivor 区中。如果 Survivor 区也满了,或者对象在经过多次 Minor GC 后仍然存活,就会被转移到老年代中。
- 栈内存分配:Java 栈的分配是和线程绑定在一起的,当创建一个线程时,JVM 就会为这个线程创建一个新的 Java 栈。一个线程的方法的调用和返回对应着这个 Java 栈的压栈和出栈。栈中主要存放一些基本的数据类型和对象句柄(引用)。局部变量表中存储着方法相关的局部变量,其内存空间可以在编译期间就确定,运行时不再改变。
- 直接内存分配:NIO 使用 java.nio.ByteBuffer.allocateDirect () 方法分配内存,是本机内存而不是 Java 堆上的内存,增加了一次系统调用。直接 ByteBuffer 产生的数据和网络或者磁盘交互都在操作系统的内核空间中发生,不需要将数据复制到 Java 内存中,加快数据处理速度。
3. 内存分配与回收策略
- 分代收集器对象优先在 Eden 分配:新创建的对象首先在新生代的 Eden 区分配内存。
- 大对象直接进入老年代:可以通过参数调配使得大对象直接进入老年代,避免在新生代频繁进行垃圾回收。
- 长期存活的对象将进入老年代:对象在新生代经过多次垃圾回收后仍然存活,会进入老年代。
- Minor GC 与对象年龄:每次对新生代的 GC(Minor GC),存活的对象通过标记 - 复制算法复制到 Survivor 空间,并且年龄 +1,如果年龄达到老年代要求(动态年龄算法),则进入老年代。如果 Survivor 空间无法存放存活的对象,则根据分配担保机制进入其他内存空间(实际上大多是实现都是进入老年代)。
- 空间分配担保:在 Minor GC 之前,检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果大于,就能保证这次 Minor GC 是安全的;如果小于,则查看 -XX:HandlePromotionFailure 参数是否允许担保失败。如果允许,继续检查老年代最大可用空间是否大于历次晋升到老年代对象的平均大小,大于则尝试进行一次 Minor GC,分配失败再进行一次 Full GC;小于则进行一次 Full GC。如果不允许担保失败,直接进行一次 Full GC。如果分配失败,再进行一次 Full GC。
四、垃圾收集器
1. 对象失效判定算法
对象失效判定是垃圾回收的关键步骤之一,主要包括引用计数算法和可达性分析算法,以及不同引用类型对象的回收方式。
引用计数算法:每有一处引用,引用计数器加 1,每有一处引用失效,引用计数器减 1,引用计数器归 0,则该对象可以被回收。这种算法简单高效,但面对复杂场景程序需要额外的工作以保证算法有效,例如解决循环引用的问题。如以下代码所示:
public class ObjA {private Object instance;public void setInstance(Object instance) {this.instance = instance;}
}
public class ObjB {private Object instance;public void setInstance(Object instance) {this.instance = instance;}
}
public class Test {public static void main(String[] args) {ObjA obja = new ObjA();ObjB objb = new ObjB();obja.setInstance(objb);objb.setInstance(obja);obja = null;objb = null;//循环引用,如果使用引用计算算法,则两个对象的引用计算器都为1,都将无法被gc回收。//而实际上两个对象已经没有其他引用了,永远也不会被程序调用。}
}
可达性分析算法:通过 GC_Roots 对象向下搜索引用形成引用链,如果对象没有与任何引用链相连,该对象可被回收。可以作为 GC_Roots 的对象类型有静态属性、字符串常量、被 Native 方法引用的对象、Java 虚拟机内部的引用、同步锁持有的对象、反映 Java 虚拟机内部情况的 JMXBean 等。
不同引用类型对象的回收:
- Strongly Reference(强引用):所有基本对象默认为强引用,强引用只要 GCRoots 可达即不能被回收。
- Soft Reference(软引用):被 java.lang.ref.SoftRefenrence 对象包装的对象即为软引用对象,当因为 JVM 堆内存空间不足时触发的 gc 会将软引用包装的对象回收。
- Weak Reference(弱引用):被 java.lang.ref.WeakRefenrence 对象包装的对象即为弱引用对象,在每一次 gc 行为发生时都将被回收。
- Phantom Reference(虚引用):被 java.lang.ref.PhantomReference 包装的对象即为虚引用对象,在构造时需要传入一个 ReferenceQueue 队列对象,可以通过该对象在对象被回收时通知系统。虚引用的 get 方法返回 null,不能通过虚引用获取其包装的对象,虚引用唯一的作用是用于对象回收的系统通知。虚引用在每一次 gc 行为发生时都将被回收。
如果在构造 SoftRefenrence/WeakRefenrence/PhantomReference 时传入一个引用队列参数,对象将被回收时都将存入该队列中。可以通过如下编码进行通知:
ReferenceQueue<RefObject> queue = new ReferenceQueue<>();
new Thread(() -> {while (true) {Reference<? extends RefObject> reference;if ((reference = queue.poll())!= null) {//需要注意,此时通过reference.get()返回的都将为null//也就是说此时对象已经被回收了System.out.println("对象是否被回收? " + reference.get() == null);System.out.printf("引用: %s 将被回收\n", reference);//DOING SOMETHING}}
}, "monitorGcThread").start();
2. 垃圾收集算法
垃圾收集算法主要有标记 - 清除算法、标记 - 复制算法、标记 - 整理算法等,以及分代收集理论。
分代收集理论基础假说:
- 弱分代假说:绝大多数对象都是朝生夕灭的。
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。
基于这些假说,垃圾收集器设计原则是将 Java 堆分出不同的区域,然后将回收对象依据其经历垃圾收集过程的次数高低分配到不同的区域之中存储。
标记 - 清除算法:标记清除算法是大部分垃圾回收算法的基础。首先标记出所有需要回收的对象,然后回收所有被标记的对象。主要缺点是执行效率不稳定,Java 堆中有太多对象时,执行效率越差;还会产生大量不连续的内存碎片。
标记 - 复制算法:适用于对象存活率较低的垃圾回收,如新生代。将内存分为两半,每次只使用其中一半。当使用中的一半内存耗尽,进行空间清理,将存活的对象复制到另一半,清除使用中的一半内存,然后启用另一半内存。但该算法内存利用率仅为 50%,如果内存中的多数对象都是存活的,需要耗费大量的内存间复制的开销。优化后的算法将内存空间分为一个 Eden 空间和两个 Survivor 空间,提高了内存利用率。
标记 - 整理算法:适用于对象存活率较高时的垃圾回收,如老年代。标记要清理或不要清理的对象,将所有存活的对象都向内存空间一端移动,然后清理掉边界以外的内存。该算法的问题是移动存活对象并更新引用内存地址带来大量消耗,且程序需要等待这些工作完成才能正常执行;不移动对象的话会导致大量内存碎片,使得内存分配非常复杂。
3. 经典垃圾收集器
经典垃圾收集器包括 Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、Garbage First 等收集器,它们各有特点和工作流程。
Seria 垃圾收集器:最基础、历史最悠久的新生代垃圾收集器,单线程工作,简单高效但存在工作线程停顿问题,是 HotSpot 虚拟机客户端模式下的默认新生代收集器。
ParNew 垃圾收集器:Serial 垃圾收集器的多线程并行版本,激活 CMS 后默认的新生代收集器,在多逻辑核心环境下相对 Serial 稍微高效。
Parallel Scavenge 垃圾收集器:基于标记 - 复制算法实现的多线程并行收集的新生代垃圾收集器,高吞吐量,适合后台运算而不需要太多交互的分析任务。
Serial Old 垃圾收集器:Serial 垃圾收集器的老年代版本,单线程收集器,基于标记 - 整理算法。
Parallel Old 垃圾收集器:Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记 - 整理算法。
CMS (Concurrent Mark Sweep) 收集器:以获取最短回收停顿时间为目标的收集器,基于标记 - 清除算法实现,增量更新解决并发收集问题。但存在对处理器资源敏感、产生浮动垃圾、导致内存碎片等问题。GC 过程包括初始标记(stop the world)、并发标记、重新标记(stop the world)、并发清除。
Garbage First (G1) 垃圾收集器:主要面向服务端应用,可控的停顿时间,整理上是基于标记 - 整理算法,局部是基于标记 - 复制算法,是当前服务端模式下的默认垃圾收集器。原始快照解决并发收集问题。G1 收集器将连续的 Java 堆分为多个大小相等的独立区域,各个区域可以独立扮演 Eden,Survivor 空间,还有特殊的 Humongous 区域专门存储大对象。GC 过程包括初始标记(stop the world)、并发标记、重新标记(stop the world)、并发清除。
4. 低延迟垃圾收集器
Shenandoah 和 ZGC 收集器是低延迟垃圾收集器,特点和工作流程如下:
Shenandoah 垃圾收集器:非 Oracle,Oracle JDK12 + 不支持。并发垃圾标记 + 并发对象清理后的整理 = 基本全程并发 = GC <10mills,是 G1 的继承者。改进了记忆集,采用链接矩阵代替,非分代收集。GC 过程包括初始标记(短暂停顿)、并发标记、最终标记(短暂停顿)、并发清理、并发回收、初始引用更新(短暂停顿)、并发引用更新、最终引用更新(短暂停顿)、并发清理。通过读屏障和 “Brooks Pointers” 解决与用户线程的并发问题。对象头部存储一个内存地址指针,正常时指向自己,当对象移动时,旧对象的地址指向新的对象内存空间,实现间接访问对象。旧对象需要在完成引用更新后再进行删除,访问内存地址指针必须是同步操作以防止多线程内存不一致问题,Shenandoah 收集器通过 CAS 来保证并发访问。
ZGC 收集器:since JDK11 Oracle,基于 Region 内存布局,不设分代,使用读屏障、染色指针和内存多重映射等技术实现可并发的标记 - 整理算法,以低延迟为首要目标。动态创建和销毁的动态容量大小的 Region,包括小型 Region(容量固定为 2MB,存放小于 256KB 的小对象)、中型 Region(容量固定为 32MB,用于放置大于等于 256KB 但小于 4MB 的对象)、大型 Region(容量不固定,可以动态变化,用于放置大于 4MB 的大对象,每个大型 Region 只会存放一个大对象)。GC 过程包括初始标记(短暂停顿)、并发标记、最终标记(短暂停顿)、并发预备重分配、并发重分配、并发重映射。染色指针在对象的内存地址上占用 4 个字节来判断对象是否被移动过,这也导致了 ZGC 能够管理的内存不能超过 4TB,不能支持 32 位平台,不能支持压缩指针。ZGC 在传输信息时会产生大量的 NAKACK 对象,并且有重发机制,在确定消息送达之前,数据会滞留在内存中。由于共享数据需要与多个节点通信,网络资源紧张导致大量的消息数据滞留在内存中。很快产生了内存溢出。解决方案是替换全局缓存,避免频繁的写操作,也避免了较多的缓存同步通信。ZGC 的劣势是没有分代收集导致在 GC 过程中产生大量新对象的场景将导致大量浮动垃圾,清理速度慢于对象产生速度的话将可能导致这种情况恶化。
五、虚拟机性能监控、故障处理工具
1. 基础故障处理工具
包括 jps、jstat、jinfo、jmap、jhat、jstack 等工具的功能和使用方法。
jps:虚拟机进程状况工具
- 功能:
- 列出正在运行的虚拟机进程。
- 显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID。
- 使用频率最高的 JDK 命令行工具。
- 命令格式:jps [options] [hostid]。
- options:
- -q:只输出 LVMID(虚拟机本地 ID),省略主类的名称。
- -m:输出虚拟机进程启动时传递给主类 main 函数的参数。
- -l:输出主类的全名,如果进程执行的是 JAR 包,则输出 JAR 路径。
- -v:输出虚拟机进程启动时的 JVM 参数。
- hostid:通过 RMI 协议查看在 RMI 注册表中的远程主机的进程信息等。
- options:
jstat:虚拟机统计信息监视工具
- 功能:
- 显示本地或与远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。
- 纯文本控制台环境的 JVM 定位虚拟机性能问题的常用工具。
- 命令格式:jstat [option vmid [interval [s|ms] [count]] ]。
- options:不同的选项可以展示不同的运行时数据。
- vmid:虚拟机 id,本地进程与 lvmid 一致,远程格式如下:[protocol:][//] vmid [@hostname [:port]/servername]。
- interval:查询间隔,单位毫秒。
- count:查询次数。
jinfo:Java 配置信息工具
- 功能:
- 查看 Java 虚拟机运行时的参数配置。
- 运行时修改一部分支持修改的虚拟机参数值。
- 命令格式:jinfo [option] pid。
- option:
- -flag:查询参数。
- -sysprops:查看 System.getProperties ()。
- option:
jmap:Java 内存映像工具
- 功能:
- 生成堆转储快照(一般称为 heapdump 或者 dump 文件)。
- 查询 finalize 执行队列、Java 堆和方法区的详细信息(空间使用率,当前用的哪种收集器等)。
- 堆快照:堆中的对象信息,实例数量,堆中的内存占用信息等信息。
- 命令格式:jmap [option] vmid。
- option:不同的选项可以实现不同的功能。
jhat:虚拟机堆转储快照分析工具
- 与 jmap 搭配使用。
- 功能:分析 jmap 生成的堆转储快照。
- 不推荐使用,可以用其他方式分析堆转储快照:
- 在服务器上使用 jhat 会比较占用服务器资源。
- jhat 的分析功能比较简陋。
- 推荐使用:
- VisualVM。
- Eclipse Memory Analyzer。
- IBM HeapAnalyzer。
jstack:Java 堆栈跟踪工具
- 功能:
- 用于生成虚拟机当前时刻的线程快照(threaddump/javacore 文件)。
- 通常用于定位线程出现长时间停顿的原因(如线程死锁、死循环、请求外部资源长时间挂起等)。
- 线程快照:当前虚拟机每一条线程正在执行的方法堆栈的集合。
- 命令格式:jstack [option] vmid。
- option:不同的选项可以实现不同的功能。
- 替代方案:
- 自 JDK5 起,Thread 类提供了 getAllStackTraces () 方法用于获取虚拟机所有线程的 StackTraceElement 对象,用这个方法可以完成 jstack 的大部分功能。
- 用 Thread.getAllStackTraces () 方法做一个 web 页面用于开发时查看线程堆栈信息。
2. 可视化故障处理工具
如 JHSDB、JConsole、VisualVM、JFR、JMS 等工具的功能和特点。
JHSDB:基于服务性处理的调试工具
- 集成式的可视化多功能工具箱,集成了多个 jdk 命令行工具,且功能更强大。
- 命令行模式下的使用:可以通过特定的命令行参数进行操作。
- 基于服务性代理 (Seriviceability Agent, SA) 实现的进程外调试工具。
- 对压缩指针的支持存在缺陷,64 位系统测试时可以禁用压缩指针:-XX:-UseCompressedOops。
JConsole:Java 监视与管理控制台
- Java Monitoring and Management Console。
- 基于 JMX (Java Management Extensssions) 的可视化监视、管理工具。
- 功能:
- 收集系统运行信息:
- 内存。
- 线程:线程运行情况、死锁。
- 类创建统计。
- CPU。
- 动态调整参数。
- 收集系统运行信息:
VisualVM:多合一故障处理工具
- All-in-One Java Troubleshooting Tool。
- Java 功能最强大的运行监视和故障处理程序之一。
- 基于 NetBeans 平台开发工具,可安装插件。
- 功能:
- 显示虚拟机进程以及进程的配置、环境信息:类似 jps、jinfo。
- 监视应用程序的处理器、垃圾收集、堆、方法区以及线程的信息:类似 jstat、jstack。
- dump 以及分析堆转储快照:类似 jmap、jhat。
- 方法级的程序运行性能分析:找出被调用最多、运行时间最长的方法。
- 离线程序快照:运行程序的运行时配置、线程 dump、内存 dump 等信息建立一个快照,可以将快照发送开发者进行 bug 反馈。
- 其他插件功能。
- JDK6 Update7 发布,向下兼容至 JDK1.4.2 版本。
- **JDK14 起 JDK 不再提供 visualvm,需手动下载安装 **https://visualvm.github.io/。
- 修改 visualvm.conf 设置 jdk 路径。
- 打开 visualvm.exe。
- 插件安装:
- JDK_HOME/bin 目录下找到 visualvm。
- 手动安装:
- 插件网站:https://visualvm.github.io/pluginscenters.html。
- 下载 nbm 插件包。
- 点击工具 > 插件 > 已下载菜单,指定 nbm 包安装。
- 插件安装目录:jdk9 jkd_HOME/lib/visualvm 目录下。
- 自动安装:
- 点击工具 > 插件菜单。
- 选择可用插件及已安装,勾选对应插件安装或激活即可。
- Mac 下使用 visualvm:
- https://visualvm.github.io/下载 visualvm,安装后即可使用。
- 集成 idea:
- 插件中心下载 visualvm launcher。
- 插件推荐列表:
- Btrace:动态加入原本不存在的调试代码 https://github.com/braceio/btrace。
- 打印调用堆栈、参数、返回值。
- 进行性能监视、定位链接泄漏、内存泄漏。
- 解决多线程竞争问题等。
- Btrace:动态加入原本不存在的调试代码 https://github.com/braceio/btrace。
JFR:可持续收集数据的飞行记录仪
- Java Flight Recorder。
- 内建在 HotSpot 虚拟机内的信息收集框架。
- 企业版收费,个人版免费。
- 对生产环境的网站吞吐量影响小于 1% – Zero Performance Overhead。
- 动态开启,停止。
- 飞行记录指程序运行一段时间内的信息记录:
- 一般信息:关于虚拟机、操作系统和记录的一般信息。
- 内存:关于内存管理和垃圾收集的信息。
- 代码:关于方法、异常错误、编译和类加载的信息。
- 线程:关于应用程序中线程和锁的信息。
- I/O:关于文件和套接字输入、输出的信息。
- 系统:关于正在运行 Java 虚拟机的系统、进程和环境变量的信息。
- 事件:关于记录中的事件类型的信息,可以根据线程或堆栈跟踪,按照日志或图形的格式查看。
- JFR 获取的数据粒度较细,也较为可靠。可以使用 JMS 进行统计分析。
JMS:可持续在线的 JVM 监控工具
- Java Mission Control。
- 监控 Java 虚拟机。
- 企业版收费,个人版免费。
- 基于 Eclipse RCP 框架。
- 采取 JMX 协议通信。
- 主要功能:
- 显示来自虚拟机 MBean 提供的数据。
- 展示 JFR 的数据并分析。
六、调优案例分析与实战
1. 大内存硬件上的程序部署策略
在大内存硬件环境下,程序部署可能会出现一些问题。以一个 15 万 PV / 日左右的在线文档类型网站为例,该网站服务器硬件为四路志强处理器、16GB 物理内存,操作系统为 64 位 CentOS5.4,Resin 作为 Web 服务器,软件版本选用 64 位的 JDK5,管理员启用一个虚拟机实例,将 Java 堆大小固定在 12GB。然而,运行效果并不理想,网站经常不定期出现长时间失去响应。
问题剖析:
这种情况是由垃圾收集器停顿所导致的。默认使用的是吞吐量优先收集器,回收 12GB 的 Java 堆,一次 Full GC 的停顿时间高达 14 秒。此外,由于程序设计原因,访问文档时会把文档从磁盘提取到内存中,导致内存中出现很多由文档序列化产生的大对象,这些大对象大多在分配时就直接进入了老年代,没有在 Minor GC 中被清理掉,使得内存很快被消耗殆尽。
解决方案:
目前在大内存硬件上主要有两种程序部署方式。
- 方式一:通过一个单独的 Java 虚拟机实例来管理大量的 Java 堆内存。这种方式需要考虑一些问题,如回收大块堆内存而导致的长时间停顿,虽然 G1 收集器出现后有了增量回收,但要到 ZGC 和 Shenandoah 收集器成熟后才相对彻底解决;大内存必须有 64 位虚拟机支持,但由于压缩指针、处理器缓存行容量等因素,64 位性能普遍低于 32 位虚拟机;必须保证应用程序足够稳定,因为这种大型单体应用要是发生了堆内存溢出,几乎无法产生堆转储快照,就算成功了也难以分析;相同的程序在 64 位消耗的内存一般比 32 位虚拟机要大,可以开启(默认开启)压缩指针来缓解。
- 方式二:同时使用若干个虚拟机,建立逻辑集群来利用硬件资源。具体做法是在一台机器上启动多个应用服务器进程,为每个服务器进程分配不同端口,然后在前端建立一个负载均衡器,以反向代理的方式来分配访问请求。这种方式可能会遇到一些问题,如节点竞争全局资源,很难最高效率地利用某些连接池,32 位虚拟机作为集群节点会受到内存限制,大量使用本地缓存的应用在集群中会造成较大内存浪费等。
对于该案例,最终方案是调整为建立 5 个 32 位 JDK 的逻辑集群,每个进程按 2GB 计算,占用 10GB。另外建立一个 Apache 服务作为前端均衡代理,考虑到用户对响应速度比较关心,并且文档服务的主要压力集中在磁盘和内存访问,处理器资源敏感度低,因此改为 CMS 收集器进行垃圾回收。
2. 集群间同步导致的内存溢出
以一个基于 B/S 的 MIS 系统为例,该系统由两台双路处理器、8GB 内存的惠普小型机组成,使用 WebLogic 部署,每台机器有 3 个 WebLogic 实例构成一个 6 节点亲和式集群。节点之间没有共享 Session,但有一些需求要实现部分数据在各个节点间共享,一开始放在数据库,后因读写频繁,影响性能。之后构建了一个全局 JBossCache 缓存,启用后不定期出现多次内存溢出问题。
问题剖析:
由于 JBossCache 在传输信息时会产生大量的 NAKACK 对象,并且有重发机制,在确定消息送达之前,数据会滞留在内存中。由于共享数据需要与多个节点通信,网络资源紧张导致大量的消息数据滞留在内存中,很快产生了内存溢出。
解决方案:
替换全局缓存,避免频繁的写操作,也避免了较多的缓存同步通信。
3. 堆外内存导致的溢出错误
以一个基于 B/S 的电子考试系统为例,测试期间发现服务端不定时抛出内存溢出异常。尝试把堆内存调到最大,基本没效果,加入 -XX:+HeapDumpOnOutOfMemoryError 参数,也没有任何反应。只好挂着 jstat 观察,发现垃圾并不频繁,内存很稳定,但就是照样不停抛出 OOM。
问题剖析:
Direct Memory 耗用的内存不算入
相关文章:
深入探索 JVM:原理、机制与实战
一、JVM 概述 JVM(Java Virtual Machine)是 Java 程序运行的核心组件,它提供了一个独立于硬件和操作系统的执行环境,使得 Java 程序能够在不同平台上具有跨平台的特性。 JVM 主要由以下几部分组成: 类装载器…...
前端成长之路:HTML(2)
HTML中有两个非常重要的标签——表格和表单,在介绍之前需要先了解表格和表单的区别:表格是用于展示数据的;表单是用于提交数据的。本文主要介绍表格。 表格标签 表格主要是用于显示、展示数据的,并非是页面布局。它可以使本来难…...
python基础:(七)类
目录 一.创建和使用类二.使用类和实例2.1给属性指定默认值2.2修改属性的值2.2.1直接修改属性的值2.2.2通过方法修改属性的值2.2.3通过方法对属性的值进行递增 三.继承3.1子类的方法__init__()3.2给子类定义属性和方法3.3重写父类的方法 四.导入类4.1语法--1:4.2语法--2 前言 p…...
Spring AOP基础、快速入门
介绍 AOP,面向切面编程,作为面向对象的一种补充,将公共逻辑(事务管理、日志、缓存、权限控制、限流等)封装成切面,跟业务代码进行分离,可以减少系统的重复代码和降低模块之间的耦合度。切面就是…...
Golang使用etcd构建分布式锁案例
在本教程中,我们将学习如何使用Go和etcd构建分布式锁系统。分布式锁系统对于管理对分布式系统中共享资源的并发访问至关重要。它有助于维护一致性,防止竞争条件,并确保在任何给定时间只有一个进程独占访问资源。 我们将使用Go作为编程语言&am…...
深度学习:基于MindSpore的极简风大模型微调
什么是PEFT?What is PEFT? PEFT(Parameter Efficient Fine-Tuning)是一系列让大规模预训练模型高效适应于新任务或新数据集的技术。 PEFT在保持大部分模型权重冻结,只修改或添加一小部份参数。这种方法极大得减少了计算量和存储开销&#x…...
如何在 Android 项目中实现跨库传值
背景介绍 在一个复杂的 Android 项目中,我们通常会有多个库(lib),而主应用程序(app)依赖所有这些库。目前遇到的问题是,在这些库中,libAd 需要获取 libVip 的 VIP 等级状态…...
HTML:表格重点
用表格就用table caption为该表上部信息,用来说明表的作用 thead为表头主要信息,效果加粗 tbody为表格中的主体内容 tr是 table row 表格的行 td是table data th是table heading表格标题 ,一般表格第一行的数据都是table heading...
STM32 出租车计价器系统设计(一) 江科大源码改写
STM32 出租车计价器系统设计 功能目标 驱动步进电机模拟车轮旋转,并实现调速功能。 设置车轮周长和单价,检测车轮转速和运转时间。 计算并显示行驶里程和价格。 硬件材料 28BYJ48 五线四相步进电机和 ULN2003 驱动板模块 测速传感器模块 嵌入式小系统…...
Git基础操作快速入门
Git是一个免费开源分布式版本控制工具,是由Linux的作者Linus开发的第二个伟大作品。2005年由于BitKeeper软件公司对Linux社区停止了免费使用权。Linus迫不得己自己开发了一个分布式版本控制工具,从而Git诞生了 目前使用Git作为版本控制的开源软件&#…...
vue‘ 不是内部或外部命令,也不是可运行的程序或批处理文件。
在Windows操作系统中,安装了nodeJs之后,并且也安装了vue依赖包,但是在cmd控制台运行vue的时候,会报错:vue‘ 不是内部或外部命令,也不是可运行的程序或批处理文件。针对这个问题我提供如下解决办法,并且是有效的。 一、原因分析 关于尝试这个问题的主要原因,我分析主要…...
JAVA安全—SpringBoot框架MyBatis注入Thymeleaf模板注入
前言 之前我们讲了JAVA的一些组件安全,比如Log4j,fastjson。今天讲一下框架安全,就是这个也是比较常见的SpringBoot框架。 SpringBoot框架 Spring Boot是由Pivotal团队提供的一套开源框架,可以简化spring应用的创建及部署。它提…...
Milvus向量数据库05-常见问题整理
Milvus向量数据库05-常见问题整理 1-什么是PipeLine 这张图展示了一个文档处理和搜索系统的架构,主要分为两个部分:Ingestion Pipeline(摄取管道)和 Search Pipeline(搜索管道)。下面是对图中各部分的详细…...
strncpy在复制含有多个\0的字符串时遇到的问题
strncpy在复制含有多个\0的字符串的时候,会产生截断,因为strncpy在读取源字符串的时候,遇到了\0,函数会认为该字符串已经结束了,然后会向目标字符串内填充\0。 char buffer[100] "ak\0jl";for (int i 0; i…...
C++作业3
作业1: 1.定义一个矩形类Rec,包含私有属性length、width,包含公有成员方法: void set_length(int l);//设置长度 Void set_width(int w);//设置宽度 Int get_length();//获取长度,将长度的值返回给调用处 Int get_widt…...
重生之我在学Vue--第1天 Vue 3 基础与开发环境搭建
重生之我在学Vue–第1天 Vue 3 基础与开发环境搭建 文章目录 重生之我在学Vue--第1天 Vue 3 基础与开发环境搭建前言一、Vue 3 的特点与核心概念二、搭建开发环境1. 安装 Node.js2. 使用 Vite 创建 Vue 3 项目创建项目进入项目目录并安装依赖启动开发服务器 3. 理解项目结构 三…...
企业经营数据分析系统:提升决策能力的利器
搭建企业经营数据分析系统是当今企业绕不开的话题,企业想要在竞争激烈的市场当中突围而出,需要对于企业内部的各种数据了然于胸,同时对于外部的数据也有敏锐的把握能力,因此企业构建自身的经营性数据分析系统就显得尤其重要。作为…...
Linux笔记9 DNS域名解析服务器
简介 DNS(Domain Name System)是互联网上的一项服务,它作为将域名和IP地址相互映射的一个分 布式数据库,能够使人更方便的访问互联网。 DNS使用的是53端口, 通常DNS是以UDP这个较快速的数据传输协议来查询的&#x…...
鸿蒙高级开发者认证的主观题试题及答案
以下是一份鸿蒙高级开发者认证的主观题试题及答案示例,涵盖了鸿蒙开发中的多个关键技术和应用场景相关内容,希望对你有所帮助: 一、论述题(每题 20 分,共 60 分) 1. 阐述鸿蒙操作系统中分布式软总线的工作原理、核心优势以及在多设备协同应用开发场景下的应用方式,并举…...
leetcode_547 省份数量
该题主要运用了图的连通性 接着使用染色法解决该问题 染色法:标记所有节点为false 访问后 将其标记位true class Solution {int n; // 代表n个数据bool colors[201]; // 标记是否访问到void dfs(vector<vector<int>>& isConnected, int u) { // …...
【开源】一款基于SpringBoot 的全开源充电桩平台
一、下载项目文件 下载源码项目文件口令:动作璆璜量子屏多好/~d1b8356ox2~:/复制口令后,进入夸克网盘app即可保存(如果复制到夸克app没有跳转资源,可以复制粘贴口令到夸克app的搜索框也可以打开(不用点搜索按钮&#…...
react antd tabs router 基础管理后台模版
在构建 React 后台管理系统时,使用标签页的方式展示路由是一种高效且用户友好的设计模式。这种实现方式通常允许用户在多个页面之间快速切换,并保留页面的状态,类似于浏览器的多标签页功能。 需求分析 1.动态标签页:根据用户的导…...
uniapp uni-table最简单固定表头
需求:固定表头数据,在网上找了半天,啥都有,就是一直实现不了,最后更改代码实现 1.效果 2.主要代码讲解完整代码 表格的父级一定要设置高度,不然会错位,我看网上说设置position:fixed…...
从0到1实现项目Docker编排部署
在深入讨论 Docker 编排之前,首先让我们了解一下 Docker 技术本身。Docker 是一个开源平台,旨在帮助开发者自动化应用程序的部署、扩展和管理。自 2013 年推出以来,Docker 迅速发展成为现代软件开发和运维领域不可或缺的重要工具。 Docker 采…...
Mac软件推荐
Mac软件推荐 截图SnipasteXnipBob 快捷启动Raycast 系统检测Stats 解压缩The UnarchiverKeka(付费) 视频播放IINA 视频下载Downie(付费) 屏幕刘海TopNotchMediaMate(付费)NotchDrop(付费&#x…...
No.4 笔记 探索网络安全:揭开Web世界的隐秘防线
在这个数字时代,网络安全无处不在。了解Web安全的基本知识,不仅能保护我们自己,也能帮助我们在技术上更进一步。让我们一起深入探索Web安全的世界,掌握那些必备的安全知识! 1. 客户端与WEB应用安全 前端漏洞࿱…...
Unity-Webview 使用指南
Unity-Webview 使用指南 Unity-Webview 主に gree/unity-webview のリファクタリング。本家を元に改良してく! [这里是图片001] 项目地址: https://gitcode.com/gh_mirrors/uni/Unity-Webview Unity-Webview 是一个专为 Unity 开发的 WebView 插件,使开…...
【Vue】自定义指令、插槽
目录 自定义指令 是什么 作用 使用方法 定义 使用 自定义指令配合绑定数据 语法 自定义指令的简写 语法 使用时机 插槽 什么是插槽 默认(匿名)插槽 编辑插槽的默认值 具名插槽 使用方法 简写 使用示例 作用域插槽 自定义指令 是什…...
AI - RAG中的状态化管理聊天记录
AI - RAG中的状态化管理聊天记录 大家好,今天我们来聊聊LangChain和LLM中一个重要的话题——状态化管理聊天记录。在使用大语言模型(LLM)的时候,聊天记录(History)和状态(State)管理是非常关键的。那我们先…...
微服务网关SpringCloudGateway、Kong比较
网关产品 1. Spring Cloud Gateway 基本信息 Spring Cloud Gateway是Spring Cloud生态系统中的一个组件,基于Spring 5、Project Reactor和Spring Boot 2构建。它旨在为微服务架构提供一种简单而有效的API网关解决方案。 功能特点 路由功能强大:使用Rou…...
MVC基础语法
文章目录 项目地址一、MVC的传值方式1.1 ViewBag和ViewData传值1.1.1 ViewBag1.1.2 ViewData 1.2 视图模型传值(ViewModel) 二、HttpConntext上下文三、中间件 项目地址 教程作者:誉尚学教育教程地址: https://www.bilibili.com…...
Web day09 会话技术 JWT令牌 Filter Interceptor
目录 会话技术: 1.Cookie: 2.Session: 3.令牌技术: JWT令牌: 生成JWT令牌: 校验JWT令牌(解析生成的令牌) 登陆时下发令牌: 过滤器Filter: 拦截器Inte…...
OpenCV相机标定与3D重建(14)用于组合两个旋转和平移(R|T)变换函数composeRT()的使用
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 cv::composeRT 是 OpenCV 库中的一个函数,用于组合两个旋转和平移(R|T)变换。这个函数可以将两个连续的刚体变…...
leetcode33.搜索旋转排序数组
整数数组 nums 按升序排列,数组中的值 互不相同 。 在传递给函数之前,nums 在预先未知的某个下标 k(0 < k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k1], ..., nums[n-1], nums[0], nums[1], ..…...
TMS Software:TMS BIZ产品——TMS XData
TMS XData 用于多层REST/JSON HTTP/HTTPS应用服务器开发和ORM远程处理的Delphi框架。 TMS扩展数据可同时用于以下框架: VCLWEBFMX TMS XData可同时用于以下操作系统/浏览器: TMS XData可同时用于以下IDE: 功能概述 基于REST/JSON架构风格的…...
leecode中的面试100题
isalnum函数用于检查一个字符是否为字母或数字。它的参数是一个int类型 isupper() 是大写 islower() 是小写 toupper() 变成大写 tolower() 变成小写 do while 首先不管怎么 先执行do一次 然后执行完了之后 在判断while如果符合while里面就继续执行do vector容器中insert…...
不同类型的集成技术——Bagging、Boosting、Stacking、Voting、Blending简述
目录 一、说明 二、堆叠 2.1 堆叠的工作原理: 2.2 例子: 2.3 堆叠的优点: 三、投票(简单投票) 3.1 例子: 3.2 投票的优点: 四、装袋和投票之间的区别 五、混合 6.1 混合的主要特征: …...
【从零开始入门unity游戏开发之——C#篇01】理论开篇
文章目录 前言前置条件什么是编程?什么是代码?什么是编程语言?常见的编程语言什么是C#?学习Unity为什么要先学习C#?选择适合自己的IDE集成开发环境VSCode安装和环境配置VSCode调试模式专栏推荐完结 前言 这个系列我想…...
TCP的“可靠性”(上)
目录 TCP的“可靠性”(上)确认应答(可靠性传输的基础)超时重传连接管理(三次握手,四次挥手) TCP的“可靠性”(上) 想必大家都或多或少的听说过TCP的特性:有连…...
Windows平台Unity3D下如何低延迟低资源占用播放RTMP或RTSP流?
技术探讨 自2017年我们发布跨平台的低延迟Unity下的RTSP|RTMP直播播放器后,Unity下的直播体验有了质的提升,特别是RTMP,从大家认知里面的几秒钟,直接缩减到100-300ms,满足了绝大多数场景下低延迟的技术诉求。今天就Un…...
burp的编解码,日志,比较器
声明! 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关&a…...
Vercel部署前端部署
Vercel 部署 今天要讲的是如何对别人向自己的开源仓库提的PR进行自动代码审核 1. 注册并登录Vercel 访问 Vercel官网点击右上角的"Sign Up"选择使用GitHub、GitLab、Bitbucket或邮箱注册完成注册流程并登录 2. 连接代码仓库 在Vercel仪表板,点击"New Proje…...
Jenkins相关的Api接口调用详解
Jenkins API是Jenkins持续集成和持续部署(CI/CD)平台提供的一组接口,允许外部程序通过HTTP请求与Jenkins进行交互。以下是对Jenkins API使用的简介: 一、Jenkins API的主要功能 作业管理:通过API,可以创建、配置、删除以及查询作业(Job)。构建触发:可以远程触发新的构…...
HBU深度学习实验15-循环神经网络(2)
LSTM的记忆能力实验 飞桨AI Studio星河社区-人工智能学习与实训社区 (baidu.com) 长短期记忆网络(Long Short-Term Memory Network,LSTM)是一种可以有效缓解长程依赖问题的循环神经网络.LSTM 的特点是引入了一个新的内部状态&am…...
洛谷P1364 医院设置(c嘎嘎)
题目链接:P1364 医院设置 - 洛谷 | 计算机科学教育新生态 题目难度:普及/提高 数据规模与约定: 对于 100%100% 的数据,保证 1≤n≤1001≤n≤100,0≤u,v≤n0≤u,v≤n,1≤w≤1051≤w≤105。 解题思路&…...
Java死锁问题如何解决?
大家好,我是锋哥。今天分享关于【Java死锁问题如何解决?】面试题。希望对大家有帮助; Java死锁问题如何解决? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Java中的死锁(Deadlock)是一种并发…...
go锁与chan的性能对比
锁的作用chan 的作用golang的数据并不是并发安全的为什么锁的性能更加优秀?如何选择? 锁的作用 解决并发安全问题,流程控制等 chan 的作用 线程通信(数据传输), 并发安全,流程控制 golang的数据并不是并发安全的 golang的变量并不是并发安全的锁与chan都可以解决并发安全…...
最小二乘法拟合出二阶响应面近似模型
背景:根据样本试验数据拟合出二阶响应面近似模型(正交二次型),并使用决定系数R和调整的决定系数R_adj来判断二阶响应面模型的拟合精度。 1、样本数据(来源:硕士论文《航空发动机用W形金属密封环密封性能分析…...
Scala的隐式转换
package hfdobject Test37 { //复习隐式转换//隐式转换:编译器 偷偷地,自动的帮我们把一种数据类型转换为另外一种类型//列如:int -->double//它有失败的时候(double -->int),有成功的时候//当它转换失败的时候,…...
vue中父组件接收子组件的多个参数的方法:$emit或事件总线
方法一:使用 $emit 方法 原理 子组件通过 $emit 方法向父组件发送事件,同时可以传递多个参数,父组件通过事件监听来接收这些参数。 示例 子组件代码 <template><div><button click"sendData">发送数据</…...