1、JVM由哪些部分组成?
- ClassLoader(类加载器)
- Runtime Data Area(运行时数据区,内存分区)
- Execution Engine(执行引擎)
- Native Method Library(本地库接口)
类加载器(Class Loader):
- 负责加载Java类到JVM中。它在运行时将Java类的
.class
文件从硬盘读入JVM,这些类文件可以来自本地文件系统、网络或其他源。运行时数据区(Runtime Data Areas):
- 方法区(Method Area):存储每一个类的结构信息,如运行时常量池、字段和方法数据、构造函数和普通方法的代码等。
- 堆(Heap):JVM管理的最大一块内存区域,用于存储所有实例对象和数组。堆是垃圾收集器管理的主要区域。
- 栈(Stacks):存储局部变量和部分过程结果,并且在方法调用和返回时起作用。每个线程拥有自己的调用栈。
- 程序计数器(Program Counter Register):当前线程执行的字节码的行号指示器。
- 本地方法栈(Native Method Stack):为JVM使用到的Native方法服务。
执行引擎(Execution Engine):
- 负责执行类文件中的指令。它包含一个虚拟处理器,能够执行字节码指令,部分执行引擎还包括即时编译器(JIT compiler),它可以将热点代码编译为本地机器码以提高效率。
本地接口库(Native Interface):
- 为Java应用提供和底层操作系统交互的能力,允许Java调用本地(C/C++)应用程序库和系统调用。
垃圾收集器(Garbage Collector):
- 自动管理JVM堆内存的回收,释放不再使用的对象所占用的内存空间。
2、什么是程序计数器?
在Java虚拟机(JVM)中,程序计数器(Program Counter)是一块较小的内存空间,它是线程私有的,即每个线程都有自己独立的程序计数器。在JVM中,程序计数器主要负责存储当前线程正在执行的字节码指令的地址或者索引。
3、元空间(MetaSpace)介绍
首先先放一张图:(取自B站up主黑马程序员)
元空间(MetaSpace)是Java虚拟机中用于存储类元数据的内存区域。在Java 8及之后的版本中,元空间取代了永久代(PermGen)作为存储类元数据的位置。永久代有着固定的大小,并且容易出现内存溢出的情况,因此Java 8引入了元空间作为永久代的替代品。
拓展:其中和永久代相比区别如下:
大小动态调整: 元空间的大小不再固定,它可以根据需要动态调整,受限于系统的物理内存大小。这使得Java应用程序能够更加灵活地利用可用的内存资源。
类元数据存储在本地内存: 元空间中存储的类元数据是在本地内存中分配的,而不是像永久代一样存储在Java堆中。这样做的好处是可以避免Java堆内存不足导致的永久代溢出问题。
垃圾回收机制: 元空间中的垃圾回收机制与Java堆中的垃圾回收机制是独立的。类加载器和元数据使用的内存不再受到垃圾收集的限制,垃圾收集器只会回收没有被任何类加载器引用的类元数据。
自动释放不再使用的类元数据: 元空间中的内存不再受到永久代的限制,因此不需要进行永久代中那样的永久代回收。Java虚拟机会自动释放不再使用的类元数据,从而避免了永久代内存溢出的问题。
4、常量池
常量池主要存在于两个地方:类文件结构中的常量池表和运行时常量池。
类文件结构中的常量池表:
在Java编译后生成的.class文件中,存在一个常量池表(Constant Pool Table),它包含了该类文件中所有字面量和符号引用。这些信息在编译期间就已经被确定,并且被存储在类文件中。常量池表的主要内容包括:
- 文字常量:如文本字符串、声明为final的常量等。
- 类和接口的全限定名:如
java/lang/String
。- 字段的名称和描述符。
- 方法的名称和描述符。
- 其他如方法句柄、方法类型、动态调用点等。
行时常量池:
当类和接口被加载到JVM中时,对应的类文件中的常量池表内容会被加载到运行时常量池中。运行时常量池是方法区(Method Area)的一部分,每个加载到JVM的类或接口都有自己的运行时常量池。
运行时常量池相较于类文件中的常量池表,不仅包含了所有的符号引用,还会在类加载的解析阶段,将这些符号引用转换成直接引用。这些直接引用指向了方法区中类的方法、字段的具体内存地址等,使得程序运行时能够直接通过引用访问这些成员,无需再通过符号查找,从而提高了程序的执行效率。
5、GC垃圾回收可达性分析算法介绍
这个算法的核心思想是通过一系列的称为“GC Roots”的对象作为起点,遍历访问这些对象所能到达的其他对象。如果从GC Roots到某个对象没有任何引用路径(即该对象不可达),则认为此对象是不可再使用的,也就是垃圾,可以被GC回收。 JVM虚拟机篇-09-垃圾回收-对象什么时候可以被垃圾器回收_哔哩哔哩_bilibili
GC Roots包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
举例:
当
demo = null;
这行代码执行后,变量demo
不再指向它原来创建的new Demo()
实例。这里需要澄清的是:
- 变量
demo
是一个引用变量,存在于栈帧的本地变量表中,它的作用是引用堆中的一个Demo
实例。- 当
demo = null;
执行时,demo
这个引用变量不再指向任何对象,因此原来通过demo
引用的new Demo()
对象在堆内存中变成了不可达状态。- 在接下来的垃圾回收过程中,由于
new Demo()
对象不可达(即没有任何GC Roots到这个对象的引用路径),它将被视为垃圾对象并可能被回收。
- 方法区中常量引用的对象:
6、G1(Garbage-First)垃圾回收器
G1垃圾回收器是Java虚拟机(JVM)的一种垃圾收集器,旨在替代老一代的垃圾收集器(如Parallel GC和CMS)提供更高的性能。自JDK 9起,G1成为了HotSpot JVM的默认垃圾收集器。G1垃圾回收器主要设计目标是兼顾高吞吐量与低延迟,适用于多核处理器、大内存的服务器环境。
分区(Region-based)堆布局
- G1将Java堆划分为多个大小相等的分区(Region)。每个分区可能被定义为Eden、Survivor或Old区,而不是事先固定分配这些角色。这种灵活的分区方法使得G1能够更有效地管理堆空间,根据应用程序的实际需要动态调整。
并发和并行处理
- G1能够同时利用多核心处理器的优势进行并行和并发的垃圾回收,从而减少应用程序停顿时间。并行是指在垃圾回收过程中,多个垃圾回收线程同时工作,而并发是指垃圾回收线程与应用线程同时运行。
可预测的停顿时间模型
- G1的一个关键特点是允许用户(通过命令行选项)指定期望的最大停顿时间。G1会尽可能地满足这个停顿时间目标。
7、JVM 调优的参数可以在哪里设置参数值
1、当你通过命令行启动Java应用程序时,可以直接将JVM参数加到命令中。例如,使用
java
命令运行一个JAR文件时,如下:这里,-Xms512m
设置了JVM的初始堆内存大小为512MB,-Xmx1024m
设置了最大堆内存大小为1024MB。
java -Xms512m -Xmx1024m -jar your-application.jar
2、在某些情况下,你可能想要通过设置环境变量来配置JVM参数,尤其是当你不直接启动应用程序,而是通过一些间接的方法(比如使用应用服务器或容器)时。例如,在UNIX系统上,可以设置
JAVA_OPTS
环境变量:
export JAVA_OPTS="-Xms512m -Xmx1024m"
然后,应用程序启动脚本需要被修改或设计成能够读取并使用这个环境变量。
3、如果你的Java应用运行在一个应用服务器(如Tomcat、JBoss、WebSphere等)或容器(如Docker)中,通常可以在服务器或容器的配置文件中设置JVM参数。这些配置文件的位置和格式取决于具体的服务器或容器
- Tomcat:在
bin/setenv.sh
(Linux)或bin/setenv.bat
(Windows)文件中设置CATALINA_OPTS
或JAVA_OPTS
变量。
export CATALINA_OPTS="-Xms512m -Xmx1024m"
-
JBoss/WildFly:在
standalone.conf
(Linux)或standalone.conf.bat
(Windows)文件中设置。 -
Docker:使用
docker run
命令时,可以通过-e
选项传递环境变量或直接在Dockerfile中设置ENV
指令来指定JVM参数。
ENV JAVA_OPTS="-Xms512m -Xmx1024m"
4、在开发阶段,如果你使用集成开发环境(IDE)如IntelliJ IDEA、Eclipse等,通常可以在项目的运行配置中设置JVM参数。这通常在运行/调试配置的“VM选项”或类似的地方进行设置。
选择哪种方法设置JVM参数取决于你的具体使用场景和偏好。无论哪种方法,关键是确保在应用程序启动时,JVM能够接收并识别这些参数。
8、java内存泄露的排查思路
确认内存泄露
在开始具体排查之前,首先确认系统确实存在内存泄露。常见的迹象包括应用随时间运行而逐渐变慢,或者频繁的Full GC却无法回收足够内存。
利用JVM工具监控内存使用情况
使用JVM自带的监控工具(如jvisualvm、jconsole)或商业工具(如YourKit, JProfiler)来观察堆内存(heap)的使用情况。这些工具能够帮助你监控内存使用量,以及观察不同区域(如Young Generation, Old Generation)的内存使用情况。
生成和分析Heap Dump
- 当确认存在内存泄露时,生成Heap Dump是一种常用的排查手段。Heap Dump是JVM堆内存的快照,里面包含了所有对象的信息及其引用关系。
- 可以使用
jmap
工具生成Heap Dump,或在OutOfMemoryError时自动生成Heap Dump(通过设置JVM参数-XX:+HeapDumpOnOutOfMemoryError
)。- 使用MAT(Memory Analyzer Tool)、VisualVM等工具分析Heap Dump文件,找出占用内存最多的对象,以及这些对象的引用链。通过分析引用链,可以找到是哪些对象持有了不应该持有的引用,导致垃圾回收器无法回收这部分内存。
代码审查和排查
- 根据Heap Dump分析的结果,定位到可能的内存泄露代码位置。内存泄露常见的原因包括静态集合类持有对象引用、各种缓存(如HashMap)未及时清理、监听器和各种回调未正确移除等。
- 审查相关代码,特别是对于那些生命周期比较长的对象,检查它们的引用是否在不需要时被正确释放。
修复并验证
- 对疑似引起内存泄露的代码进行修复,然后重新运行应用,并使用之前的监控和分析工具验证修复是否有效。
代码和设计优化
- 在内存泄露排查的过程中,也可能会发现一些设计上的问题。对这些问题进行优化,比如使用弱引用(WeakReference)、软引用(SoftReference)等,以减少内存泄露的可能性。
- 实施好的编程实践,比如及时释放不再使用的资源,使用缓存时设定合理的过期策略等。