概述
JVM整体架构:(简化版)
Java虚拟机(JVM)的设计目的是为了支持跨平台的可移植性和高性能。它是一种抽象的计算模型,可以在各种不同的硬件平台上运行。以下是JVM的基本架构概述:
-
类加载器子系统:
- 类加载器子系统负责将字节码文件(
.class
文件)加载到内存中,并解析成方法区中的数据结构。 - 类加载器包括引导类加载器(
Bootstrap ClassLoader
)、扩展类加载器(Extension ClassLoader
)和应用类加载器(Application ClassLoader
)。
双亲委派机制(Parent Delegation Model):加载委托链app => exc => boot,避免核心类库被篡改。
- 类加载器子系统负责将字节码文件(
-
运行时数据区:
-
运行时数据区是JVM在执行Java程序时使用的内存区域。
-
它包括以下几个部分:
-
方法区
:存储类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。方法区
是 JVM 内存模型的一部分,定义了存储类元数据和相关信息的区域。永久区
是 JDK 6 及以前专门用于存储方法区
内容的堆内存
区域。在 JDK 8 及之后,永久代
被元空间
替代,元空间
也用于存储方法区
的内容,但元空间
使用的是本地内存
,提供了更大的灵活性和改进的内存管理。会发生垃圾回收。方法区
是特殊的堆。 -
堆
:所有线程共享的内存区域,用于存放对象实例。垃圾回收主要发生在这一区域。默认情况下,堆内存的最大大小为总内存的1/4,初始大小为总内存的1/64。堆内存的初始大小和最大大小可以分别通过JVM启动参数:-Xms<size>
和-Xmx<size>
来调节。
-
程序计数器
:指示当前线程所执行的字节码指令的位置。这个区域不会发生内存溢出或垃圾回收。 -
Java虚拟机栈
:每个线程拥有一个私有的栈。这个区域不会发生垃圾回收。栈内部存放的数据结构为栈帧
,栈帧存储局部变量、操作数栈、动态链接、方法返回地址。局部变量
:存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)操作数栈
:用于存放方法执行过程中产生的中间计算结果和计算过程中产生的临时变量动态链接
:为了将符号引用转换为调用方法的直接引用
-
本地方法栈
:与Java虚拟机栈类似,但它用于支持本地(native)方法的调用。这个区域不会发生垃圾回收。
-
-
-
执行引擎:
- 执行引擎负责解释字节码指令并执行它们。
- 它可以采用解释执行的方式,也可以通过即时编译器(Just-In-Time Compiler, JIT)将热点代码编译成本地机器码,提高性能。
-
垃圾收集器:
- 垃圾收集器负责自动回收不再使用的对象所占用的内存。
- 不同版本的JVM提供了多种垃圾收集器,如Serial Collector、Parallel Collector、CMS Collector、G1 Collector等。
-
本机接口:
- Java Native Interface (JNI) 允许Java代码调用本地方法(用C/C++等语言编写的方法)。
JVM中的常量池与缓存机制
字符串常量池(String Constant Pool)位于 Java 堆(Heap)区域中。具体而言,在不同版本的 JVM 中,字符串常量池的位置有所变化:
- Java 6 及之前:
- 字符串常量池位于方法区(Method Area)中的永久代(PermGen)中。
- Java 7:
- 字符串常量池从永久代移到了堆内存中,但仍然属于方法区的一部分。这样做是为了减少永久代的内存压力,避免永久代内存不足的问题。
- Java 8 及之后:
- 字符串常量池完全位于堆内存中,因为永久代被移除了,取而代之的是元空间(Metaspace)。元空间主要存储类元数据,而
字符串常量池
和静态变量等都存储在堆内存
中。需要注意的是,字符串常量池在堆中,但是运行时常量池
则在元空间实现的方法区即本地内存
中。
- 字符串常量池完全位于堆内存中,因为永久代被移除了,取而代之的是元空间(Metaspace)。元空间主要存储类元数据,而
在 Java 7 及之后版本中,可以使用 -XX:StringTableSize=<size>
来设置字符串常量池的哈希表大小,从而影响其性能和垃圾收集行为。
字符串会在以下情况下自动放入字符串常量池:
- 使用字符串字面量定义字符串。
- 编译期常量表达式生成的字符串。
- 调用
intern()
方法将字符串显式放入常量池。
Q:new一个String,这个String会被放入字符串常量池吗?
A:
在Java中,
new String()
不会将字符串放入字符串常量池。具体来说,当你使用
new String("abc")
创建一个字符串时,会有两个对象被创建:
- 字面量 “abc”:这个字符串会被放入字符串常量池中。如果常量池中已经有这个字面量,那么不会再创建新的常量池对象。
- new String(“abc”):这是在堆中创建的一个新的
String
对象,它是一个对常量池中"abc"字符串的拷贝。
整型常量池(Integer Cache)是 Java 提供的一种优化机制,用于缓存和复用某些范围内的整型值。这样做可以提高性能并节省内存,特别是对于频繁使用的整数值。
工作原理:
-
缓存范围:在 Java 中,整型常量池默认缓存的范围是
-128
到127
。这个范围是由 JVM 规范定义的,目的是优化性能,因为这一区间内的整数值非常常用。 -
缓存机制:对于范围内的整数值,当你使用
Integer.valueOf(int)
方法或直接使用自动装箱(例如Integer i = 10;
)时,JVM 会检查缓存中是否存在该值。如果存在,则返回缓存中的对象引用;如果不存在,则创建一个新的Integer
对象,并将其添加到缓存中。 -
自动装箱:Java 的自动装箱机制会将基本数据类型(如
int
)自动转换为其包装类(如Integer
)。在这个过程中,如果数值在缓存范围内,则使用缓存中的对象;否则,创建一个新的Integer
对象。
缓存范围的配置:可以通过 JVM 参数 -XX:IntegerCacheLow
和 -XX:IntegerCacheHigh
来配置缓存的范围。但这些参数并不常用,默认设置是 -128
到 127
。
总结:
整型缓存机制:类似于整型常量池的机制也适用于其他包装类,如 Byte
和 Short
,它们也有各自的缓存机制。
字符常量池:类似于整型常量池,Character
类也有一个字符常量池,用于缓存字符值。
常用的JVM参数
在IDEA中可以这样设置当前启动的JVM参数:
-
堆大小:
-Xms<size>
:初始堆大小。X
表示非标准选项,m
表示memory
,x
表示start
-Xmx<size>
:最大堆大小。X
表示非标准选项,m
表示memory
,x
表示maximum
-
GC相关:
-XX:+UseSerialGC
:使用串行垃圾收集器。-XX:+UseParallelGC
:使用并行垃圾收集器。-XX:+UseConcMarkSweepGC
:使用CMS垃圾收集器。-XX:+UseG1GC
:使用G1垃圾收集器。
-
方法区:
-XX:MetaspaceSize=<size>
:方法区触发Full GC的阈值-XX:MaxMetaspaceSize=<size>
:方法区最大大小。
-
日志和调试:
-XX:+PrintGCDetails
:输出详细的GC日志。-verbose:gc
:输出简略的GC日志。
-
Dump文件
-XX:+DumpOnOutOfMemoryError
:发生OOM的时候生成内存快照Dump文件,默认在当前工作目录下-XX:HeapDumpPath=/path/to/dump/
:指定Dump文件的输出位置为/path/to/dump
示例:
假设需要优化一个生产环境中的Java应用程序,你可以这样设置JVM参数:
java -server -Xms1024m -Xmx1024m -XX:NewRatio=3 -XX:SurvivorRatio=8 -XX:+UseParallelGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Djava.awt.headless=true -jar your-app.jar
在这个示例中,我们设置了以下参数:
-server
:启用服务器模式,以获得更好的性能。-Xms1024m
和-Xmx1024m
:设置初始堆大小为1GB。-XX:NewRatio=3
:设置新生代与老年代的比例为1:3。-XX:SurvivorRatio=8
:设置Survivor空间与Eden空间的比例为1:8。-XX:+UseParallelGC
:使用并行垃圾收集器。-XX:+PrintGCDetails
和-XX:+PrintGCTimeStamps
:输出详细的GC日志和时间戳。-Djava.awt.headless=true
:确保在无图形用户界面的环境中运行。
JProfile分析OOM原因
JProfile激活码:S-J14-NEO_PENG#890808-1jqjtz91lywcp9#23624
内存快照分析工具JProfile可以:
- 分析Dump内存文件,快速定位内存泄漏
- 获取堆中的数据
- 获得大的对象
获取Dump内存文件:
JVM启动参数:-XX:+HeapDumpOnOutOfMemoryError
发生堆内存溢出(OOM)的时候就会保存堆内存快照为Dump文件,默认的Dump文件路径是项目的src
文件夹所在的目录。
JProfile分析OOM一般流程:
示例:
启动参数:-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
import java.util.ArrayList;
import java.util.List;
public class Demo {
public static List<Object> list = new ArrayList<>();
public static int count = 0;
public static void main(String[] args) {
try {
while (true) {
list.add(new int[1024 * 1024]);
count++;
}
} catch (Error e) {
System.out.println("error occurs while count = " + count);
}
}
}
-
运行时加入JVM启动参数:
-XX:+HeapDumpOnOutOfMemoryError
-
运行,出现错误后会在工作目录下生成Dump文件
- 将Dump文件导入到JProfile中,查看
堆遍历器(Heap Walker)
中的当前对象集(Current Object Set)
中的最大对象(Biggest Object)
。从下图例子可以得知,是一个ArrayList
对象占用了过多的内存。
-
查看
堆遍历器(Heap Walker)
中的线程转储(Thread Dump)
可以查看当前的所有线程情况,查看可知main
线程代码第13行出现了问题,由此确定有问题的对象与代码位置。
垃圾回收的触发条件
Full GC
:会对整个堆,其中包括新生代(eden,s0,s1),老年代,永久代(如果存在)或元空间进行垃圾回收。
Minor GC
:也叫Young GC
。针对新生代进行垃圾回收。
触发Minor GC
的条件:
- 当
young gen
的Eden
区满时,触发young GC
,部分存活的对象会晋升到old gen
。
触发Full GC
的条件:(满足其一即可)
- 准备触发
young GC
时,但平均晋升大小比old gen
剩余空间大 - 要在
prem gen
分配空间时,若prem gen
已经没有足够的空间(Java8 以前) - 调用
System.gc()
时,系统建议执行Full GC
,但不必然执行 - 当创建一个大对象时,
Eden
区域当中放不下这个大对象,会直接保存在old gen
当中,如果old gen
空间也不足,就会触发Full GC
; - 在新生代回收内存时,由
Eden
区和Survivor From
区把存活的对象向Survivor To
区复制时,对象大小大于Survivor To
空间的可用内存,则把该对象转存到old gen
(这个过程称为分配担保),且old gen
的可用内存小于该对象大小。即old gen
无法存放下young gen
过渡到old gen
的对象的时候,便会触发Full GC
。 - Java8之后,
prem gen
被在本地内存的Metaspace
取代。Metaspace
使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace
的大小只与本地内存大小有关。-XX:PermSize=N
(约为20.8MB)超过这个值就会引发Full GC
,这个值不是固定的,是会随着JVM的运行进行动态调整的,
JMM模型
JMM模型: