JVM学习02:内存结构
1. 程序计数器
1.1、定义
Program Counter Register 程序计数器(寄存器)
-
作用:是记住下一条jvm指令的执行地址
-
特点:
- 是线程私有的
- 不会存在内存溢出
1.2、作用
程序计数器物理上是由寄存器来实现的,因为寄存器的读取速度比较快,而读取指令地址这个动作比较频繁。
2、虚拟机栈
2.1、定义
Java Virtual Machine Stacks (Java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
栈可以看做一个弹夹,先进后出。
测试代码:
package com.jvm.stack;
/**
* 演示栈帧
*/
public class demo01 {
public static void main(String[] args) throws InterruptedException {
method1();
}
private static void method1() {
method2(1, 2);
}
private static int method2(int a, int b) {
int c = a + b;
return c;
}
}
查看结果:我们debug发现每调用一个新的方法时,该方法就会在顶部压入栈,当这个方法运行完,就会在栈中弹出。最上面的那个方法就是活动栈帧。
问题辨析:
-
垃圾回收是否涉及栈内存?
答:不需要,因为每个栈帧内存在每个方法调用完后就会弹出栈。
-
栈内存分配越大越好吗?
答:不是,因为我们的物理内存是不变的,内存越大,分配的线程数就会越小。
-
方法内的局部变量是否线程安全?
答:如果方法内局部变量没有逃离方法的作用访问,它是线程安全的;如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。测试代码如下:
测试代码:
package com.jvm.stack;
/**
* 局部变量的线程安全问题
*/
public class demo02 {
//多个线程同时执行此方法
//是线程安全的
static void m1() {
int x = 0;
for (int i = 0; i < 5000; i++) {
x++;
}
System.out.println(x);
}
}
package com.jvm.stack;
/**
* 局部变量的线程安全问题
*/
public class demo03 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
}
//线程安全
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
//需要考虑线程安全,参数sb其他线程也可以访问并修改到
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
//需要考虑线程安全,sb作为了返回结果,其他线程可以拿到并修改它
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
2.2、栈内存溢出
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
测试代码1:
package com.jvm.stack;
/**
* 演示栈内存溢出 java.lang.StackOverflowError
* -Xss256k:设置栈内存大小
* 配置参数前调用方法18823次报错,配置参数后调用方法2080次报错
*/
public class demo04 {
private static int count;
public static void main(String[] args) {
try {
method1();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(count);
}
}
private static void method1() {
count++;
method1();
}
}
配置参数:
测试代码2:
package com.jvm.stack;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.List;
/**
* json 数据转换
*/
public class demo05 {
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));//转换为json格式
}
}
//员工类
class Emp {
private String name;
@JsonIgnore //转为为json的时候,这个属性不转了,否则会变成套娃。
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;
}
}
测试结果:
- 不写
@JsonIgnore
注解会报错 - 加上
@JsonIgnore
后在转为json的时候回忽视此属性,得到结果:
{"name":"Market","emps":[{"name":"zhang"},{"name":"li"}]}
2.3、线程运行诊断
案例1: cpu 占用过多
定位:(linux系统下操作)
- 用
top
定位哪个进程对cpu的占用过高。 ps H -eo pid,tid,%cpu | grep 进程id
(用ps命令进一步定位是哪个线程引起的cpu占用过高)。
ps H -eo pid,tid,%cpu #ps查看线程情况;-H打印进程树;eo输出所有感兴趣的内容
ps H -eo pid,tid,%cpu | grep 32655 #grep 进程号,根据进程号进行过滤,只看进程32655的所有线程的三项指标
jstack 进程id
查看线程信息。- 可以根据线程id(要转为16进制再查找)找到有问题的线程,进一步定位到问题代码的源码行号。
注意:图片截的视频里的类名叫Demo_16,我这个类名字叫demo06
问题:我用的mac系统命令不一样?出现的结果不一样。
案例2:程序运行很长时间没有结果
用同样的方法定位到有问题的代码,发现出现死锁。
测试代码:
package com.jvm.stack;
/**
* 演示线程死锁
*/
class A{};
class B{};
public class demo07 {
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();
}
}
我们发现一开始锁住了a,睡眠1秒再锁了b,而锁了b立即再锁a,发现a已经没锁住了,两秒后再锁b,而b也被锁住了,出现死锁。
3、本地方法栈
在 JVM 中调用一些本地方法时需要给本地方法提供的内存空间。
本地方法:由于java有限制,不可以直接与操作系统底层交互,所以需要一些用c/c++编写的本地方法与操作系统底层的API交互,java可以间接的通过本地方法来调用底层功能。
例如,下面Object类中具有native标识的clone()方法:
protected native Object clone() throws CloneNotSupportedException;
4、堆
4.1、定义
Heap 堆:
- 通过 new 关键字,创建对象都会使用堆内存。
特点:
- 它是线程共享的,堆中对象都需要考虑线程安全的问题。
- 有垃圾回收机制。
4.2、堆内存溢出
测试代码:
package com.jvm.heap;
import java.util.ArrayList;
import java.util.List;
/**
* 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
* -Xmx8m:配置堆空间的大小
*/
public class demo01 {
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);
}
}
}
4.3、堆内存诊断
-
jps 工具
- 查看当前系统中有哪些 java 进程:
jps
。
- 查看当前系统中有哪些 java 进程:
-
jmap 工具
- 查看堆内存占用情况:
jmap -heap 进程id
。
- 查看堆内存占用情况:
-
jconsole 工具
- 图形界面的,多功能的监测工具,可以连续监测:
jconsole
。
- 图形界面的,多功能的监测工具,可以连续监测:
测试代码:
package com.jvm.heap;
/**
* 演示堆内存
*/
public class demo02 {
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);
}
}
jps
运行程序,终端输入jps
查看进程:
jmap
-
jmap工具mac环境jdk8不支持,需要替换成jdk11。
参考博客:https://juejin.cn/post/7028758774621929480 。
-
也可以设置参数来调试:
-XX:+PrintGCDetails
。
我使用的第二个方法:
当输出"1…",byte数组还没创建时,堆内存的情况:
当输出"2…",byte数组创建后,堆内存的情况:
当输出"3…",进行完垃圾回收机制后,堆内存的情况:
我们可以看到堆内存中eden区内存的变化。
jconsole
在终端输入jconsole
,选择连接正在运行的程序,点击不安全连接,可以查看监测的情况:
可以点击内存,再点击GC进行垃圾回收。
Jvisualvm
案例
- 垃圾回收后,内存占用仍然很高。
需要工具VisualVM,终端输入jvisualvm
可以打开,但是我又失败了,自己重新下载的这个工具,参考博客:
https://blog.csdn.net/xiaomolimicha/article/details/126911104
https://blog.csdn.net/Tanganling/article/details/119790892
开启工具后,点击堆Dump抓取堆的当前快照:
查看占用内存最大的对象:
我们发现ArrayList占用最大,打开发现存放的Student对象的big属性占了1M:
查看代码分析,找到问题:
package com.jvm.heap;
import java.util.ArrayList;
import java.util.List;
/**
* 演示查看对象个数 堆转储 dump
*/
public class demo03 {
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];
}
5、 方法区
5.1、定义
方法区是所有java虚拟机线程的共享区域;存储类的结构的相关信息,如运行时常量池、成员变量、方法数据、成员方法和构造器的代码等;方法区在虚拟机启动时创建,其逻辑上是堆的一个组成部分,但在实现时不同的JVM厂商可能会有不同的实现。
5.2、组成
5.3、方法区内存溢出
- 1.8 以前会导致永久代内存溢出。
演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
-XX:MaxPermSize=8m:配置元空间大小
- 1.8 之后会导致元空间内存溢出。
演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=8m:配置永久代大小
测试代码:(JDK1.8)
package com.jvm.metaspace;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
*/
public class demo01 extends ClassLoader { // 可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
demo01 test = new demo01();
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);
}
}
}
场景:
-
spring
-
mybatis
5.4、运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息。
- 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量
池,并把里面的符号地址变为真实地址。
测试代码:
package com.jvm.lesson02;
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class test {
public static void main(String[] args) {
System.out.println("hello world");
}
}
我们对上面代码的class文件进行反编译,并查看字节码文件的内容。二进制字节码包括类基本信息、常量池、类方法定义(包含了虚拟机指令)。
我们查看一下主方法,每一条指令后面跟的是常量池的地址。
在常量池中找到对应的地址,后面还有地址的话继续找。
最后这条指令找的是 java/lang/System类下的out成员变量,类行为java/io/PrintStream。
5.5、StringTable
5.5.1、StringTable 特性
-
常量池中的字符串仅是符号,第一次用到时才变为对象。
-
利用串池的机制,来避免重复创建字符串对象。
-
字符串变量拼接的原理是 StringBuilder (1.8)。
-
字符串常量拼接的原理是编译期优化。
-
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池。
-
1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回。
-
1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回。
-
测试代码1:
package com.jvm.lesson01.stringtable;
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class demo01 {
// 常量池中的信息,都会被加载到运行时常量池中, 这时 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
System.out.println(s3 == s5);//true
System.out.println(s3 == s4);//false
System.out.println(s4 == s5);//false
}
}
结果分析:
对代码进行反编译,找到主方法,查看相关信息。
类加载时,常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象。当执行到String s1 = "a";
时,符号会变成字符串对象,并且开辟一个 StringTable 空间,把字符串对象放入。
注意:上面”本地“写错了,改成”局部“。
当执行到String s4 = s1 + s2;
时,从下面图中可以看出,新创建了一个StringBuilder对象,然后append("a").append("b")
,然后再toString()
。查看toString()
方法源码为new String("ab")
。
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
这样我们可以知道,s3在串池中,而s4在堆中,因此s3 == s4
为false。
当执行String s5 = "a" + "b";
时,相当于String s3 = "ab";
。
因此,s3 == s5
为true,s4 == s5
为false。
测试代码2:
package com.jvm.lesson01.stringtable;
/**
* 演示字符串字面量也是【延迟】成为对象的
*/
public class demo02 {
public static void main(String[] args) {
int x = args.length;
System.out.println();
System.out.print("1");// 字符串个数 1253
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");// 字符串个数 1263,字符串个数多了十个
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");// 字符串个数 1263,字符串个数不变了
System.out.print(x);
}
}
结果分析
对代码进行Debug调试,当执行到第一个 System.out.print("1");
时,内存中字符串个数位1253;当执行到第二个 System.out.print("1");
时,字符串个数位1263,串池中添加了十个对象,说明字符串字面量是延迟成为对象的;而执行到最后的System.out.print("0");
时,字符串个数不变了,说明重复的字符串在串池中不会添加了。
测试代码3:
Jdk1.8:
public class demo03 {
// ["ab", "a", "b"]
public static void main(String[] args) {
// 堆 new String("a") new String("b") new String("ab")
String s = new String("a") + new String("b");//这时"ab"还不在串池中
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回.
System.out.println( s == "ab" );//true;
System.out.println( s2 == "ab");//true;
}
}
public class demo03 {
// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
// 堆 new String("a") new String("b") new String("ab")
String s = new String("a") + new String("b");//这时"ab"还不在串池中
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回.
//s放入失败
System.out.println( s == x );//false;
System.out.println( s2 == x);//true;
}
}
Jdk1.6:
public class demo03 {
// ["ab", "a", "b"]
public static void main(String[] args) {
// 堆 new String("a") new String("b") new String("ab")
String s = new String("a") + new String("b");
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回。
System.out.println( s == "ab" );//false;
System.out.println( s2 == "ab");//true;
}
}
public class demo03 {
// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
// 堆 new String("a") new String("b") new String("ab")
String s = new String("a") + new String("b");
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回。
System.out.println( s == x );//false;
System.out.println( s2 == x);//true;
}
}
测试代码4:面试题
package com.jvm.lesson01.stringtable;
/**
* 演示字符串相关面试题
*/
public class demo04 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // ab
String s4 = s1 + s2; // new String("ab")
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")
x2.intern();
String x1 = "cd";
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);//false;调换:true;jdk1.6+调换:false
}
}
5.5.2、StringTable 位置
测试代码:
package com.jvm.lesson01.stringtable;
import java.util.ArrayList;
import java.util.List;
/**
* 演示 StringTable 位置
* 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit:关闭这个开关
* 在jdk6下设置 -XX:MaxPermSize=10m
*/
public class demo05 {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
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下:会报错栈内存溢出。
jdk1.6下:会报错永久代内存溢出。
5.5.3、StringTable 垃圾回收
测试代码:
/**
* 演示 StringTable 垃圾回收
* -Xmx10m
* -XX:+PrintStringTableStatistics :打印字符串表的统计信息
* -XX:+PrintGCDetails -verbose:gc :打印垃圾回收的信息
*/
public class demo06 {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 10000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
结果分析:
代码中我们往串池中添加10000个字符串,而统计表中只有905个字符串对象,说明发生了垃圾回收。
5.5.4、StringTable 性能调优
- 调整 -XX:StringTableSize=桶个数
- 考虑将字符串对象是否入池
测试代码1:
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 演示串池大小对性能的影响
* -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
*/
public class demo07 {
public static void main(String[] args) throws IOException {
//在try的括号中声明的类都必须实现java.io.Closeable接口,这样try就会自动将声明的流在使用完毕后自动关闭。
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if (line == null) {
break;
}
line.intern();
}
//System.nanoTime():返回的是纳秒
System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
}
}
}
结果分析:
把桶的个数调整的越大,消耗的时间就越小,效率就越高。
测试代码2:
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
/**
* 演示 intern 减少内存占用
*/
public class demo08 {
public static void main(String[] args) throws IOException {
List<String> address = new ArrayList<>();
System.in.read(); //按回车进行下一步
for (int i = 0; i < 10; i++) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
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)/1000000);
}
}
System.in.read();
}
}
结果分析
使用VisualVM分析,我们发现,当使用intern()入池时,字符串对象所占内存比没有使用intern()入池时明显变少了。
6、直接内存
6.1、定义
直接内存(Direct Memory):
- 常见于 NIO 操作时,用于数据缓冲区。
- 分配回收成本较高,但读写性能高。
- 不受 JVM 内存回收管理。
测试代码1:
package com.jvm.lesson01.direct;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 演示 ByteBuffer 作用
*/
public class demo01 {
//不同的系统使用的路径分隔符也不同:windows和DOS系统默认使用\来表示,在Java字符串中需要用\\表示一个\,UNIX和URL使用/来表示。
static final String FROM = "/Users/wangcheng/IdeaProjects/JVM02/shipin.mp4";
static final String TO = "/Users/wangcheng/IdeaProjects/JVM02/交换余生.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
//常规读写操作
io(); // io 用时:64.699292
//使用直接内存读写操作
directBuffer(); // directBuffer 用时:48.394292
}
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 len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException 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 len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
}
结果分析:
使用直接内存进行读写操作比使用常规方法进行读写操作耗时少。
常规IO操作:磁盘文件要先读到系统缓冲区,再读到java缓冲区中,造成了不必要的复制,效率较低。
使用直接内存读写操作:少了一次缓冲区的复制操作,提高了效率。
测试代码2:
package com.jvm.lesson01.direct;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* 演示直接内存溢出 : Direct buffer memory
*/
public class demo02 {
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);
}
}
}
结果分析:
会报错java.lang.OutOfMemoryError: Direct buffer memory
。
6.2、分配和回收原理
-
使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法。
-
ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存。
测试代码1:
package com.jvm.lesson01.direct;
import sun.misc.Unsafe;
import java.io.IOException;
import java.lang.reflect.Field;
/**
* 直接内存分配的底层原理:Unsafe
*/
public class demo04 {
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();
}
//得到Unsafe对象
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);
}
}
}
结果分析:
打开任务管理器来查看监测信息,当代码运行到分配内存时,java进程多占了1G的内存,运行到释放内存时,java进程的内存被释放了。我们可以看出,直接内存的释放是由一个 unsafe 对象控制的。
原理分析:
- 点进
ByteBuffer.allocateDirect()
方法进行查看,它创建了一个DirectByteBuffer
对象。
- 再点进
DirectByteBuffer
对象查看它的构造方法,可以看到构造器中调用了unsafe对象完成了对直接内存的分配,内存释放在下面的Cleaner
对象中。
- 点进去
Cleaner
的回调任务对象Deallocator
,它的run()
方法中包含释放内存的方法。
- 当
ReferenceHandler
线程监测到cleaner
关联的对象(this
对象,也就是DirectByteBuffer
)被回收后,会自动触发cleaner
对象的clean()
方法,clean
方法会执行回调任务对象Deallocator
的run()
方法来释放直接内存。
测试代码2:
package com.jvm.lesson01.direct;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* 禁用显式回收对直接内存的影响
*/
public class demo03 {
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();
}
}
结果分析
配置参数可以禁用显式的垃圾回收System.gc()
,此时ByteBuffer
无法被回收进而导致直接内存无法释放。此时可以通过直接使用unsafe.freeMemory()
进行主动释放内存。