文章目录
视频简介
JVM篇 笔记2:垃圾回收
https://blog.csdn.net/qq_46539281/article/details/120174805
JVM篇 笔记3:类加载与字节码技术
https://blog.csdn.net/qq_46539281/article/details/120174860
JVM篇 笔记4:内存模型
https://blog.csdn.net/qq_46539281/article/details/120174885
什么是JVM?
定义:
Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境)
好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检擦
- 多态
比较:
JVM JRE JDK的区别
学习JVM有什么用?
用处:
- 面试
- 理解底层的实现原理
- 中高级程序员的必备技能
常见的JVM
JVM是一个规范
主要学习的是这个HotSpot 这个,这个也是最著名的。
学习路线
.class文件 先被 类加载器 加载进入JVM内存结构进行执行。
- 类放在方法区 Method Area
- 类创建的实例对象放在 堆Heap中
- 堆里面的对象在调用方法时,会用到 虚拟机栈,程序计数器,本地方法栈
- 方法执行时,每行代码 由 执行引擎中的 解释器 进行逐行 执行
- 方法里面的热点代码(被频繁调用的代码),会使用 即时编译器 进行优化。
- 垃圾回收 会对 堆 里面 一些不再引用的对象,进行垃圾回收。
- java代码不太容易实现的功能,需要调用底层系统的功能,需要借助 本地方法接口
一. 内存结构
1. 程序计数器
1.1 定义
Program Counter Register 程序计数器(寄存器)
作用:是记住下一条jvm指令的执行地址
特点
- 是线程私有的
- 不会存在内存溢出(规定的)
作用解释:
解释器:将拿到的二进制字节码 转化为 机器码
CPU : 执行机器码
程序计数器:记住下一条JVM 指令的地址
程序计数器(概念)在物理上实现:寄存器。
图中二进制字节码前的数字就i是 地址
执行流程:将地址为0的指令,发送给解释器,同时将将下一条指令 的地址4 ,给程序计数器 ,解释器 将 字节码 转化为 机器码后,发送给cpu.执行完后, 解释器 从 程序计数器 中拿取执行 指令 的地址 , 根据地址拿取指令, 将下一条指令的地址,存放到 程序计数器中。
反复执行,直到结束。
特点解释
是线程私有的 (一个线程对应一个程序计数器)
根据多线程的案例,当有多个线程在执行的时候,CPU随机执行其中一个A线程,A线程还未执行完毕。然后执行其他的B线程,当重新执行线程A时,会继续接着上一次未执行的代码的地方进行执行。
那么,计算机就是通过线程中程序计数器,来记录 还未执行的代码的地址。
2. 虚拟机栈
2.1 定义
Java Virtual Machine Stacks (Java 虚拟机栈)
- 每个线程运行时,所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame) 组成,对应着当前正在执行的那个方法
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
先进后出,后进先出
- 栈-- 线程运行需要的内存空间
- 里面的每一个元素叫: 栈帧
- 一个栈帧对应一次方法的调用。
- 栈帧:每个方法运行时,需要的内存
案例:
/**
* 演示栈帧
*/
public class Demo1_1 {
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;
}
}
右边的参数:就是栈帧中需要的变量。同时需要内存。
问题解析:
- 垃圾回收是否涉及栈内存? 不
栈帧内存,当对应的方法执行完毕后,就释放该栈帧内存, gc主要是回收的堆 里面对应不用对象的内存
- 栈内存分配越大越好吗?
栈内存分配的越大,反而会将线程数变少,一个计算机的物理内存是一定的,一个栈对应一个线程。
栈内存分配的越大,不会增加运行的效率的。
- 方法内的局部变量是否线程安全?
1. 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
2. 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
代码解释1:
/**
* 局部变量的线程安全问题
*/
public class Demo1_18 {
// 多个线程同时执行此方法
static void m1() {
int x = 0;
for (int i = 0; i < 5000; i++) {
x++;
}
System.out.println(x);
}
}
不会出现线程问题。
会出现安全问题的。
代码解释2:
/**
* 局部变量的线程安全问题
*/
public class Demo1_17 {
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());
}
public static void m2(StringBuilder sb) { //不是安全的,不是该方法私有
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static StringBuilder m3() { //也不是线程安全的,因为 返回了 这个对象。
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
2.2 栈内存溢出
栈内存溢出的原因:
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
栈帧过多导致栈内存溢出的情况:(递归调用会出现,自己调用自己)
方法一 调用方法二 , 方法二调用方法三,递归的形式调用下去,直到栈内存不够。
栈帧过大导致栈内存溢出的情况:(不太容易出现)
栈帧里面,一般存放的是变量,等,,所以不太容易出现这个问题。
案例1:栈帧过多导致栈内存溢出
/**
* 演示栈内存溢出 java.lang.StackOverflowError
* -Xss256k
*/
public class Demo1_2 {
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:自己没那么傻,写个死递归。出现栈内存溢出,还有第三方库导致的。
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 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;
}
}
2.3 线程运行诊断
案例1:CPU占用过多(某个Java代码出问题了)
定位:
- 用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
- jstack 进程id
- 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
/**
* 演示 cpu 占用过高
*/
public class Demo1_16 {
public static void main(String[] args) {
new Thread(null, () -> {
System.out.println("1...");
while(true) {
}
}, "thread1").start();
new Thread(null, () -> {
System.out.println("2...");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2").start();
new Thread(null, () -> {
System.out.println("3...");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread3").start();
}
}
案例2:程序运行很长时间没有结果
原因:可能是线程死锁导致的。
/**
* 演示线程死锁
*/
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();
}
}
3. 本地方法栈
java虚拟机调用本地方法时,需要给本地方法提供内存空间。本地方法运行的时候,使用的内存就是本地方法栈。
4. 堆
以上三个,虚拟机栈,程序计数器,本地方法栈 都是属于线程私有的。
下面两个,堆,方法区是属于,所有线程共有的
4.1 定义
Heap 堆
- 通过 new 关键字,创建对象都会使用堆内存
特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
4.2 堆内存溢出
出现的原因:一直创建对象,且一直有线程使用,那么,垃圾回收机制就回收不了。最后,导致 堆内存溢出。
代码案例:
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);
}
}
}
4.3 堆内存诊断
cmd中使用:
- jps 工具
查看当前系统中有哪些 java 进程 - jmap 工具
查看堆内存占用情况 jmap - heap 进程id - jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
案例1:
/**
* 演示堆内存
*/
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(30000);
array = null;
System.gc();
System.out.println("3...");
Thread.sleep(1000000L);
}
}
打印 出 1 的时候 ,堆的使用内存情况:
打印 出 2 的时候 ,堆的使用内存情况:
打印 出 3 的时候 ,堆的使用内存情况:
还是该案例:cmd使用jconsole命令 会出现以下界面
案例2:垃圾回收后,内存占用仍然很高
使用工具进行排查 jvisualvm , 图形界面工具
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];
}
5. 方法区
5.1 定义
方法区是所有JVM线程共享的区域。存储了跟类的结构有关的信息。有成员变量,方法数据,成员方法和构造方法的代码部分。
方法区是在JVM启动时被创建。方法区逻辑上是堆,但是规定生产时,不强制一定要使用堆。
5.3 方法区内存溢出
- 1.8 以前(1.6)会导致永久代内存溢出
- 1.8 之后会导致元空间内存溢出,元空间使用的是系统内存。
演示元空间内存溢出
为了方便演示:设置JVM参数:-XX:MaxMetaspaceSize=8m,因为元空间使用的系统的内存,足够大,所以需要设置参数。
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);
}
}
}
演示永久代内存溢出 1.6
使用1.6的jdk,进行执行,需要设置jvm参数 -XX:MaxPermSize=8m
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxPermSize=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);
}
}
}
常用场景:
- spring:通过反射,进行加载class对象。
- Mybatis
5.4 运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息 - 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量
池,并把里面的符号地址变为真实地址
上图中的参数是 -v 不是 - c
反编译
常量池就是一张表,存储了类型,值等信息。
5.5 StringTable (串值)
先看几道面试题:
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);
为了更好的理解上述题,看下面的案例
5.5.1 案例1
// 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
System.out.println(s3 == s5);
}
}
5.5.2 案例2:延迟实例化
/**
* 演示字符串字面量也是【延迟】成为对象的
*/
public class TestString {
public static void main(String[] args) {
int x = args.length;
System.out.println(); // 字符串个数 2275
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"); // 字符串个数 2285
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); // 字符串个数
}
}
这里引用了常量池中的字面值,就会放入到串池中去。根据key找value, StringTable就是一个hash表,没有则添加,有则直接使用,不会存储。
5.5.3 StringTable 特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
-
1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串
池中的对象返回 -
1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,
放入串池, 会把串池中的对象返回
-
jdk = 1.8
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
}
}
jdk = 1.6
public class Demo1_23 {
// ["ab", "a", "b"]
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();
// 将这个字符串对象复制一份放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
String x = "ab";
System.out.println( s2 == x); // true
System.out.println( s == x ); // false
}
}
这个代码放在1.8 环境下,就是 true , true 了。
5.5.4 面试题解答
/**
* 演示字符串相关面试题
*/
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 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);
}
}
5.6 StringTable 位置
以下案例在进行推断出 StringTable 所在的位置(1.6 / 1.8版本中)
import java.util.ArrayList;
import java.util.List;
/**
* 演示 StringTable 位置
* 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
* 在jdk6下设置 -XX:MaxPermSize=10m
*/
public class Demo1_6 {
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);
}
}
}
5.7 StringTable 垃圾回收
串池中没有被引用的字符常量,也会被gc回收。
-Xmx10m: 设置JVM中堆内存的大小。
-XX:+PrintStringTableStatistics 打印字符串常量,在串池的个数,等信息。
-XX:+PrintGCDetails -verbose:gc 垃圾回收机制的信息。
import java.util.ArrayList;
import java.util.List;
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Demo1_7 {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
import java.util.ArrayList;
import java.util.List;
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Demo1_7 {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 100000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
内存不足,导致启动了垃圾回收机制。回收了不用的字符串常量
5.8 StringTable 性能调优
- 调整 -XX:StringTableSize=桶个数
这个桶的个数,就是hash中表中的数组的个数,根据数据结构中来说,需要插入相同的数据来说,数组的个数越多,查询的速度越快。反之,越慢。
每当应用一个字符串字面值时,先回从 串池(StringTable)中 进行查询,如果有,就返回该值的地址。没有就插入其中。
所以 性能调优就是根据这个桶的数量来的。
linux.words 文件中,有48000多个单词。
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 Demo1_24 {
public static void main(String[] args) throws IOException {
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.out.println("cost:" + (System.nanoTime() - start) / 1000000);
}
}
}
-XX:StringTableSize=1009 值 越大,执行的越快。
性能问题
6. 直接内存
不属于Java虚拟机的内存,属于系统的内存。
6.1 定义
Direct Memory
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
读写性能高,
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 演示 ByteBuffer 作用
*/
public class Demo1_9 {
static final String FROM =
"E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
static final String TO = "E:\\a.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); // io 用时:1535.586957 1766.963399 1359.240226 15s多
directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592 4s多
}
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);
}
}
为什么 directBuffer 读写能力 这么高 ?? 下面是原理!!!
普通的java 的 io操作
使用直接内存的操作
6.2 直接内存溢出案例
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* 演示直接内存溢出
*/
public class Demo1_10 {
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);
// 这个方法生成直接内存 ,但是byteBuffer 对象是存放在堆中的。
list.add(byteBuffer);
i++;
}
} finally {
System.out.println(i);
}
// 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
// jdk8 对方法区的实现称为元空间
}
}
6.3 直接内存释放原理
直接调用gc() 方法, 直接内存会被释放掉的。不是gc直接回收的,而是间接释放的。
import java.io.IOException;
import java.nio.ByteBuffer;
public class Demo1_26 {
static 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(); // 显式的垃圾回收,Full GC , 会影响性能的。
System.in.read();
}
}
ByteBuffer的原理
import sun.misc.Unsafe;
import java.io.IOException;
import java.lang.reflect.Field;
/**
* 直接内存分配的底层原理: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() { // 通过反射获得 Unsafe 对象
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对象
long base = unsafe.allocateMemory(_1Gb); 分配直接内存的空间,返回地址
unsafe.freeMemory(base); 根据地址释放直接内存。
6.3 分配和回收原理
1. 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
2. ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦
ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调
用 freeMemory 来释放直接内存
6.4 禁用显式回收对直接内存的影响
-XX:+DisableExplicitGC 禁止显式使用gc方法
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* 禁用显式回收对直接内存的影响
*/
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();
}
}
需要释放直接空间的话,可以直接使用gc 。即可。