Files
www/blogs/java/jvm/JVM-内存结构.md
zhuhjay 22e48d9558 build(www): 添加 Drone CI 流水线配置
- 新增 .drone.yml 文件用于定义 CI/CD 流程
- 配置了基于 Docker 的部署步骤
- 设置了工作区和卷映射以支持持久化数据
- 添加了构建准备阶段和 Docker 部署阶段
- 定义了环境变量和代理设置
- 配置了 artifacts 目录的处理逻辑
- 添加了 timezone 映射以确保时间同步
- 设置了 docker.sock 映射以支持 Docker in Docker
2025-11-01 13:36:00 +08:00

1206 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: JVM之内存结构
date: 2022-11-22
sidebar: 'auto'
tags:
- JVM
categories:
- Java
---
## 1 程序计数器
![1669015637679](/jvm/1669015637679.png)
Program Counter Register 程序计数器(寄存器)
- 作用是记住下一条jvm指令的执行地址
- 特点
- 是线程私有的
- 不会存在内存溢出
## 2 虚拟机栈
![1669015814320](/jvm/1669015814320.png)
### 2.1 定义
Java Virtual Machine Stacks Java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧Frame组成对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问题辨析
1. 垃圾回收是否涉及栈内存?
- 每一个栈帧使用完后会自动释放该栈帧所对应的内存,垃圾回收只涉及到堆内存
2. 栈内存分配越大越好吗?
- 每个线程都会对应一个栈内存,如果栈内存分配越大则所能使用的线程数则减小,也对程序的执行起不到加快的作用
3. 方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
### 2.2 栈内存溢出
- 栈帧过多导致栈内存溢出
- 递归没有给到合适的出口
- 第三方组件使用不当导致循环引用ObjectMapper对象 -> Json
- 栈帧过大导致栈内存溢出(局部变量过多导致,但较难触发)
> Tip
>
> - 可以使用 `-Xss` 来改变栈内存的大小
>
> - 栈内存溢出的错误信息为:`java.lang.StackOverflowError`
### 2.3 线程运行诊断
eg1CPU占用过多
定位问题步骤Linux
-`top` 命令定位哪个进程对CPU的占用过高
- `ps H -eo pid,tid,%cpu | grep 进程id` (用`ps`命令进一步定义是哪个线程引起的cpu占用过高
- `jstack 进程id` 可以根据线程id找到有问题的线程进一步定位到问题代码的源码行号
eg2程序运行很长时间没有结果线程死锁
## 3 本地方法栈
![1669019510517](/jvm/1669019510517.png)
本地方法栈的作用类似于JVM虚拟机栈就是调用本地方法Native时会开辟一个本地方法的执行内存也就是本地方法栈其中有调用库函数的执行栈帧。
## 4 堆
![1669021362263](/jvm/1669021362263.png)
### 4.1 定义
Heap 堆
- 通过 new 关键字,创建对象都会使用堆内存
特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
### 4.2 堆内存溢出
当创建的对象足够多并且这些对象都正在被使用,不会被当成垃圾进行处理时,就会产生堆内存溢出的现象
> Tip
>
> - 可以使用 `-Xmx` 来改变堆内存的最大大小
> - 堆内存溢出的错误信息为:`java.lang.OutOfMemoryError: Java heap space`
### 4.3 堆内存诊断
有如下代码进行测试
```java
public class Demo1_4 {
public static void main(String[] args) throws InterruptedException {
System.out.println("1..");
Thread.sleep(30000);
// 分配 10M内存
byte[] bytes = new byte[1024 * 1024 * 10];
System.out.println("2..");
Thread.sleep(30000);
bytes = null;
// 进行一次垃圾回收
System.gc();
System.out.println("3..");
Thread.sleep(1000000L);
}
}
```
1. jps 工具:查看当前系统中有哪些 Java 进程
```shell
PS JVM> jps
12128 Demo1_4
12116 RemoteMavenServer
14968
7720 Jps
7800 Launcher
```
2. jmap 工具:查看堆内存占用情况 `jmap -heap 进程id`
- 在输出1时的堆内存情况
```tex
Heap Usage: # 堆内存占用情况
PS Young Generation
Eden Space: # 新创建对象使用的分区
capacity = 34078720 (32.5MB) # 堆内存容量
used = 4771568 (4.5505218505859375MB) # 堆内存已使用情况
free = 29307152 (27.949478149414062MB) # 可分配
14.001605694110577% used
```
- 在输出2时的堆内存情况因为创建了一个 10M 的字节数组,所以此时堆内存使用量比上次多了 10M 左右的使用
```tex
Heap Usage: # 堆内存占用情况
PS Young Generation
Eden Space: # 新创建对象使用的分区
capacity = 34078720 (32.5MB) # 堆内存容量
used = 15257344 (14.550537109375MB) # 堆内存已使用情况
free = 18821376 (17.949462890625MB) # 可分配
44.77088341346154% used
```
- 在输出3时的堆内存情况进行了垃圾回收将分配的 10M 字节数组进行回收,使得堆内存使用量大大减少
```tex
Heap Usage: # 堆内存占用情况
PS Young Generation
Eden Space: # 新创建对象使用的分区
capacity = 34078720 (32.5MB) # 堆内存容量
used = 681592 (0.6500167846679688MB) # 堆内存已使用情况
free = 33397128 (31.84998321533203MB) # 可分配
2.0000516451322117% used
```
3. jconsole 工具:图形界面,多功能的监测工具,可以连续监测
分为三个阶段的使用情况,阶段一为初始阶段。阶段二时分配了一段内存,阶段三进行了垃圾回收
![1669024208924](/jvm/1669024208924.png)
4. jvisualvm 工具:与 jconsole 类似的图形化界面,但是该用具可以 dump堆内存快照可以查看堆中存活的对象信息
- 案例/代码:垃圾回收后,内存占用仍然很高
```java
public class Demo1_6 {
public static void main(String[] args) throws InterruptedException {
List<Student> list = new ArrayList<>(200);
for (int i = 0; i < 200; i++) {
list.add(new Student());
}
// 为了调试而休眠
Thread.sleep(1000000000L);
}
}
class Student {
private byte[] big = new byte[1024 * 1024];
}
```
- 使用 `jps` 命令获取到该程序的进程id使用 `jmap -heap 进程id` 查看堆内存使用
可以发现堆内存总共占用了 (16+187)M然后使用 `jconsole` 工具进行该进程的 gc 回收
```tex
Heap Usage:
PS Young Generation
Eden Space:
capacity = 116916224 (111.5MB)
used = 17285928 (16.485145568847656MB)
free = 99630296 (95.01485443115234MB)
14.784883918249019% used
PS Old Generation # 老年代堆内存使用情况
capacity = 334495744 (319.0MB)
used = 196736312 (187.62236785888672MB)
free = 137759432 (131.37763214111328MB)
58.815789297456654% used
```
可以发现,虽然进行了 gc 垃圾回收,但是堆内存占用还是居高不下,使用 jconsole 工具已经没办法进一步查询到具体问题了
![1669043972054](/jvm/1669043972054.png)
- 使用 jvisualvm 进行堆内存 dump
使用命令 `jvisualvm` 打开可视化界面,选择当前执行的进程,查看详细信息,然后进行堆内存 dump
![1669044220427](/jvm/1669044220427.png)
可以发现当前的 ArrayList 对象占用的内存高达约 200M 左右的内存,点击进去查看该对象的详细信息
![1669044331372](/jvm/1669044331372.png)
在 ArrayList 对象中的每一个元素都占用了约 1M 的内存大小一共有200个元素则总占用就有了大约 200M 的堆内存空间,由此快速定位到代码中存在的问题
![1669044475571](/jvm/1669044475571.png)
## 5 方法区
![1669044952813](/jvm/1669044952813.png)
### 5.1 定义
[Java 虚拟机规范](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html)
以下是官方对方法区的定义
> Java 虚拟机具有 在所有 Java 虚拟机之间共享*的方法区域* 线程。方法区域类似于已编译的存储区域传统语言的代码或类似于“文本”段的代码 操作系统进程。它存储每个类结构,如运行时常量池、字段和方法数据以及方法和构造函数,包括特殊方法 [§2.9](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.9) 用于类和实例初始化 和接口初始化。
>
> 创建方法区域 在虚拟机启动时。虽然方法区域在逻辑上是作为堆的一部分,简单的实现可以选择不垃圾收集或压缩它。这规范不要求方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小或可根据计算需要进行扩展,并且可以如果不需要更大的方法区域,则收缩。记忆对于方法区域不需要是连续的。
>
> Java 虚拟机实现可以为程序员或用户控制方法区域的初始大小,以及,对于不同尺寸的方法区域,控制最大值和最小方法区域大小。
>
> 以下特殊条件与方法区域相关联:
>
> - 如果方法区域中的内存无法用于满足分配请求Java 虚拟机抛出一个。`OutOfMemoryError`
### 5.2 组成
JDK6 和 JDK8 的内存结构发生了变化
> **注意**:图中的 常量池 指的是 运行时常量池
![1669045525089](/jvm/1669045525089.png)
### 5.3 方法区内存溢出
- JDK1.8 以前会导致永久代内存溢出:`java.lang.OutOfMemoryError: PermGen space`
- JDK1.8 以后会导致元空间内存溢出:`java.lang.OutOfMemoryError: Metaspace`
演示 JDK1.8 以后元空间内存溢出JDK1.6 代码几乎相同,永久代使用 `-XX:MaxPermSize=8m`
```java
public class Demo1_7 extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
Demo1_7 test = new Demo1_7();
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);
}
}
}
```
在 1.8 以后元空间使用的是系统没存,没有限制大小,不会发生溢出的情况,需要设置 `-XX:MaxMetaspaceSize=8m`
```tex
5411
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.zhuhjay.Demo1_7.main(Demo1_7.java:23)
```
方法区内存溢出场景:
- Spring
- Mybatis
这些场景都会使用 CGLIB 等代理技术动态的生成字节码,使用不当可能会导致方法区内存溢出的情况。
### 5.4 运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
> **Tip**: 常量池的概念解析
>
> 编译运行以下代码
>
> ```java
> public class Demo1_8 {
> public static void main(String[] args) {
> System.out.println("hello world");
> }
> }
> ```
>
> 将编译后的 class 文件进行反编译 `javap -v Demo1_8.class`
>
> - 源码 1-8 行是文件的 类的基本信息
> - 源码 9-42 行是 常量池信息
> - 源码 44-72 行是 类方法定义信息
> - **当 class 文件被运行之后,所有的 `#..` 都会被赋予真实的物理地址信息**
>
> ```tex
> Classfile /G:/Idea_workspace/JVM/target/classes/com/zhuhjay/Demo1_8.class
> Last modified 2022-11-22; size 548 bytes
> MD5 checksum 551e83a2ede53badac918978a8846964
> Compiled from "Demo1_8.java"
> public class com.zhuhjay.Demo1_8
> minor version: 0
> major version: 52
> flags: ACC_PUBLIC, ACC_SUPER
> Constant pool:
> #1 = Methodref #6.#20 // java/lang/Object."<init>":()V
> #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
> #3 = String #23 // hello world
> #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
> #5 = Class #26 // com/zhuhjay/Demo1_8
> #6 = Class #27 // java/lang/Object
> #7 = Utf8 <init>
> #8 = Utf8 ()V
> #9 = Utf8 Code
> #10 = Utf8 LineNumberTable
> #11 = Utf8 LocalVariableTable
> #12 = Utf8 this
> #13 = Utf8 Lcom/zhuhjay/Demo1_8;
> #14 = Utf8 main
> #15 = Utf8 ([Ljava/lang/String;)V
> #16 = Utf8 args
> #17 = Utf8 [Ljava/lang/String;
> #18 = Utf8 SourceFile
> #19 = Utf8 Demo1_8.java
> #20 = NameAndType #7:#8 // "<init>":()V
> #21 = Class #28 // java/lang/System
> #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
> #23 = Utf8 hello world
> #24 = Class #31 // java/io/PrintStream
> #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
> #26 = Utf8 com/zhuhjay/Demo1_8
> #27 = Utf8 java/lang/Object
> #28 = Utf8 java/lang/System
> #29 = Utf8 out
> #30 = Utf8 Ljava/io/PrintStream;
> #31 = Utf8 java/io/PrintStream
> #32 = Utf8 println
> #33 = Utf8 (Ljava/lang/String;)V
> {
> public com.zhuhjay.Demo1_8();
> descriptor: ()V
> flags: ACC_PUBLIC
> Code:
> stack=1, locals=1, args_size=1
> 0: aload_0
> 1: invokespecial #1 // Method java/lang/Object."<init>":()V
> 4: return
> LineNumberTable:
> line 7: 0
> LocalVariableTable:
> Start Length Slot Name Signature
> 0 5 0 this Lcom/zhuhjay/Demo1_8;
>
> public static void main(java.lang.String[]);
> descriptor: ([Ljava/lang/String;)V
> flags: ACC_PUBLIC, ACC_STATIC
> Code:
> stack=2, locals=1, args_size=1
> 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
> 3: ldc #3 // String hello world
> 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
> 8: return
> LineNumberTable:
> line 9: 0
> line 10: 8
> LocalVariableTable:
> Start Length Slot Name Signature
> 0 9 0 args [Ljava/lang/String;
> }
> SourceFile: "Demo1_8.java"
> ```
>
> 进行main方法信息进行分析 62-66 行代码截取在CPU中是不会读取到 `//` 后面的注释信息的,这些注释信息是为了让程序员快速明白的注释信息。
>
> ```tex
> stack=2, locals=1, args_size=1
> 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
> 3: ldc #3 // String hello world
> 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
> 8: return
> ```
>
> - `0: getstatic #2`
>
> - getstatic获取类的成员变量
>
> - #2去常量池 `Constant pool` 中查找对应的信息
>
> 1. `#2 = Fieldref #21.#22`:需要找到地址 `#21.#22`,该组成为 成员变量的引用
>
> 2. `#21 = Class #28`:需要找到地址 `#28`,该地址信息为 Class
> 3. `#28 = Utf8 java/lang/System`:找到 `System` 类名
> 4. `#22 = NameAndType #29:#30`:需要找到 方法名和其类型
> 5. `#29 = Utf8 out`:找到调用的静态方法
> 6. `#30 = Utf8 Ljava/io/PrintStream;`:找到类型
>
> - 即该条命令就是为了找到该结果 `Field java/lang/System.out:Ljava/io/PrintStream;`,这也就是源码的 `System.out`
>
> - `3: ldc #3`
>
> - ldc找到一个地址并作为下一次指令的参数
> - #3
> 1. `#3 = String #23`:需要找到地址 `#23`
> 2. `#23 = Utf8 hello world`:找到类型为 `Utf8` 的字符串
> - 即该条指令就是为了找到结果 `hello world`
>
> - `5: invokevirtual #4`
>
> - invokevirtual执行一次虚方法调用方法调用
> - #4
> 1. `#4 = Methodref #24.#25`:需要找到地址 `#24.#25`,该组为 方法引用
> 2. `#24 = Class #31`:需要找到地址 `#31`,是一个 Class 对象
> 3. `#31 = Utf8 java/io/PrintStream`:一个输出流的类名
> 4. `#25 = NameAndType #32:#33`:需要找到地址 `#32:#33`,方法名及其类型
> 5. `#32 = Utf8 println`:找到需要调用的方法名称
> 6. `#33 = Utf8 (Ljava/lang/String;)V`:参数类型为 String无返回值
> - 即该条指令就是为了找到结果 `Method java/io/PrintStream.println:(Ljava/lang/String;)V`,也就是源码中的 `println("hello world")`,需要将上一个指令的数据作为方法参数
### 5.5 StringTable
用来存储字符串的常量池,**使用硬编码的字符串都会被存储到字符串常量池中**
eg:
- `new String("a") ` 会被放入字符串常量池和堆中,返回值是堆中的引用
- `"a"` 会被直接放到字符串常量池中,返回值是串池中的引用
##### 5.5.1 解析引入
- 现有以下代码,将其进行编译后,使用 `javap` 对其进行反编译,查看常量池的信息
```java
public class Demo1_9 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}
```
- 编译结果如下(只展示部分)
- 常量池中的信息,都会被加载到运行时常量池中,这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
- `ldc #2` 会把 a 符号变为 "a" 字符串对象,然后到 StringTable 中去找是否存在该值,不存在则存入 StringTable 中
- StringTable 是 hashtable 结构,不能扩容,一创建就固定了大小
- `ldc #3` `ldc #4` 同上
- `astore_1` 即是将上一条指令的内容存入到方法栈桢的变量表的 1 位置上
```tex
Constant pool:
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab
public static void main(java.lang.String[]);
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
LocalVariableTable: # main方法栈帧的变量表
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
```
##### 5.5.2 变量拼接
- 现对源代码进行改变如下,需要了解字符串变量拼接的原理,以及 `s3 == s4` 是否成立
```java
public class Demo1_9 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
}
}
```
- 进行反编译(只展示部分),根据如下执行的指令,可以发现
- `String s4 = s1 + s2;` 是通过 `new StringBuilder().append("a").append("b").toString()` 来进行组成的字符串
- StringBuilder 中的 toString 方法就是通过 `new String("ab")` 在堆空间中创建一个对象
- **以上即可得出 `s3 == s4` 不成立s3 变量指向串池s4 变量指向堆中的对象**
```tex
Constant pool:
#2 = String #30 // a
#3 = String #31 // b
#4 = String #32 // ab
#5 = Class #33 // java/lang/StringBuilder
#6 = Methodref #5.#29 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#35 // java/lang/StringBuilder.toString:()Ljava/lang/String;
public static void main(java.lang.String[]);
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: return
```
##### 5.5.3 常量拼接
- 现对源代码进行改变如下,需要了解字符串常量拼接的原理,以及 `s3 == s5` 是否成立
```java
public class Demo1_9 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
}
}
```
- 进行反编译(只展示部分),根据如下执行的指令,可以发现
- `String s5 = "a" + "b";` 直接往 StringTable 串池中查找 "ab"
- 发生以上的原因javac 在编译期间的优化,结果已经在编译器确定为 "ab"
- **即得出 `s3 == s5` 成立s3 和 s5 都是来自串池中的 "ab",属于同一个对象**
```tex
public static void main(java.lang.String[]);
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: ldc #4 // String ab
31: astore 5
33: return
```
##### 5.5.4 intern (JDK1.8)
- 现对源代码进行改变如下,需要了解 String.intern() 方法的原理,以及 `s6 == s7` 是否成立
```java
public class Demo1_9 {
public static void main(String[] args) {
String s6 = new String("c") + new String("d");
String s7 = "cd";
String intern = s6.intern();
}
}
```
- 进行反编译(只展示部分),根据如下执行的指令,可以发现
- 通过 `new String("c")` 方法,会在堆内存和常量池中各创建一份数据,然后通过 变量拼接 的方式在堆内存中生成一份 `cd`,但此数据没有入串池中
- `String s7 = "cd";` 此时串池中没有存在该数据,所以入串池操作
- `s6.intern();` 将该字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
- **程序执行完后,会发现 s6 属于堆内存中的对象,而 s7 属于串池中的对象,所以 `s6 == s7` 不成立**
```tex
public static void main(java.lang.String[]);
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: new #4 // class java/lang/String
10: dup
11: ldc #5 // String c
13: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: new #4 // class java/lang/String
22: dup
23: ldc #8 // String d
25: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: ldc #10 // String cd
37: astore_2
38: aload_1
39: invokevirtual #11 // Method java/lang/String.intern:()Ljava/lang/String;
42: astore_3
43: return
```
> 思考:
>
> 将以上代码修改为以下代码,`s6 == s7` 又会是怎样的结果?
>
> ```java
> public class Demo1_9 {
> public static void main(String[] args) {
> String s6 = new String("c") + new String("d");
> String intern = s6.intern();
> String s7 = "cd";
> }
> }
> ```
>
> 分析:
>
> 1. 执行完 s6 变量赋值过后,常量池和堆的数据如下
> - StringTable ["c", "d"]
> - Heap ['new String("c")', 'new String("d")', 'new String("cd")']
> 2. 执行完 s6.intern 后,会判断常量池中是否存在 "cd"
> - StringTable ["c", "d", "cd"] *实际上这里的 "cd" 是堆中 'new String("cd")' 的一个引用,是用一个对象*
> - Heap ['new String("c")', 'new String("d")', 'new String("cd")']
> 3. 执行到 s7 时,发现常量池中存在了 "cd",所以直接引用
>
> **综上所述,当前情况下的 `s6 == s7` 是成立的**
##### 5.5.5 intern (JDK1.6)
在 JDK1.6 中,以下执行的结果与 JDK1.8 的相同: `s6 == s7` 不成立
```java
public class Demo1_9 {
public static void main(String[] args) {
String s6 = new String("c") + new String("d");
String s7 = "cd";
String intern = s6.intern();
}
}
```
但是当改为以下代码时,结果却不尽相同
1. 执行完 s6 变量赋值过后,常量池和堆的数据如下
- StringTable ["c", "d"]
- Heap ['new String("c")', 'new String("d")', 'new String("cd")']
2. 执行完 s6.intern 后,会判断常量池中是否存在 "cd",如果不存在,那么将 **s6 拷贝一份放入常量池中,并返回**
- StringTable ["c", "d", "cd"] *在此时,"cd" 和 'new String("cd")' 不是同一个对象,因为是通过拷贝的方式*
- Heap ['new String("c")', 'new String("d")', 'new String("cd")']
3. 执行到 s7 时,发现常量池中存在了 "cd",所以直接引用
**综上所述,当前情况下的 `s6 == s7` 是不成立的**
```java
public class Demo1_9 {
public static void main(String[] args) {
String s6 = new String("c") + new String("d");
String intern = s6.intern();
String s7 = "cd";
}
}
```
##### 5.5.6 特性总结
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder1.8
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象**复制**一份,放入串池,会把串池中的对象返回
#### 5.6 StringTable 面试题
学习完 StringTable 字符串常量池后,能正确的理解回答下面的题目,那么恭喜你掌握了一个新的知识!
```java
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
// 问如果调换了【最后两行代码】的位置呢如果是jdk1.6呢?
System.out.println(x1 == x2);
```
答案:
```tex
false true true
jdk1.8: false true
jdk1.6: false false
```
#### 5.7 StringTable 位置
在 JDK1.6 时StringTable 是常量池的一部分,常量池存储在永久代中(在永久代中需要进行 Full GC 才会进行垃圾回收,回收效率低)
在 JDK1.8 时StringTable 是堆中的一部分,垃圾回收跟随堆进行,回收效率高
怎么证明以上两个版本存储的位置不相同?
- 在 JDK1.6 中,如果常量池中发生了内存溢出,那么就会报错 `java.lang.OutOfMemoryError: PermGen space`,永久代内存溢出
- 在 JDK1.8 中,如果常量池中发生了内存溢出,那么就会报错 `java.lang.OutOfMemoryError: Java heap space`,堆内存溢出
通过以上思路,来证明 JDK1.6 和 JDK1.8 StringTable 常量池的位置不同
案例代码
```java
public class Demo1_10 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
try {
for (int j = 0; j < 260000; j++) {
list.add(String.valueOf(j).intern());
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
```
**JDK1.8 验证**:需要设置堆内存的大小 `-Xmx10m`,会发现有以下结果,报错信息和预想的结果不一样?
- 原来,在 JVM规范中 当GC垃圾回收花了 98% 的时间只释放了 2% 的堆空间时JVM 就会认为该程序没救了,就直接抛出该异常
- 那么要跳过这个限制,需要再多配置一个参数 `-XX:-UseGCOverheadLimit`
```tex
java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at com.zhuhjay.Demo1_10.main(Demo1_10.java:16)
145367
```
最终结果就是预想的结果了!
```tex
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message can't create byte arrau at JPLISAgent.c line: 813
java.lang.OutOfMemoryError: Java heap space
at java.lang.Integer.toString(Integer.java:401)
at java.lang.String.valueOf(String.java:3099)
at com.zhuhjay.Demo1_10.main(Demo1_10.java:16)
146837
```
**JDK1.6 验证**:需要设置永久代内存的大小 `-XX:MaxPermSize=10m`
#### 5.8 StringTable 垃圾回收
查看字符串常量池 StringTable 的垃圾回收是否会在空间不足时进行字符串常量池的垃圾回收
示例代码,为了更进一步查看堆内存和字符串常量池的空间,需要添加以下参数
- `-Xmx10m`设置堆的大小为了方便测试GC垃圾回收
- `-XX:+PrintStringTableStatistics`:打印字符串表的信息
- `-XX:+PrintGCDetails -verbose:gc`:统计垃圾回收的信息
```java
public class Demo1_11 {
public static void main(String[] args) {
int i = 0;
try {
// TODO 待插入代码
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
```
查看输出结果,认识一下下面所代表的含义
```tex
Heap # 堆内存的占用情况
PSYoungGen total 2560K, used 1676K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 81% used [0x00000000ffd00000,0x00000000ffea3198,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
Metaspace used 3167K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 345K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics: # 符号表的详细信息(也是常量池的一部分,但是并不关心这部分内容)
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13260 = 318240 bytes, avg 24.000
Number of literals : 13260 = 568232 bytes, avg 42.853
Total footprint : = 1046560 bytes
Average bucket size : 0.663
Variance of bucket size : 0.666
Std. dev. of bucket size: 0.816
Maximum bucket size : 6
StringTable statistics: # StringTable 统计信息,使用 hashtable 结构
Number of buckets : 60013 = 480104 bytes, avg 8.000 # 桶个数 = 字节数
Number of entries : 1739 = 41736 bytes, avg 24.000 # 键值对个数 = 字节数
Number of literals : 1739 = 156592 bytes, avg 90.047 # 字符串常量个数 = 字节数
Total footprint : = 678432 bytes # 总的空间
Average bucket size : 0.029
Variance of bucket size : 0.029
Std. dev. of bucket size: 0.171
Maximum bucket size : 3
```
现在在以上代码中插入以下代码将10000个字符串添加到字符串常量池中此时字符串常量池中理应会在原来的 1739 基础上多 10000
```java
for (int j = 0; j < 10000; j++) {
String.valueOf(j).intern();
i++;
}
```
可以看到此时运行就有了 GC垃圾回收 的信息,此时字符串常量池没有达到预期的效果,说明当常量池中有不被使用的数据会被垃圾回收
```tex
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->704K(9728K), 0.0031150 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
10000
Heap
...省略展示
SymbolTable statistics:
...省略展示
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 4026 = 96624 bytes, avg 24.000
Number of literals : 4026 = 266888 bytes, avg 66.291
Total footprint : = 843616 bytes
Average bucket size : 0.067
Variance of bucket size : 0.065
Std. dev. of bucket size: 0.256
Maximum bucket size : 3
```
#### 5.9 StringTable 性能调优
- 字符串常量池的性能随着分配桶的个数越多,效率越高,参数配置 `-XX:StringTableSize=1009`,配置最小值为 1009
- String.intern 调优了一大部分性能,防止滋生很多的重复数据
## 6 直接内存
直接内存是系统内存
### 6.1 定义
Direct Memory
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
### 6.2 使用直接内存
#### 6.2.1 测试IO与Direct Memory读写效率
使用差不多 1G 的文件来进行读写对比
```java
public class Demo1_12 {
private static final String FROM = "G:\\maven\\maven_repository_0.zip";
private static final String TO = "G:\\IDM_DownLoad\\Video\\Temp\\maven.zip";
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
io();
directBuffer();
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1MB);
while (true) {
int length = from.read(bb);
if (length == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1MB];
while (true) {
int length = from.read(buf);
if (length == -1) {
break;
}
to.write(buf, 0, length);
}
} catch (Exception e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
}
```
对比结果如下单位ms可以发现使用直接内存读写效率更高
```tex
io 用时:19611.177
directBuffer 用时:5490.16
```
![1669128719697](/jvm/1669128719697.png)
- Java的 io 操作涉及到 CPU 的用户态和内核态
- io 操作需要操作系统从磁盘中进行磁盘文件的读取,将磁盘文件读取到系统内存中的系统缓存区
- 此时 Java 不能直接操作该缓存区,需要在 Java 堆内存中开辟一块 java缓冲区byte[]
- 然后将 系统缓冲区 的数据写到 Java缓冲区 中才能进一步操作
![1669128690245](/jvm/1669128690245.png)
- 调用 `ByteBuffer bb = ByteBuffer.allocateDirect(_1MB);` 时,会在 系统内存 和 Java堆内存 中开辟一块 `direct Memory` 内存来进行使用,这就是直接内存,使得 Java 可以直接操作
#### 6.2.2 直接内存溢出
每次分配 100M 内存,直到 Direct buffer memory 溢出
```java
public class Demo1_13 {
private static final int _100MB = 1024 * 1024 * 100;
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100MB);
list.add(byteBuffer);
i++;
}
} finally {
System.out.println(i);
}
}
}
```
可以看到直接内存分配到大约 1.7G 时就溢出了
```tex
17
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:695)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.zhuhjay.Demo1_13.main(Demo1_13.java:19)
```
#### 6.2.3 直接内存释放
查看以下案例,分配 1GB 的内存,然后使用 GC 进行回收
```java
public class Demo1_14 {
private static final int _1GB = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc();
System.in.read();
}
}
```
结果如图所示,这样一来感觉好像可以被 GC垃圾回收 进行 直接内存 的回收,但其实并不然。
![1669130149269](/jvm/1669130149269.png)
接下来 Unsafe 脱离 JVM 的内存管理来进行 直接内存 分配
```java
public class Demo1_15 {
private static final int _1GB = 1024 * 1024 * 1024;
public static void main(String[] args) throws Exception {
Unsafe unsafe = getUnsafe();
// 分配内存,并返回分配到的内存首地址
long base = unsafe.allocateMemory(_1GB);
unsafe.setMemory(base, _1GB, (byte) 0);
System.in.read();
// 手动释放内存
unsafe.freeMemory(base);
System.in.read();
}
private static Unsafe getUnsafe() throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
}
}
```
可以发现使用 直接内存 是通过 Unsafe 进行管理的GC垃圾回收 只能对 JVM内存有效
![1669130788224](/jvm/1669130788224.png)
查看 `ByteBuffer.allocateDirect()` 方法的源码
- 使用了 Unsafe 进行 直接内存的分配
- 使用了 `Cleaner` 特殊对象,虚引用 `PhantomReference<Object>` 对象
- 虚引用对象 `Cleaner` 所关联的实际对象 `ByteBuffer` 被 GC垃圾回收之后就会主动调用回调对象 `Deallocator` 主动的释放 直接内存
![1669131731576](/jvm/1669131731576.png)
### 6.3 分配和回收原理
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffer 的实现类内部,使用了 Cleaner虚引用来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调 用 freeMemory 来释放直接内存
禁用显示回收对直接内存的影响(就是禁用代码层面的 `System.gc()`,使其无效)
- `System.gc()` 触发的是 `Full GC`,比较影响性能的,不光回收新生代,还会回收老年代,会使得程序暂停时间较长
- 使用虚拟机参数 `-XX:+DisableExplicitGC`
- 代码层面的 GC 失效后,`ByteBuffer ` 得不到内存的释放,使得 直接内存 也无法被释放,需要等到下一次 GC 时才会触发
- **问题解决**:手动调用 Unsafe 释放直接内存
![1669132766803](/jvm/1669132766803.png)