- 新增 .drone.yml 文件用于定义 CI/CD 流程 - 配置了基于 Docker 的部署步骤 - 设置了工作区和卷映射以支持持久化数据 - 添加了构建准备阶段和 Docker 部署阶段 - 定义了环境变量和代理设置 - 配置了 artifacts 目录的处理逻辑 - 添加了 timezone 映射以确保时间同步 - 设置了 docker.sock 映射以支持 Docker in Docker
1206 lines
42 KiB
Markdown
1206 lines
42 KiB
Markdown
---
|
||
title: JVM之内存结构
|
||
date: 2022-11-22
|
||
sidebar: 'auto'
|
||
tags:
|
||
- JVM
|
||
categories:
|
||
- Java
|
||
---
|
||
## 1 程序计数器
|
||
|
||

|
||
|
||
|
||
|
||
Program Counter Register 程序计数器(寄存器)
|
||
|
||
- 作用:是记住下一条jvm指令的执行地址
|
||
- 特点
|
||
- 是线程私有的
|
||
- 不会存在内存溢出
|
||
|
||
|
||
|
||
## 2 虚拟机栈
|
||
|
||

|
||
|
||
|
||
|
||
### 2.1 定义
|
||
|
||
Java Virtual Machine Stacks (Java 虚拟机栈)
|
||
|
||
- 每个线程运行时所需要的内存,称为虚拟机栈
|
||
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
|
||
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
|
||
|
||
|
||
|
||
问题辨析
|
||
|
||
1. 垃圾回收是否涉及栈内存?
|
||
- 每一个栈帧使用完后会自动释放该栈帧所对应的内存,垃圾回收只涉及到堆内存
|
||
2. 栈内存分配越大越好吗?
|
||
- 每个线程都会对应一个栈内存,如果栈内存分配越大则所能使用的线程数则减小,也对程序的执行起不到加快的作用
|
||
3. 方法内的局部变量是否线程安全?
|
||
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
|
||
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
|
||
|
||
|
||
|
||
### 2.2 栈内存溢出
|
||
|
||
- 栈帧过多导致栈内存溢出
|
||
- 递归没有给到合适的出口
|
||
- 第三方组件使用不当导致循环引用(ObjectMapper:对象 -> Json)
|
||
|
||
- 栈帧过大导致栈内存溢出(局部变量过多导致,但较难触发)
|
||
|
||
|
||
|
||
> Tip:
|
||
>
|
||
> - 可以使用 `-Xss` 来改变栈内存的大小
|
||
>
|
||
> - 栈内存溢出的错误信息为:`java.lang.StackOverflowError`
|
||
|
||
|
||
|
||
### 2.3 线程运行诊断
|
||
|
||
eg1:CPU占用过多
|
||
|
||
定位问题步骤(Linux)
|
||
|
||
- 用 `top` 命令定位哪个进程对CPU的占用过高
|
||
- `ps H -eo pid,tid,%cpu | grep 进程id` (用`ps`命令进一步定义是哪个线程引起的cpu占用过高)
|
||
- `jstack 进程id` (可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号)
|
||
|
||
eg2:程序运行很长时间没有结果(线程死锁)
|
||
|
||
|
||
|
||
## 3 本地方法栈
|
||
|
||

|
||
|
||
|
||
|
||
本地方法栈的作用类似于JVM虚拟机栈,就是调用本地方法Native时会开辟一个本地方法的执行内存,也就是本地方法栈,其中有调用库函数的执行栈帧。
|
||
|
||
|
||
|
||
## 4 堆
|
||
|
||

|
||
|
||
|
||
|
||
### 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 工具:图形界面,多功能的监测工具,可以连续监测
|
||
|
||
分为三个阶段的使用情况,阶段一为初始阶段。阶段二时分配了一段内存,阶段三进行了垃圾回收
|
||
|
||

|
||
|
||
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 工具已经没办法进一步查询到具体问题了
|
||
|
||

|
||
|
||
- 使用 jvisualvm 进行堆内存 dump
|
||
|
||
使用命令 `jvisualvm` 打开可视化界面,选择当前执行的进程,查看详细信息,然后进行堆内存 dump
|
||
|
||

|
||
|
||
可以发现当前的 ArrayList 对象占用的内存高达约 200M 左右的内存,点击进去查看该对象的详细信息
|
||
|
||

|
||
|
||
在 ArrayList 对象中的每一个元素都占用了约 1M 的内存大小,一共有200个元素,则总占用就有了大约 200M 的堆内存空间,由此快速定位到代码中存在的问题
|
||
|
||

|
||
|
||
|
||
|
||
## 5 方法区
|
||
|
||

|
||
|
||
|
||
|
||
### 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 的内存结构发生了变化
|
||
|
||
> **注意**:图中的 常量池 指的是 运行时常量池
|
||
|
||

|
||
|
||
|
||
|
||
### 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 特性总结
|
||
|
||
- 常量池中的字符串仅是符号,第一次用到时才变为对象
|
||
|
||
- 利用串池的机制,来避免重复创建字符串对象
|
||
- 字符串变量拼接的原理是 StringBuilder(1.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
|
||
```
|
||
|
||
|
||
|
||

|
||
|
||
- Java的 io 操作涉及到 CPU 的用户态和内核态
|
||
- io 操作需要操作系统从磁盘中进行磁盘文件的读取,将磁盘文件读取到系统内存中的系统缓存区
|
||
- 此时 Java 不能直接操作该缓存区,需要在 Java 堆内存中开辟一块 java缓冲区byte[]
|
||
- 然后将 系统缓冲区 的数据写到 Java缓冲区 中才能进一步操作
|
||
|
||
|
||
|
||

|
||
|
||
- 调用 `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垃圾回收 进行 直接内存 的回收,但其实并不然。
|
||
|
||

|
||
|
||
|
||
|
||
接下来 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内存有效
|
||
|
||

|
||
|
||
|
||
|
||
查看 `ByteBuffer.allocateDirect()` 方法的源码
|
||
|
||
- 使用了 Unsafe 进行 直接内存的分配
|
||
- 使用了 `Cleaner` 特殊对象,虚引用 `PhantomReference<Object>` 对象
|
||
- 虚引用对象 `Cleaner` 所关联的实际对象 `ByteBuffer` 被 GC垃圾回收之后,就会主动调用回调对象 `Deallocator` 主动的释放 直接内存
|
||
|
||

|
||
|
||
|
||
|
||
### 6.3 分配和回收原理
|
||
|
||
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
|
||
- ByteBuffer 的实现类内部,使用了 Cleaner(虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调 用 freeMemory 来释放直接内存
|
||
|
||
|
||
|
||
禁用显示回收对直接内存的影响(就是禁用代码层面的 `System.gc()`,使其无效)
|
||
|
||
- `System.gc()` 触发的是 `Full GC`,比较影响性能的,不光回收新生代,还会回收老年代,会使得程序暂停时间较长
|
||
- 使用虚拟机参数 `-XX:+DisableExplicitGC`
|
||
- 代码层面的 GC 失效后,`ByteBuffer ` 得不到内存的释放,使得 直接内存 也无法被释放,需要等到下一次 GC 时才会触发
|
||
- **问题解决**:手动调用 Unsafe 释放直接内存
|
||
|
||

|
||
|