什么是 JVM ?
定义
- Java Virtual Machine – java 程序的运行环境(java 二进制 字节码的运行环境)
好处
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态
- jvm jre jdk
常见的 JVM
整体结构
内存结构
程序计数器
定义
- Program Counter Register 程序计数器(寄存器)
- 作用
- 是记住下一条 j VM 指令的执行地址,也就是 线程 当前要执行的指令地址
- 特点
- 线程私有
- 不会存在内存溢出(唯一)
虚拟机栈
定义
- Java Virtual Machine Stacks ( Java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 栈的大小
- Linux /x64(64-bit):1024 KB
- maxOS(64-bit):1024 KB
- Oracle Solaris/x64(64-bit):1024 KB
- Windows:The default value depends on virtual memory
问题
- 垃圾回收是否涉及栈内存?
- 不涉及。每一次方法调用之后栈帧会被弹出,释放内存,不需要垃圾回收。
- 栈内存分配越大越好吗?
- 不。计算机总的物理内存有限,栈内存越大,栈的数量就越少,能够开启的线程就越少
- 方法内的局部变量是否线程安全?如果方法内局部变量没有逃离方法的作用访问,它是线程安全的如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈内存溢出
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
public static void main(String[] args) throws Exception { | |
try { | |
method(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} finally { | |
System.out.println(count); | |
} | |
} | |
public static void method() { | |
count++; | |
method(); | |
} | |
Exception in thread "main" java.lang.StackOverflowError |
本地方法栈
定义
- 管理本地方法,即非 Java 语言编写的方法( C语言 )的调用
- Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库
- 线程私有
- HotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一
// Object 类中有大量的本地方法 | |
public final native Class<?> getClass(); | |
public native int hashCode(); | |
protected native Object clone() throws CloneNotSupportedException; | |
public final native void notify(); | |
public final native void notifyAll(); | |
public final native void wait(long timeout) throws InterruptedException; |
堆
定义
- 通过 new 关键字,创建对象都会使用堆内存
- 线程共享的,堆中对象都需要考虑线程安全的问题
- 垃圾回收机制
堆内存溢出
- 创建的对象被虚拟机认为有用,不被回收,最后可能造成 OOM
- 注意不一定非得 new 对象的时候才会出现。
public static void main(String[] args) throws Exception { | |
String s = "a"; | |
ArrayList<String> array = new ArrayList<>(); | |
int count =; | |
try { | |
while (true) { | |
s += "a"; | |
array.add(s); | |
count++; | |
} | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} finally { | |
System.out.println(count); | |
} | |
} | |
Exception in thread "main" java.lang. OutOfMemory Error: Java heap space |
堆内存诊断
- jps 工具
- 查看当前系统中有哪些 java 进程
- jmap 工具
- 查看堆内存占用情况 jmap – heap 进程id
- jconsole 工具
- 图形界面的,多功能的监测工具,可以连续监测
- jvisualvm 工具
- 更强大的可视化工具
实例:
- 输出 1… 之后,线程休眠 30 秒
- 终端输入 jps ,查看进程 id,寻找到 Main 线程的 pid
- 终端输入 jmap -heap pid
- 程序创建一个 10 MB 大小的 byte 数组,
- 输出 2… 之后,线程休眠 30 秒
- 终端输入 jmap -heap pid
- 垃圾回收,释放数组内存
- 输出 3… 之后,线程休眠
- 终端输入 jmap -heap pid
public static void main(String[] args) throws Exception { | |
System.out.println("..."); | |
Thread.sleep(); | |
byte [] bytes = new byte[1024 * 1024 * 10]; | |
System.out.println("..."); | |
Thread.sleep(); | |
bytes = null; | |
System.gc(); | |
System.out.println("..."); | |
Thread.sleep(L); | |
} |
三次输入 jmap -heap pid 之后输出的部分内容如下
1️⃣ 第一次:程序刚开始
Eden Space: | |
capacity = (63.5MB) | |
used = (7.620185852050781MB) | |
free = (55.87981414794922MB) | |
.000292680394931% used |
2️⃣ 第二次:创建 10 MB byte 数组之后
Eden Space: | |
capacity = (63.5MB) | |
used = (17.620201110839844MB) | |
free = (45.879798889160156MB) | |
.748348206046998% used |
注意到 used 大小扩大了 10 MB
3️⃣ 第三次:垃圾回收之后
Eden Space: | |
capacity = (63.5MB) | |
used = (1.27001953125MB) | |
free = (62.22998046875MB) | |
.0000307578740157% used |
发现 used 减小明显。
还可以使用 jconsole 图形化工具
程序运行之后终端输入 jconsole 即可
使用 jvisualvm 获取更详细的堆内存描述:
jvisualvm // 终端输入
使用 堆 Dump 可以查看堆内具体信息。
方法区
定义
- 方法区(method area)只是 JVM 规范 中定义的一个 概念 ,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,不同的实现可以放在不同的地方。
- 逻辑上是堆的一部分,但不同厂商具体实现起来是不一样的,不强制位置
- hotspot 虚拟机使得在 jdk 1.8 之前方法区由 永久代 实现,在jdk1.8之后由 元空间 实现(本地内存)
- 线程共享
- 会导致内存溢出
方法区内存溢出
- jdk1.8 元空间内存溢出
因为虚拟机默认使用本机内存作为元空间,内存较大,所以要调小一下元空间的大小。
输入参数
-XX:MaxMetaspaceSize=m | |
public class Test extends ClassLoader { | |
public static void main(String[] args) { | |
int j =; | |
try { | |
Test test = new Test(); | |
for (int i =; i < 10000; i++, j++) { | |
// ClassWriter 作用是生成类的二进制字节码 | |
ClassWriter cw = new ClassWriter(); | |
// 版本号, public, 类名, 包名, 父类, 接口 | |
cw.visit(Opcodes.V_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); | |
// 返回 byte[] | |
byte[] code = cw.toByteArray(); | |
// 执行了类的加载 | |
test. define Class("Class" + i, code, 0, code.length); // Class 对象 | |
} | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} finally { | |
System.out.println(j); | |
} | |
} | |
} | |
Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space |
和预想的不太一样,Compressed class space 是什么呢?
-XX:+UseCompressedOops 允许对象指针压缩。
-XX:+UseCompressedClassPointers 允许类指针压缩。
它们默认都是开启的,可以手动关闭它们。
在VM options中输入
-XX:-UseCompressedOops | |
-XX:-UseCompressedClassPointers |
再次运行结果如下
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
表明元空间内存溢出。
- jdk1.6 永久代内存溢出
相同的代码和虚拟机参数配置,结果如下
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
表明永久代内存溢出
运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
反编译 字节码 命令(终端先 cd 进入 out 目录下相应字节码文件的目录)
javap -v Class.class
- 二进制字节码:由类基本信息、常量池、类方法定义、虚拟机指令组成
public class test { | |
public static void main(String[] args) { | |
System.out.println("hello world"); | |
} | |
} | |
:ProjectJavaProjectPracticeoutproductionPracticedemo>javap -v test02.class | |
Classfile /E:/Project/JavaProject/Practice/out/production/Practice/demo/test02.class | |
Last modified-11-18; size 535 bytes | |
MD checksum 6da0b7066cec4b7beb4be01700bf3897 | |
Compiled from "test.java" | |
public class demo.test02 | |
minor version: | |
major version: | |
flags: ACC_PUBLIC, ACC_SUPER | |
Constant pool: // 常量池 | |
# = Methodref #6.#20 // java/lang/Object."<init>":()V | |
# = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; | |
# = String #23 // hello world | |
# = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V | |
# = Class #26 // demo04/test02 | |
# = Class #27 // java/lang/Object | |
# = Utf8 <init> | |
# = Utf8 ()V | |
# = Utf8 Code | |
# = Utf8 LineNumberTable | |
# = Utf8 LocalVariableTable | |
# = Utf8 this | |
# = Utf8 Ldemo04/test02; | |
# = Utf8 main | |
# = Utf8 ([Ljava/lang/String;)V | |
# = Utf8 args | |
# = Utf8 [Ljava/lang/String; | |
# = Utf8 SourceFile | |
# = Utf8 test02.java | |
# = NameAndType #7:#8 // "<init>":()V | |
# = Class #28 // java/lang/System | |
# = NameAndType #29:#30 // out:Ljava/io/PrintStream; | |
# = Utf8 hello world | |
# = Class #31 // java/io/PrintStream | |
# = NameAndType #32:#33 // println:(Ljava/lang/String;)V | |
# = Utf8 demo04/test02 | |
# = Utf8 java/lang/Object | |
# = Utf8 java/lang/System | |
# = Utf8 out | |
# = Utf8 Ljava/io/PrintStream; | |
# = Utf8 java/io/PrintStream | |
# = Utf8 println | |
# = Utf8 (Ljava/lang/String;)V | |
{ | |
public demo.test02(); // 构造方法 | |
descriptor: ()V | |
flags: ACC_PUBLIC | |
Code: | |
stack=, locals=1, args_size=1 | |
: aload_0 | |
: invokespecial #1 // Method java/lang/Object."<init>":()V | |
: return | |
LineNumberTable: | |
line: 0 | |
LocalVariableTable: | |
Start Length Slot Name Signature 5 0 this Ldemo04/test02; | |
public static void main(java.lang.String[]); | |
descriptor: ([Ljava/lang/String;)V | |
flags: ACC_PUBLIC, ACC_STATIC | |
Code: | |
stack=, locals=1, args_size=1 | |
: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; | |
: ldc #3 // String hello world | |
: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V | |
: return | |
LineNumberTable: | |
line: 0 | |
line: 8 | |
LocalVariableTable: | |
Start Length Slot Name Signature 9 0 args [Ljava/lang/String; | |
} | |
SourceFile: "test.java" |
- 常量池可以给虚拟机指令提供一些常量符号,可以通过查表的方式查到。
StringTable
StringTable 的数据结构
- hash表(数组+ 链表 )
- 不可扩容
- 存 字符串 常量,唯一不重复
- 每个数组单元称为一个哈希桶
- 大小至少是 1009
面试题
String s = "a"; | |
String s = "b"; | |
String s = "a" + "b"; | |
String s = s1 + s2; | |
String s = "ab"; | |
String s6 = s4.intern(); | |
// 问 | |
System.out.println(s == s4); | |
System.out.println(s == s5); | |
System.out.println(s == s6); | |
String x = new String("c") + new String("d"); | |
String x = "cd"; | |
x.intern(); | |
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk.6呢 | |
// x.intern(); | |
// String x = "cd"; | |
System.out.println(x == x2); | |
false | |
true | |
true | |
false | |
// 调换后,true |
解析
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量 拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- jdk1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- jdk1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
字符串常量
String s = "a"; | |
String s = "b"; | |
String s = "ab"; |
反编译后的执行过程:
Constant pool: | |
# = Methodref #6.#24 // java/lang/Object."<init>":()V | |
# = String #25 // a | |
# = String #26 // b | |
# = String #27 // ab | |
... | |
Code: | |
stack=, locals=4, args_size=1 | |
: ldc #2 // String a | |
: astore_1 | |
: ldc #3 // String b | |
: astore_2 | |
: ldc #4 // String ab | |
: astore_3 | |
: return | |
... | |
常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象 | |
ldc # 会把 a 符号变为 "a" 字符串对象 | |
ldc # 会把 b 符号变为 "b" 字符串对象 | |
ldc # 会把 ab 符号变为 "ab" 字符串对象 |
字符串延迟加载
字符串变量拼接
String s = "a"; // 懒惰的 | |
String s = "b"; | |
String s = "ab"; | |
String s = s1 + s2; |
反编译结果
Code: | |
stack=, locals=5, args_size=1 | |
: ldc #2 // String a | |
: astore_1 | |
: ldc #3 // String b | |
: astore_2 | |
: ldc #4 // String ab | |
: astore_3 | |
: new #5 // class java/lang/StringBuilder | |
: dup | |
: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V | |
: aload_1 | |
: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; | |
: aload_2 | |
: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; | |
: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; | |
: astore 4 | |
: return |
字符串拼接的过程 new StringBilder().append(“a”).append(“b”).toString(),而StringBuilder的toString()方法又在底层创建了一个String对象
public String toString() { | |
// Create a copy, don't share the array | |
return new String(value,, count); | |
} |
所以 s3 == s4 为 false
字符串常量拼接
String s = "a"; // 懒惰的 | |
String s = "b"; | |
String s = "ab"; | |
String s = s1 + s2; | |
String s = "a" + "b"; |
反编译结果
Code: | |
stack=, locals=6, args_size=1 | |
: ldc #2 // String a | |
: astore_1 | |
: ldc #3 // String b | |
: astore_2 | |
: ldc #4 // String ab | |
: astore_3 | |
: new #5 // class java/lang/StringBuilder | |
: dup | |
: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V | |
: aload_1 | |
: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; | |
: aload_2 | |
: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; | |
: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; | |
: astore 4 | |
: ldc #4 // String ab | |
: astore 5 | |
: return |
注意 29: ldc #4 // String ab 和 6: ldc #4 // String ab
指向的是字符串常量池中相同的字符串常量 #4,说明 javac 在编译期间进行了优化,结果已经在编译期确定为 ab
所以 s3 == s5 为 true
intern 方法
String s = new String("a") + new String("b");
反编译结果
Constant pool: | |
... | |
# = String #30 // a | |
... | |
# = String #33 // b | |
... | |
Code: | |
stack=, locals=2, args_size=1 | |
: new #2 // class java/lang/StringBuilder | |
: dup | |
: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V | |
: new #4 // class java/lang/String | |
: dup | |
: ldc #5 // String a | |
: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V | |
: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; | |
: new #4 // class java/lang/String | |
: dup | |
: ldc #8 // String b | |
: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V | |
: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; | |
: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; | |
: astore_1 | |
: return | |
... |
可以发现,创建了三个对象,”a”,”b” 以及StringBuilder.toString()创建的 “ab”。
字符串常量 “a”,”b” 进入串池,”ab” 是动态拼接出的一个字符串,没有被放入串池。
s 是一个变量指向堆中的 “ab” 字符串对象
调用 String.intern() 方法可以将这个字符串对象尝试放入串池,如果有则并不会放入,把串池中的对象返回;如果没有则放入串池, 再把串池中的对象返回。
注意这里说的返回是指调用 String.intern() 方法后返回的值。比如 String ss = s.intern() , ss 接收返回的对象,与 s 无关。而 s 只与对象本身有关,与返回值无关。
String x = "ab"; | |
String s = new String("a") + new String("b"); | |
String s = s.intern(); | |
System.out.println(s == x); | |
System.out.println(s == x); |
过程:
- 字符串常量 “ab” 放入串池
- “a” 和 “b” 放入串池
- s 指向堆中创建的 “ab” 对象
- 串池中已经有 “ab” 对象,则返回串池中的对象引用给变量 s2 , s 依然指向堆中的 “ab” 对象
- s2 == x 为 true
- s == x 为 false
如果调换一下位置
String s = new String("a") + new String("b"); | |
String s = s.intern(); | |
String x = "ab"; | |
System.out.println( s == x); | |
System.out.println( s == x ); |
过程:
- “a” 和 “b” 放入串池
- s 指向堆中创建的 “ab” 对象
- 串池中没有 “ab” 对象,则返回串池中的对象引用给变量 s2 , s 指向串池中的 “ab” 对象
- s2 == x 为 true
- s == x 为 true
StringTable 的位置
- jdk1.6 StringTable 放在永久代中,与常量池放在一起
- jdk1.8 StringTable 放在堆中
StringTable 垃圾回收
- StringTable 会发生垃圾回收
-Xmxm -XX:+PrintStringTableStatistics | |
-XX:+PrintGCDetails -verbose:gc | |
public static void main(String[] args) throws InterruptedException { | |
int i =; | |
try { | |
for (int j =; j < 100000; j++) { // j=100, j=10000 | |
String.valueOf(j).intern(); | |
i++; | |
} | |
} catch (Throwable e) { | |
e.printStackTrace(); | |
} finally { | |
System.out.println(i); | |
} | |
} | |
[2048K->676K(9728K), 0.0010489 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] | ]|
... | |
StringTable statistics: | |
Number of buckets : = 480104 bytes, avg 8.000 | |
Number of entries : = 105312 bytes, avg 24.000 | |
Number of literals : = 284264 bytes, avg 64.782 | |
Total footprint : = bytes |
可以看到 entries 的个数小于 10000,从第一行也可以看出发生了 GC。
StringTable 调优
调整 StringTable 的大小
-XX:StringTableSize=桶个数
- 哈希桶越多,分布越分散,发生哈希冲突的可能性越低,效率越高
- 字符串常量多的话,可以调大 StringTable 的大小,能增加哈希桶的个数,提供效率
考虑字符串是否入池
- 使用 String.intern() 方法使重复字符串常量入池,减少堆的内存占用
public static void main(String[] args) throws IOException { | |
List<String> address = new ArrayList<>(); | |
System.in.read(); | |
for (int i =; i < 10; i++) { | |
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-"))) { | |
String line = null; | |
long start = System.nanoTime(); | |
while (true) { | |
line = reader.readLine(); | |
if(line == null) { | |
break; | |
} | |
address.add(line.intern()); // 字符串常量放入串池 | |
} | |
System.out.println("cost:" +(System.nanoTime()-start)/); | |
} | |
} | |
System.in.read(); | |
} |
直接内存
定义
- Direct Memory
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
Java 本身不具有磁盘读写能力,需要调用操作系统提供的函数。
当 CPU 从用户态切换为内核态时,操作系统中会划分出一个系统缓冲区,Java 无法直接访问系统缓冲区,而堆中存在 Java 缓冲区,数据进入系统缓冲区再进入 Java 缓冲区就可以被 Java 访问。
两个缓冲区直接存在不必要的数据复制。
直接内存可以使系统缓冲区和 Java 缓冲区共享,使 Java 可以直接访问系统缓冲区的数据,减少了不必要的数据复制,适合文件的 IO 操作。
public class Demo_9 { | |
static final String FROM = "E:编程资料第三方教学视频youtubeGetting Started with Spring Boot-sbPSjItt10.mp4"; | |
static final String TO = "E:a.mp"; | |
static final int _Mb = 1024 * 1024; | |
public static void main(String[] args) { | |
io(); // io 用时:.586957 1766.963399 1359.240226 | |
directBuffer(); // directBuffer 用时:.295165 702.291454 562.56592 | |
} | |
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(_Mb); | |
while (true) { | |
int len = from.read(bb); | |
if (len == -) { | |
break; | |
} | |
bb.flip(); | |
to.write(bb); | |
bb.clear(); | |
} | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
long end = System.nanoTime(); | |
System.out.println("directBuffer 用时:" + (end - start) /_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[_Mb]; | |
while (true) { | |
int len = from.read(buf); | |
if (len == -) { | |
break; | |
} | |
to.write(buf,, len); | |
} | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
long end = System.nanoTime(); | |
System.out.println("io 用时:" + (end - start) /_000.0); | |
} | |
} |
分配和回收原理
- ByteBuffer 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存
ByteBuffer 的 allocateDirect 方法
public static ByteBuffer allocateDirect(int capacity) { | |
return new DirectByteBuffer(capacity); | |
} |
DirectByteBuffer 对象
// Primary constructor | |
// | |
DirectByteBuffer(int cap) { // package-private | |
super(-, 0, cap, cap); | |
boolean pa = VM.isDirectMemoryPageAligned(); | |
int ps = Bits.pageSize(); | |
long size = Math.max(L, (long)cap + (pa ? ps : 0)); | |
Bits.reserveMemory(size, cap); | |
long base =; | |
try { | |
base = unsafe.allocateMemory(size); // 调用了 unsafe 类的 allocateMemory 方法 | |
} catch (OutOfMemoryError x) { | |
Bits.unreserveMemory(size, cap); | |
throw x; | |
} | |
unsafe.setMemory(base, size, (byte)); | |
if (pa && (base % ps !=)) { | |
// Round up to page boundary | |
address = base + ps - (base & (ps -)); | |
} else { | |
address = base; | |
} | |
cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); // Cleaner 虚引用监控 DirectByteBuffer 对象 | |
att = null; | |
} |
Cleanr 对象的 clean 方法
public void clean() { | |
if (remove(this)) { | |
try { | |
this.thunk.run(); // 执行任务对象 | |
} catch (final Throwable var) { | |
AccessController.doPrivileged(new PrivilegedAction<Void>() { | |
public Void run() { | |
if (System.err != null) { | |
(new Error("Cleaner terminated abnormally", var)).printStackTrace(); | |
} | |
System.exit(); | |
return null; | |
} | |
}); | |
} | |
} | |
} |
Deallocator 任务对象
private static class Deallocator | |
implements Runnable | |
{ | |
private static Unsafe unsafe = Unsafe.getUnsafe(); | |
private long address; | |
private long size; | |
private int capacity; | |
private Deallocator(long address, long size, int capacity) { | |
assert (address !=); | |
this.address = address; | |
this.size = size; | |
this.capacity = capacity; | |
} | |
public void run() { | |
if (address ==) { | |
// Paranoia | |
return; | |
} | |
unsafe.freeMemory(address); | |
address =; | |
Bits.unreserveMemory(size, capacity); | |
} | |
} |
DirectByteBuffer 这个 Java 对象被垃圾回收器调用的时候,会触发虚引用对象 Cleaner 中的 clean 方法,执行任务对象 Deallocator,调用任务对象中的 freeMemory 去释放直接内存。
禁用显式垃圾回收
禁用显式垃圾回收
-XX:+DisableExplicitGC // 禁用显式的 System.gc()
System.gc() 触发的是 Full GC,回收新生代和老年代,程序暂停时间长,JVM 调优的时候可能会禁用掉,防止无意使用 System.gc() 。
但是禁用显式的 System.gc() ,直接内存不能被即时释放,可以通过直接调用 Unsafe 的 freeMemory 方法手动管理回收直接内存。
static int _Gb = 1024 * 1024 * 1024; | |
public static void main(String[] args) throws IOException { | |
Unsafe unsafe = getUnsafe(); | |
// 分配内存 | |
long base = unsafe.allocateMemory(_Gb); | |
unsafe.setMemory(base, _Gb, (byte) 0); | |
System.in.read(); | |
// 释放内存 | |
unsafe.freeMemory(base); | |
System.in.read(); | |
} | |
public static Unsafe getUnsafe() { | |
try { | |
Field f = Unsafe.class.getDeclaredField("theUnsafe"); | |
f.setAccessible(true); | |
Unsafe unsafe = (Unsafe) f.get(null); | |
return unsafe; | |
} catch (NoSuchFieldException | IllegalAccessException e) { | |
throw new RuntimeException(e); | |
} | |
} |