简介
更多信息查看官网
JVM,全称是 Java Virtual Machine,Java运行环境(Java二进制字节码的运行环境)
优点:
- 是Java一次编写,到处运行的基石。
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态
JVM有很多,常见的JVM有:HotSpot Opent JDK edition(平时使用的)、Eclipse OpenJ9等
内存架构
程序计数器
Program Counter Register,程序计数器(寄存器):作用是记住下一条jvm指令的执行地址,在物理上,是通过cpu中的寄存器来实现的。他是线程私有,且是唯一一个不会存在内存溢出的区域。
工作流程:java源码编译为二进制字节码,字节码中包含jvm指令,这些指令交给解释器->机器码->CPU执行。
1 | // 二进制字节码 java 源代码 |
虚拟机栈
Java Virtual Machine Stacks(Java 虚拟机栈):
- 虚拟机栈是每个线程运行需要的内存空间,原理是数据结构中的栈,先进后出;
- 每个栈由多个栈帧(Frame,栈帧中包括参数、局部变量、返回地址等)组成,对应着每次方法调用时所占用的内存;
- 每个线程只能有一个活动栈帧,一个栈帧表示一个方法的调用,对应着当前正在执行的那个方法,如果一个方法里面调用了其他方法,后续方法会依次压入栈。
垃圾回收没有涉及管理栈内存
方法内的局部变量没有逃离方法的作用范围时,它是线程安全的;如果是局部变量引用了对象,并逃离方法的作用方法,需要考虑线程安全。
内存溢出
此外,某些情况会引起栈内存溢出:
- 栈帧过多导致占内存溢出,如果方法递归没有正确结束,就会导致栈内存溢出。
- 栈帧过大导致栈帧内存溢出。
参数:-Xss 指定栈内存大小,如 - Xss 1024k
本地方法栈
给本地方法的运行提供内存运行空间,如Object中的native方法。
堆
通过new关键字创建的对象,都会使用堆内存。它是线程共享的,堆中的对象需要考虑线程安全问题;并且它有垃圾回收机制
但是存储的对象太大会导致堆内存溢出
参数: -Xmx 指定堆内存空间大小,如 - Xmx 1024M
内存溢出
可以使用堆内存诊断工具(JDK内)来监测堆:
- jsp工具(查看当前系统中有哪些java程序,可以查看当前Java程序中有那些线程)
- Jmap工具(查看堆内存占用情况:Jmap -head 进程id)
- jconsole(图像界面的多功能检测工具,可以连续监测)
- jvisualvm(和jconsole有点像)
OOM 问题如何定位和解决?
OOM(OutOfMemoryError)通常不是单纯的“堆不够大”,常见原因包括:
- 一次性创建或加载了过多对象,例如查询结果未分页、批量数据一次性全部读入内存。
- 对象已经不再使用,但仍然被 GC Roots 引用,导致无法回收,形成内存泄漏。
- JVM 堆、元空间、线程栈或直接内存配置不足。
系统已经发生 OOM
启动参数中提前配置 Heap Dump:
1 | -XX:+HeapDumpOnOutOfMemoryError |
需要提前确认 dump 目录有足够磁盘空间。服务发生 OOM 时,JVM 会自动导出堆转储文件。使用 VisualVM、MAT 等工具打开该文件,重点查看:
- 占用内存较大的对象类型;
- 对象数量异常增长的集合或缓存;
- 对象到 GC Roots 的引用链,定位仍然持有这些对象的线程、静态变量或其他对象。
系统仍在运行
可以先查看堆概况和对象分布:
1 | jmap -heap PID |
必要时导出堆转储:
1 | jmap -dump:live,format=b,file=/path/to/heap-dump.hprof PID |
也可以使用 Arthas 等诊断工具辅助观察。导出堆转储可能触发较长时间的 Stop-The-World,应结合业务流量和磁盘空间评估操作时机。
排查思路
- 先确认是哪一类内存区域耗尽,以及 OOM 发生的具体异常信息。
- 结合 GC 日志、
jstat -gc PID和对象直方图,判断是对象创建过快还是对象无法回收。 - 使用 Heap Dump 分析对象数量、保留大小和 GC Roots 引用链。
- 回到业务代码检查集合、缓存、监听器、连接、线程本地变量等长生命周期引用,并修复根因。
方法区
方法区(Method Area)也被称为永久代。方法区用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
内存溢出
- 1.8 以前会导致永久代内存溢出
1 | 永久代内存溢出 java.lang.0utOfMemoryError:PermGen space |
- 1.8 之后会导致元空间内存溢出
1 | 元空间内存溢出 java.lang.0utOfMemoryError: Metaspace |
1.8后,元空间使用的系统内存,一般不容易内存溢出
运行时常量池
运行时常量池(JDK 8 之前是永久代,JDK 8 及之后是元空间),Class 文件中除了有类的版本、字段、方法、接口等描述信息,还有一项信息是常量池(Constant Pool Table),就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息,这部分内容会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池。
🔔 注意:当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常
字符串常量池
为了极致地优化字符串性能和减少内存开销,JVM 设计了一块特殊的存储区域——字符串常量池(StringTable[])。
JDK6 时,字符串常量池存在于运行时常量池之中,而运行时常量池在方法区(永久代)。而JDK 7 及之后:字符串常量池被移动到了 Java 堆中。
至于为什么移动,原因有两点:
- 永久代空间有限且垃圾回收条件苛刻,容易发生 OutOfMemoryError: PermGen space。
- 堆空间更大,并且垃圾回收机制更高效。将字符串常量池放在堆中,可以更及时地回收不再使用的字符串对象,从而减少内存泄漏的风险。
StringTable 运行逻辑
- 1.当代码中出现字面量(直接双引号声明)方式创建字符串时(例如 String s = “abc”;),JVM 会首先去字符串常量池中查找是否存在内容相等的字符串。
- 2.如果存在,则直接返回池中该字符串的引用;如果不存在,则在字符串常量池中创建一个新的字符串对象,然后返回其引用。
- 3.使用 new 关键字(例如 String s = new String(“abc”);)会在堆上(注意,不是池里)创建一个新的字符串对象。但如果字面量 “abc” 之前不在池中,这个操作也会导致池中先创建 “abc”。
StringTable的底层逻辑可细看下面demo图:
StringTable特点:
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用字符串常量池的机制,来避免重复创建字符串
- 对象字符串变量拼接的原理是 StringBuilder (1.8),如下面的:
s1+s2 - 字符串常量拼接的原理是编译期优化
- 可以使用 inten 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
常量字符串拼接底层原理:
1 | String s1 = "a"; |
StringTable 有垃圾回收机制
StringTable 调优
- StringTable 性能调优,它的底层是数组+红黑树,调优主要就是设置桶的个数,去调整哈希分布,减少哈希冲突;可以通过
-XX:StringTableSize=桶个数设置桶的个数,让哈希查找变快。 - 考虑字符串对象是否入池,程序中存在大量字符串可以考虑入池减少内存的使用。
面试题
1 | String s1 = "a"; |
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 JVM 规范中定义的内存区域。
是操作系统的内存,分配回收成本较高,但读写性能高。
在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
🔔 注意:直接内存这部分也被频繁的使用,且也可能导致 OutOfMemoryError 异常。
💡 提示:直接内存容量可通过 -XX:MaxDirectMemorySize 指定,如果不指定,则默认与 Java 堆最大值(-Xmx 指定)一样。
分配和回收机制
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffer 的实现类内部,使用了 Cleaner(虚引用)来监测 ByteBufer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法用 freeMemory 来释放直接内存
Java 内存区域对比
| 内存区域 | 内存作用范围 | 异常 |
|---|---|---|
| 程序计数器 | 线程私有 | 无 |
| Java 虚拟机栈 | 线程私有 | StackOverflowError 和 OutOfMemoryError |
| 本地方法栈 | 线程私有 | StackOverflowError 和 OutOfMemoryError |
| Java 堆 | 线程共享 | OutOfMemoryError |
| 方法区 | 线程共享 | OutOfMemoryError |
| 运行时常量池 | 线程共享 | OutOfMemoryError |
| 直接内存 | 非运行时数据区 | OutOfMemoryError |





