目录
[JVM 内存结构](#JVM 内存结构)
[一. 程序计数器](#一. 程序计数器)
[二. 虚拟机栈](#二. 虚拟机栈)
[1. 基本概念](#1. 基本概念)
[2. 常见问题辨析](#2. 常见问题辨析)
[3. 栈内存溢出问题](#3. 栈内存溢出问题)
[4. 线程运行诊断](#4. 线程运行诊断)
[(1) CPU 占用过高](#(1) CPU 占用过高)
[(2) 程序运行很长时间没有结果](#(2) 程序运行很长时间没有结果)
[三. 本地方法栈](#三. 本地方法栈)
[四. 堆](#四. 堆)
[1. 定义](#1. 定义)
[2. 堆内存溢出](#2. 堆内存溢出)
[3. 堆内存诊断工具](#3. 堆内存诊断工具)
[五. 方法区](#五. 方法区)
[1. 定义](#1. 定义)
[2. 组成](#2. 组成)
[3. 方法区内存溢出](#3. 方法区内存溢出)
[4. 运行时常量池](#4. 运行时常量池)
[5. StringTable (字符串常量池)](#5. StringTable (字符串常量池))
[(1) StringTable 的特性](#(1) StringTable 的特性)
[(2) StringTable 位置](#(2) StringTable 位置)
[(3) StringTable 垃圾回收](#(3) StringTable 垃圾回收)
[(4) StringTable 性能调优](#(4) StringTable 性能调优)
[六. 直接内存](#六. 直接内存)
[1. 定义](#1. 定义)
[2. 分配和回收原理](#2. 分配和回收原理)
JVM 内存结构

一. 程序计数器
java 源代码 --(javac) --> 字节码 --(类加载器) --> 机器码
程序计数器是线程私有的, 用于记录当前线程下一条 jvm 指令的执行地址.
-
每个线程都有自己的程序计数器.
-
程序计数器不存在内存溢出问题 (jvm 规范).
二. 虚拟机栈
1. 基本概念
虚拟机栈 (简称"栈").
-
每个线程运行时所需要的内存, 就称为虚拟机栈.
-
每个栈由多个栈帧 (Frame) 组成, 一个栈帧对应一次方法的调用, (每个方法执行所需要的内存, 就称为栈帧).
-
每个线程只能有一个活动栈帧, 对饮当前正在执行的那个方法.
2. 常见问题辨析
-
栈内存是否与垃圾回收有关?
不涉及. 方法调用时创建栈帧入栈, 方法执行完毕后栈帧自动出栈并释放内存.
-
栈内存分配越大越好吗?
不是.
栈内存过大 可能会导致同时执行的线程数量减少, 程序运行效率降低 ; 内存资源浪费.
栈内存过小 可能会导致栈溢出问题.
所以我们应当合理设置栈内存大小.
- 方法内的局部变量是否存在线程安全问题?
不存在. 局部变量存储在线程私有的栈内存中, 不同线程的栈内存完全隔离, 互不干扰.
注:
-
如果方法内的局部变量(对象) 没有超出方法的作用范围, 则是线程安全的.
-
如果方法内的局部变量(对象) 超出了方法的作用范围, 则不是线程安全的.
3. 栈内存溢出问题
-
栈帧过多导致栈内存溢出 (eg: 方法调用层级过深, 深度递归调用, 两个类之间的循环引用).
-
栈帧过大导致栈内存溢出 (eg: 栈内存设置过小, 局部变量占用空间过大) .
4. 线程运行诊断
(1) CPU 占用过高
定位:
-
先用 top 命令定位哪个进程对 CPU 的占用过高.
-
用 ps 命令进一步定位是哪个线程引起的 CPU 占用过高 (ps H -eo pid,tid,%cpu | grep 进程id).
-
jstack 进程id: 根据线程id找到有问题的线程, 进一步定位到有问题的源代码行数.
(2) 程序运行很长时间没有结果
有可能是多个线程发生死锁 导致程序运行不出来结果. 可以通过 jstack 进程id 来定位问题.
三. 本地方法栈
为 本地方法的调用 提供 内存空间.
四. 堆
1. 定义
使用 new 关键字创建的对象会存放到堆内存中.
特点:
-
堆是线程共享的, 堆中的对象都需要考虑线程安全问题.
-
堆有垃圾回收机制.
2. 堆内存溢出
java.lang.OutOfMemoryError: Java heap space
循环创建大量对象.
业务逻辑导致对象占用持续增长.
3. 堆内存诊断工具
-
jps 工具
- 查看当前 java 进程中 ++有哪些 java 进程++.
-
jmap 工具
-
查看 (某时刻) 堆内存占用情况. (无法连续监测)
jmap -heap 进程ID
-
-
jconsole 工具
-
图形界面的 多功能监测工具 (可连续监测)
-
-
jvisualvm
java可视化虚拟机. 用可视化的方式展现虚拟机信息.
4.案例
- 垃圾回收之后, 内存占用仍然很高, 是什么情况?
五. 方法区
1. 定义
方法区只是一个规范, 不同的厂商有不同的实现方式. (对于 Oracle HotSpot 虚拟机: 方法区在jdk1.8之前的实现是永久代, 在jdk1.8之后的实现是元空间)
方法区是一块线程共享的区域, 主要用于存储 类的元数据信息, 常量池 和 静态变量.

2. 组成
方法区: 类 (class) + 类加载器 (ClassLoader) + 运行时常量池 (StringTable).

3. 方法区内存溢出
-
jdk1.8 之前: 永久代内存溢出.
# 演示永久代内存溢出 -XX:MaxPermSize=8m java.lang.OutOfMemoryError: PermGen space
-
jdk 1.8 之后: 元空间内存溢出.
# 演示元空间内存溢出 -XX:MaxMetaspaceSize=8m java.lang.OutOfMemoryError: Metaspace
类加载器 用于 加载类的二进制字节码.
如果加载的类过多, 会导致方法区内存溢出. 如下代码, 加载 10000 个类, 同时元空间最大容量是8M, 就会导致方法区内存溢出.
/**
* 演示元空间内存溢出
* -XX:MaxMetaspaceSize=8m
*/
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 10000; i++, j++) {
// ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
// 版本号, public, 类名, 包名, 父类, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[]
byte[] code = cw.toByteArray();
// 执行了类的加载
test.defineClass("Class" + i, code, 0, code.length);
}
} finally {
System.out.println(j);
}
}
}
4. 运行时常量池
类基本信息, 常量池, 类方法定义.
-
常量池: 就是一张表, 虚拟机指令根据这张常量表找到要执行的 类名, 方法名, 参数类型, 字面量 等信息.
-
运行时常量池 : 常量池是在 .class 文件 中的. 当该类被加载, 它的常量池信息就会放入运行时常量池, 并把里面的符号地址变为真实地址.
5. StringTable (字符串常量池)
(1) StringTable 的特性
-
常量池中的字符串仅是符号, 第一次用到时才变为对象.
当 Java 代码被编译成 .class 文件时, 字符串字面量 (如 "abc") 会以符号形式存储在 .class 文件的常量池中. 此时它们还不是真正的 Java 对象, 只是一种标记或引用符号, 用于表示这段字符串的内容.
只有当类被加载到 JVM 中, 并且程序第一次使用到这个字符串时 (比如 赋值给变量, 作为方法参数等): 此时 JVM 才会在堆中创建 对应的字符串对象, 并将该对象的引用存入字符串常量池 (StringTable).
// StringTable [ "a", "b", "ab" ] hashtable结构,不能扩容 public class Demo1_22 { // 常量池中的信息,都会被加载到运行时常量池中. 这时 a b ab 都是常量池中的符号, 还没有变为 java 字符串对象 // ldc #2 会把 a 符号变为 "a" 字符串对象 // ldc #3 会把 b 符号变为 "b" 字符串对象 // ldc #4 会把 ab 符号变为 "ab" 字符串对象 public static void main(String[] args) { String s1 = "a"; // 懒惰的 String s2 = "b"; String s3 = "ab"; String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab") String s5 = "a" + "b"; // javac 在编译期间的优化 System.out.println(s3 == s5); } }
-
利用串池的机制, 来避免重复创建字符串对象.
-
字符串变量拼接的原理是 StringBuilder (jdk 1.8).
-
字符串常量拼接的原理是编译期优化.
-
可以使用 intern 方法, 主动将串池中还没有的字符串对象放入串池.
-
jdk1.8: intern 方法将字符串对象尝试放入字符串常量池. 如果串池中有则不会放入 ; 如果串池中没有则放入串池, 最后返回串池中的对象.
public class Demo23 { // ["a", "b", "ab"] public static void main(String[] args) { // 堆 new String("a") new String("b") new String("ab") String s = new String("a") + new String("b"); // intern: 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,最后把串池中的对象返回 String s2 = s.intern(); System.out.println( s2 == x); //true System.out.println( s == "ab"); //false } }
- jdk1.6: intern 方法将字符串对象尝试放入字符串常量池. 如果串池中有则不会放入 ; 如果串池中没有, 则将此对象复制一份 再放入串池, 最后返回串池中的对象.
-
(2) StringTable 位置
-
在 jdk1.6 及以前: 字符串常量池位于永久代 (方法区) 中.
-
在 jdk1.7 及以后: 字符串常量池位于堆中.
++做此迁移的原因++: 永久代内存的回收效率很低, 在 FullGC 时才会触发永久代的垃圾回收 (触发时机晚, 触发频率太少), 所以就导致 StringTable 的回收效率不高, 当字符串常量过多时, 很容易触发永久代内存溢出 ; 而在堆中, MinorGC 就会触发垃圾回收, 回收频率提高, 大大降低了内存溢出的风险.

(3) StringTable 垃圾回收
-
在 jdk1.6 前: StringTable 位于永久代中, 基本不涉及垃圾回收.
-
在 jdk1.7 后: StringTable 位于堆中, 会随着堆区进行垃圾回收.
(4) StringTable 性能调优
-
如果有字符串数量很大 --> 可以通过 -XX:StringTableSize=xxx 调整底层 HashTable 的++桶的个数++. 可以减少哈希冲突,提升字符串的查找效率加快读取速度.
-
如果有大量字符串 且 字符串存在重复的问题 --> 可以考虑将字符串对象是否入池 (避免重复的字符串入池) ---> 使用 intern().
-
减少不必要的字符串创建. 创建过多不必要的字符串会增加 StringTable 的负担, 占用更多内存, 还可能导致哈希冲突增多.
六. 直接内存
1. 定义
直接内存 (Direct Memory) 是 JVM 向操作系统"申请" 后, 由操作系统直接分配的内存. 不占用 JVM 堆内存空间, 也不占用系统内存空间. Java 代码可以直接访问, 操作系统也可以直接访问.
直接内存不受 JVM 垃圾回收的管理 (回收需显式调用或依赖操作系统机制).
-
常见于 NIO 操作时, 用于数据缓冲区
-
分配回收成本较高, 但读写性能高
-
不受 JVM 内存回收管理
为什么使用了直接内存, 大文件的读写效率就会非常高?
- 不使用直接内存的文件读写过程:
-
Java 本身不具备磁盘读写的能力, 想要进行磁盘读写 必须调用操作系统提供函数 (调用本地方法). [状态切换: 用户态 --> 内核态].
-
在内核态下去真正读取磁盘文件内容, 磁盘内容会先读入到系统缓冲区中, 然后再将数据从系统缓冲区读到 java 缓冲区中. 这样就导致了一次不必要的数据复制, 使得读写效率下降.

- 使用直接内存的文件读写过程:
-
Java 程序从用户态切换到内核态.
-
在内核态下读取磁盘文件内容到直接内存 (direct memory) 中, java 代码和系统都可以直接从 direct memory 中读取数据, 减少了一步缓冲区复制操作, 提高了效率.

2. 分配和回收原理
-
使用 Unsafe 对象 完成直接内存的 分配和回收, 并且回收需要主动调用 freeMemory() 方法.
-
ByteBuffer 的实现类内部使用了 Cleaner (虚引用) 来监测 ByteBuffer 对象, 一旦 ByteBuffer 对象被垃圾回收, 就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存.