网站建设ihuibest国际域名注册网站
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; // hellohellohellohelloi++;}} 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 MbSystem.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 在编译期间的优化,结果已经在编译期确定为abSystem.out.println(s3 == s5);//trueSystem.out.println(s3 == s4);//falseSystem.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 arrayreturn 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");// 字符串个数 1253System.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"; // abString s4 = s1 + s2; // new String("ab")String s5 = "ab";String s6 = s4.intern();// 问System.out.println(s3 == s4); // falseSystem.out.println(s3 == s5); // trueSystem.out.println(s3 == s6); // trueString 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=10000String.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 GCSystem.in.read();}
}
结果分析
配置参数可以禁用显式的垃圾回收System.gc()
,此时ByteBuffer
无法被回收进而导致直接内存无法释放。此时可以通过直接使用unsafe.freeMemory()
进行主动释放内存。