程序计数器
定义
Program Counter Register 程序计数器(寄存器)
- 作用:记住下一条指令的执行地址
- 特点
- 是线程私有的
- 不会存在内存溢出
计算机在运行程序时,对于多线程来说,会给每一个内存分配一个时间片。当时间片结束,切换线程时,每个线程都会有其私有的程序计数器来保存它当前的状态。当该线程再次抢到时间片时,便可以通过程序计数器接着继续运行。
作用
程序计数器作用:记住下一条jvm指令的执行地址
- 在JAVA程序运行时,java源代码会被编译成二进制字节码,作为操纵jvm的jvm指令,然后交给解释器,解释器将其解释为机器码,交给cpu去运行。与此同时,程序计数器会记住下一条jvm指令的执行地址,然后交给解释器继续运行。
- 这些jvm指令是通用的,用来直接操纵虚拟机的。所以无论是在window、linux还是macos,只要有虚拟机,java代码都能在其上面运行。这也是java程序跨平台的原因。
- 程序计数器是jvm中的一个角色。程序计数器是通过计算机中的寄存器来实现的,寄存器访问速度快,存取速度快。
虚拟机栈
栈:线程运行需要的内存空间
栈帧:每个方法运行时需要的内存
一个栈由多个栈帧组成,每个栈帧都为所调用的方法分配了内存空间,比如方法的参数,局部变量,返回地址等。
在线程运行时,不同的方法在执行过程中,被依次压入栈中。待执行结束后,弹出栈,释放内存。
定义
Java Virtual Machine Stacks(Java虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应则当前正在执行的那个方法。
问题辨析
-
垃圾回收是否涉及到栈内存
不涉及。栈内存是方法调用是分配的,在方法结束调用后,就将栈帧弹出栈了,释放了内存。
-
栈内存分配越大越好吗
并不是。系统的物理内存是一定的,栈空间越大,会导致线程数越少。栈空间越大,也并不会让程序更快,只是有更大的栈空间,能让你做更多次的递归调用。
-
方法内的局部变量是否线程安全
- 判断是否安全,即看这些变量对于多个线程是共享的还是私有的。
- 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用方法,需要考虑线程安全
案例分析1
对于局部变量int x 来说,它是方法内的局部变量,在多线程下,每个线程会为其运行的方法分配栈和栈帧,栈是线程私有的,方法中的局部变量不会受其他线程影响。
每个线程读取static变量到线程的工作内存中,然后再进行计算,最后将修改以后的变量的值同步到static变量中
案例分析2
- m1方法线程安全:局部变量在方法内,线程私有。
- m2方法线程不安全:变量是通过应用类型拿到的,那么与此同时其他线程也可能拿到这个应用然后对其进行修改。
- m3方法线程不安全:局部变量虽然声明在方法内,但是在最后确返回出去了。返回出去后可能被其他线程引用进行修改。
栈内存溢出
栈空间调整参数
-Xss空间大小
-Xss8M
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
当程序递归调用次数太多时,会超出栈的空间,导致栈内存溢出。
方法携带的参数等占用内存太多,导致栈帧过大,使栈内存溢出。
演示1(栈帧过多)
递归调用次数太多。
演示2(互相调用,类似死锁)
**
* json 数据转换
*/
public class Demo1_19 {
public static void main(String[] args) throws JsonProcessingException {
Dept d = new Dept();
d.setName("Market");
Emp e1 = new Emp();
e1.setName("zhang");
e1.setDept(d);
Emp e2 = new Emp();
e2.setName("li");
e2.setDept(d);
d.setEmps(Arrays.asList(e1, e2));
// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(d));
}
}
class Emp {
private String name;
@JsonIgnore
private Dept dept;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
}
class Dept {
private String name;
private List<Emp> emps;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Emp> getEmps() {
return emps;
}
public void setEmps(List<Emp> emps) {
this.emps = emps;
}
}
在工具类将类转换成json格式时,emp对象里有dep,dep里又有emp字段。
dep: { name: ‘Market’, emps: [{ name:‘zhang’, dept:{ name:’’, emps: [ {}]} },] }
解决方法:使用JsonIgnore
线程运行诊断
案例1:cpu占用过多
定位
- 用top定位哪个进程对cpu的占用过高
- top
- 用ps命令进一步定位是哪个线程引起的cpu占用过高
- ps H -eo pid,tid,%cpu | grep 32655
- jstack根据线程id找到有问题的线程,进一步定位到问题代码的源码行数。
- jstack 进程id
案例2:程序运行很长时间没有结果
演示1(cpu占用过多)
# 使用top命令查看当前cup运行情况
top
# 使用ps查看线程的运行情况
# -eo 后的参数是想要查看的参数信息,pid进程号,tid线程号,%cpu cpu占用率
ps H -eo pid,tid,%cpu | grep 32655
32665线程有大问题。
# 输出进程内的所有信息,线程号用16进制表示的
# 32665线程换算成6进制为7f99
jstack 32655
演示2(死锁)
jstack 32275
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EyAxFIKV-1631327192562)(https://chasing1874.oss-cn-chengdu.aliyuncs.com/image-20210828181101161.png)]
package cn.itcast.jvm.t1.stack;
/**
* 演示线程死锁
*/
class A{};
class B{};
public class Demo1_3 {
static A a = new A();
static B b = new B();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (a) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
Thread.sleep(1000);
new Thread(()->{
synchronized (b) {
synchronized (a) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
}
}
本地方法栈
native 本地方法
本地方法栈,给本地方法的运行提供一个空间。jvm类并不是所有的方法都是java代码编写的,有些底层的方法就是通过c/c++实现的。而java可以调用这些底层方法来完成一些功能。在java调用这些底层方法时,就是运行在本地方法栈中。
堆
定义
Heap堆
- 通过new关键字,创建对象都会使用堆内存
特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
堆内存溢出
OutOfMemoryError:java heap space 堆内存溢出
堆空间调整参数
-Xmx空间大小
-Xmx4G
案例
案例代码
package cn.itcast.jvm.t1.heap;
import java.util.ArrayList;
import java.util.List;
/**
* 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
* -Xmx8m
*/
public class Demo1_5 {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a); // hello, hellohello, hellohellohellohello ...
a = a + a; // hellohellohellohello
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}
在实际生产中,对于堆内存溢出问题,可能宾士那么容易检测出来。因为堆内存空间比较大,在运行时,一时间还不会使其溢出。
所以为了使堆内存问题尽早暴露出来,可以在测试时,将堆内存空间调整小一些。
堆内存诊断
- jps工具
- 查看当前系统中有哪些java进程
- jmap工具
- 查看某一时刻堆内存占用情况
- jhsdb jmap --heap --pid 进程id
- jconsole工具
- 图形界面的,多功能的监测工具,可以连续监测
- 堆内存调整指令参数
- -Xmx容量大小
jmp诊断堆内存
案例代码
package cn.itcast.jvm.t1.heap;
/**
* 演示堆内存
*/
public class Demo1_4 {
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
System.out.println("2...");
Thread.sleep(20000);
array = null;
System.gc();
System.out.println("3...");
Thread.sleep(1000000L);
}
}
- Thread.sleep 是为了留有时间间隔执行命令,监控进程状态
- 程序打印 1… 后,执行jps查看该进程的进程号
- jmap -heap 进程id,查看这一时刻进程堆空间使用情况
- 程序打印 2… 后,再次执行 jmap 指令查看内存情况
- 程序打印 3… 后,再次执行 jmap 指令查看内存情况
程序运行后
97751为该进程的pid,调用命令
jhsdb jmap --heap --pid 97751
具体的堆内存占用在Heap Usage
在程序打印了 2… 后,再次
jhsdb jmap --heap --pid 97751
按理说应该增加10M,此处有些疑惑
在打印了 3… 之后,代表着已经被垃圾回收了
jhsdb jmap --heap --pid 97751
jconsole诊断堆内存
但是在jconsole里面可以看出,在给array初始化后,堆内存使用量增加了10M,在垃圾回收后,堆内存使用量又迅速下降。
jvisualvm诊断堆内存
jvisualvm是功能更加强大的图形化jvm管理软件。可以进行堆转储,拿到进程某一时刻的快照dump进行分析。
案例代码:
package cn.itcast.jvm.t1.heap;
import java.util.ArrayList;
import java.util.List;
/**
* 演示查看对象个数 堆转储 dump
*/
public class Demo1_13 {
public static void main(String[] args) throws InterruptedException {
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
// Student student = new Student();
}
Thread.sleep(1000000000L);
}
}
class Student {
private byte[] big = new byte[1024*1024];
}
经过测试,在执行了垃圾回收后,堆内存占用还是居高不下。
于是点击 堆dump 拿取快照,分析详情
点击查看
由源代码可知,确实是Student类的原因。
student数组一直在循环引用,没有被垃圾回收。
方法区
定义
方法区是一种规范,永久代和元空间都只是它的实现。
组成
方法区内存溢出
元空间大小调整参数
-XX:MaxMetaspaceSize=8m
案例代码
package cn.itcast.jvm.t1.metaspace;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -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); // Class 对象
}
} finally {
System.out.println(j);
}
}
}
由于元空间使用本地内存,所以很难方法区溢出所以需要手动调整元空间大小。
-XX:MaxMetaspaceSize=8m
运行结果 OutOfMemoryError: Metaspace
场景
- spring
- mybatis
都用到了cglib技术,字节码的动态生成技术,动态加载类,动态生成类,运行期间,经常会产生大量的类,可能会产生方法区溢出。
运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
案例
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
通过反编译这段程序来学习相关的知识
用到的命令
- 先进入到java源文件的目录
cd 目标目录
- 将HelloWorld.java 编译成 HelloWorld.class
javac HelloWorld.java
- 反编译HelloWorld.class
javap -v HelloWorld.class
结果如下
类基本信息
常量池
类方法定义
方法运行流程
-
对于主方法来说,解释器依次执行指令。getstatic–>得到某个常量,索引为#2。
-
以#2作为索引去常量池查询,得到 Fieldref ,即属性索引,索引为 #16,#17
-
再以#16,#17为索引继续查询常量池
-
依照这个步骤下去继续阅读即可
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";
}
运行时常量池
方法区
局部变量表
-
常量池最初存在于字节码中,运行时会被加载到运行时常量池中。这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
-
字符串的加载是懒惰的,当执行到具体的引用时,才会创建对象。如String s1 = “a”。创建了s1对象。
-
同时会创建StringTable串池。将s1作为key去串池中寻找,如果没有,才会加入串池
变量字符串拼接
// 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";
// new StringBuilder().append("a").append("b").toString()
// new String("ab")
String s4 = s1 + s2;
// 输出false,s3在常量池,s4在堆里
System.out.println(s3 == s4);
}
}
可以看到StringBuilder.toString是new了一个String,new的对象在堆里。
编译期优化(常量字符串拼接)
// 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 在编译期间的优化,结果已经在编译期确定为ab
// true
System.out.println(s3 == s5);
}
}
字符串延迟加载
/**
* 演示字符串字面量也是【延迟】成为对象的
*/
public class TestString {
public static void main(String[] args) {
int x = args.length;
System.out.println(); // 字符串个数 4695
System.out.print("1");
System.out.print("2");
System.out.print("3");
System.out.print("4");
System.out.print("5");
System.out.print("6");
System.out.print("7");
System.out.print("8");
System.out.print("9");
System.out.print("0");
System.out.print("1"); // 字符串个数 4703
System.out.print("2");
System.out.print("3");
System.out.print("4");
System.out.print("5");
System.out.print("6");
System.out.print("7");
System.out.print("8");
System.out.print("9");
System.out.print("0");
System.out.print(x); // 字符串个数2285
}
}
StringTable特性
- 常量池中的字符串仅是符号,第一次用到时才变成对象。
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder(1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
- 1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,两种情况均会把串池中的对象返回。
- 1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,两种情况均会把串池中的对象返回。即调用intern的对象,和将来放入串池的对象,是两个对象。
public class Demo1_23 {
// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == x);//true
System.out.println( s == x );//false
}
}
- 执行到第10行String s2 = s.intern(); 时,将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回。由于String x = “ab”;StringTable中有"ab"了,所以s不会放在常量池。但是会吧常量池的"ab"返回回来给s2。
- 所以 s2 == x 即 常量池的"ab"
- s != x,因为s没有放进去
public class Demo1_23 {
// ["a", "b","ab" ]
public static void main(String[] args) {
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == "ab"); //true
System.out.println( s == "ab" ); //true
}
}
- 执行到第10行String s2 = s.intern(); 时,将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回。此时StringTable还没有"ab",所以s会放在常量池。同时常量池的"ab"返回回来给s2。
- 所以 s2 == x 即 常量池的"ab"
- s == x,因为s放进去了
StringTable面试题
public class Demo1_21 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a"+"b"; // 优化为"ab"
String s4 = s1 + s2; // new StringBuilder 在堆中
String s5 = "ab"; // 去串池查找添加
String s6 = s4.intern();
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d"); // new String("cd")
String x1 = "cd"; //"cd"
x2.intern(); // 串池已经有cd,x2放不进去
System.out.println(x1 == x2); // false
}
}
如果调换位置
String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern(); // 串池已经有cd,x2放不进去
String x1 = "cd"; //"cd"
System.out.println(x1 == x2); // true
StringTable位置
在1.6之前。StringTable在方法区永久代中,但是放在这之中,虚拟机只会在full GC时才会对StringTable进行垃圾回收。
但是其实StringTable的操作是非常频繁的,如果没有即使进行垃圾回收,容易造成永久代空间不足。
在1.8后,StringTable放在了堆中,使其垃圾回收的效率更高。
案例
1.6下
虚拟机参数
-XX:MaxPermSize=10m //永久代空间设置
1.8下
虚拟机参数
// 堆空间大小设置、关闭UseGCOverheadLimit
-Xmx10m -XX:-UseGCOverheadLimit
public class Demo1_22 {
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);
}
}
}
可以看到1.6是永久代空间溢出,1.8是堆空间溢出。
StringTable垃圾回收
StringTable中的字符串常量不再引用后,也会被垃圾回收。
public class Demo1_23 {
public static void main(String[] args) {
int i = 0;
try {
for (int j = 0; j < 10; j++) {
// String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
public class Demo1_23 {
public static void main(String[] args) {
int i = 0;
try {
for (int j = 0; j < 10; j++) {
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
for (int j = 0; j < 100000; j++) {
String.valueOf(j).intern();
i++;
}
清理了一些未引用的字符串常量。
StringTable性能调优
- 调整 -XX:StringTableSize=桶个数
- 考虑是否将字符串对象放入池中,即用intern
参数调优
StringTable的数据结构实现是哈希表,调优即对哈希表的桶的个数的调整。
jvm参数
-XX:StringTableSize=1009
由于入池时候会先去查找StringTable中有无这个字符串,hash表的的寻找,桶个数越多越快。
当桶个数为1009时,耗费12097毫秒
intern调优
直接内存
定义
不是虚拟机的内存,是系统内存。Direct Memory
- 常见于NIO操作时,用于数据缓存区
- 分配回收成本过高,但读写性能高
- 不受JVM内存回收管理
基本使用
java操作磁盘文件
当java读取磁盘文件时,会从用户态切换到内核态,才能去操作系统内存。读取时,系统内存先开辟一块缓存空间,磁盘文件分块读取。然后java虚拟机内存再开辟缓存空间new Byte[]来读取系统内存的文件。由于有从系统内存读取到java虚拟机的内存,所以效率较低。
NIO操作磁盘文件
读取磁盘文件时,会有一块直接内存,java虚拟机和视同内存都能访问使用,所以效率更高。
内存溢出
public class demo1_24 {
static 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);
}
}
}
分配和释放原理
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法。
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存。
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;
/*
* -XX:+DisableExplicitGC 显式的
*/
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(); // 显式的垃圾回收,Full GC
System.in.read();
}
}
-
直接内存不受jvm内存管理,但是这段代码示例中,当byteBuffer = null,执行垃圾回收后,直接内存却被释放了。这是因为跟jdk中的一个类Unsafe有关。
-
-XX:+DisableExplicitGC 禁用显式的垃圾回收。只有等到jvm自己进行垃圾回收才会回收。
/**
* 直接内存分配的底层原理:Unsafe
*/
public class Demo1_27 {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (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);
}
}
}
Unsafe类调用其方法freeMemory来释放了直接内存。
ByteBuffer源码
ByteBuffer分配直接内存是通过new 了一个DirectByteBuffer
在DirectByteBuffer内部可以看到,确实是用Unsafe类去分配内存。
DirectByteBuffer的run方法释放了直接内存。