当前位置: 首页 > news >正文

JVM学习02:内存结构

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发现每调用一个新的方法时,该方法就会在顶部压入栈,当这个方法运行完,就会在栈中弹出。最上面的那个方法就是活动栈帧

在这里插入图片描述


问题辨析:

  1. 垃圾回收是否涉及栈内存?

    答:不需要,因为每个栈帧内存在每个方法调用完后就会弹出栈。

  2. 栈内存分配越大越好吗?

    答:不是,因为我们的物理内存是不变的,内存越大,分配的线程数就会越小。

  3. 方法内的局部变量是否线程安全?

    答:如果方法内局部变量没有逃离方法的作用访问,它是线程安全的;如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。测试代码如下:

测试代码:

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、堆内存诊断

  1. jps 工具

    • 查看当前系统中有哪些 java 进程:jps
  2. jmap 工具

    • 查看堆内存占用情况: jmap -heap 进程id
  3. 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 对象控制的。

原理分析:

  1. 点进ByteBuffer.allocateDirect()方法进行查看,它创建了一个DirectByteBuffer对象。

在这里插入图片描述

  1. 再点进DirectByteBuffer对象查看它的构造方法,可以看到构造器中调用了unsafe对象完成了对直接内存的分配,内存释放在下面的Cleaner对象中。

在这里插入图片描述

  1. 点进去Cleaner的回调任务对象Deallocator,它的run()方法中包含释放内存的方法。

在这里插入图片描述

  1. ReferenceHandler线程监测到cleaner关联的对象(this对象,也就是DirectByteBuffer)被回收后,会自动触发cleaner对象的clean()方法,clean方法会执行回调任务对象Deallocatorrun()方法来释放直接内存。

在这里插入图片描述


测试代码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()进行主动释放内存。

相关文章:

JVM学习02:内存结构

JVM学习02&#xff1a;内存结构 1. 程序计数器 1.1、定义 Program Counter Register 程序计数器&#xff08;寄存器&#xff09; 作用&#xff1a;是记住下一条jvm指令的执行地址 特点&#xff1a; 是线程私有的不会存在内存溢出 1.2、作用 程序计数器物理上是由寄存器来实…...

6年软件测试经验,从我自己的角度理解自动化测试

接触了不少同行&#xff0c;由于他们之前一直做手工测试&#xff0c;现在很迫切希望做自动化测试&#xff0c;其中不乏工作5年以上的人。 本人从事软件自动化测试已经近6年&#xff0c;从server端到web端&#xff0c;从API到mobile&#xff0c;切身体会到自动化带来的好处与痛楚…...

三种方式查看linux终端terminal是否可以访问外网ping,curl,wget

方法1&#xff1a;ping注意不要用ping www.google.com.hk来验证&#xff0c;因为有墙&#xff0c;墙阻止了你接受网址发回的响应数据。即使你那啥过&#xff0c;浏览器都可以访问Google&#xff0c;terminal里面也是无法得到响应 百度在墙内&#xff0c;所以可以正常拿到响应信…...

【Call for papers】SIGCOMM-2023(CCF-A/计算机网络/2023年2月15日截稿)

ACM SIGCOMM is the flagship annual conference of the ACM Special Interest Group on Data Communication (SIGCOMM). ACM SIGCOMM 2023, the 37th edition of the conference series, will be held in New York City, US, September 10 - 14, 2023. 文章目录1.会议信息2.时…...

Chapter5:机器人感知

ROS1{\rm ROS1}ROS1的基础及应用&#xff0c;基于古月的课&#xff0c;各位可以去看&#xff0c;基于hawkbot{\rm hawkbot}hawkbot机器人进行实际操作。 ROS{\rm ROS}ROS版本&#xff1a;ROS1{\rm ROS1}ROS1的Melodic{\rm Melodic}Melodic&#xff1b;实际机器人&#xff1a;Ha…...

[acwing周赛复盘] 第 90 场周赛20230211 补

[acwing周赛复盘] 第 90 场周赛20230211 补 一、本周周赛总结二、 4806. 首字母大写1. 题目描述2. 思路分析3. 代码实现三、4807. 找数字1. 题目描述2. 思路分析3. 代码实现四、4808. 构造字符串1. 题目描述2. 思路分析3. 代码实现六、参考链接一、本周周赛总结 T1 模拟T2 模拟…...

数组

一、数组中重复的数字题目描述&#xff1a;在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的&#xff0c;但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如&#xff0c;如果输入长度为7的数组{2,3,1…...

MicroBlaze系列教程(4):AXI_UARTLITE的使用

文章目录 @[toc]AXI_UARTLITE简介MicroBlaze添加串口IP常用函数使用示例参考资料工程下载本文是Xilinx MicroBlaze系列教程的第4篇文章。 AXI_UARTLITE简介 axi_uartlite是Xilinx提供axi-lite接口的通用串口IP核,用AXI-Lite总线接口和用户进行交互,速度可以根据不同的芯片调…...

GO 中的 init 函数

前言 go 语言中有一个非常神奇的函数 init ,它可以在所有程序执行开始前被执行&#xff0c;并且每个 package 下面可以存在多个 init 函数&#xff0c;我们一起来看看这个奇怪的 init 函数。 init 特性 init 函数在 main 函数之前执行&#xff0c;并且是自动执行&#xff1b…...

使用C#编写k8s CRD Controller

本文项目地址&#xff1a;k8s-crd - Repos (azure.com)CRDCRD指的是Custom Resource Definition。开发者更多的关注k8s对于容器的编排与调度&#xff0c;这也是k8s最初惊艳开发者的地方。而k8s最具价值的地方是它提供了一套标准化、跨厂商的 API、结构和语义。k8s将它拥有的一切…...

Ansible---playbook剧本

目录 引言&#xff1a;什么是playbook&#xff1f; 一、Playbook 1.1、playbook中的核心元素 1.2、playbook中的基础组件 1.3、playbook格式说明 1.4、实例&#xff1a;httpd服务剧本 二、playbook中的模块 2.1、Templates 模块 2.2、tags 模块 2.3、Roles 模块 引言&…...

Delphi 中TImageCollection和TVirtualImageList 控件实现high-DPI

一、概述RAD Studio允许你通过使用TImageCollection组件和TVirtualImageList组件&#xff0c;在你的Windows VCL应用程序中包含缩放、高DPI、多分辨率的图像。这两个组件位于Windows 10面板中&#xff1a;注意&#xff1a;如果你使用FireMonkey进行跨平台应用&#xff0c;请看T…...

Ros中如何给UR5配置自定义工具 | 在Rviz中给UR5机器人装载定义工具 | UR5配置自定义末端执行器

前言 在学习和项目研究的过程中&#xff0c;我需要在Ur5e上装上工具&#xff0c;以对现实场景进行仿真。网上会有一些装载/配置现成的夹爪&#xff0c;例如Robotiq等。但和我们装载自定义工具的场景还有些差异&#xff0c;因此写一篇博客记录&#xff0c;可能有偏差。如果有问…...

数据库 delete 表数据后,磁盘空间为什么还是被一直占用?

插&#xff1a; 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 坚持不懈&#xff0c;越努力越幸运&#xff0c;大家一起学习鸭~~~ 最近有个上位机获取下位机上报数据的项目&#xff0c…...

docker-微服务篇

docker学习笔记1.docker简介1.1为什么会出现docker&#xff1f;1.2docker理念1.3虚拟机&#xff08;virtual machine&#xff09;1.4容器虚拟化技术1.5一次构建到处运行2.docker安装2.1前提条件2.2docker基本构成2.3docker安装步骤*2.4测试镜像3.docker常用命令3.1 启动docker3…...

图像优化篇

目录&#xff08;1&#xff09;矢量图&#xff08;2&#xff09;位图 2.1 分辨率2&#xff0c;图像格式格式选择建议&#xff1a;&#xff08;1&#xff09;矢量图 被定义为一个对象&#xff0c;包括颜色&#xff0c;大小&#xff0c;形状&#xff0c;以及屏幕位置等属性&…...

在surface go 2上安装ubuntu 20.04

在surface go 2上安装ubuntu 20.04 1.制作安装盘 下载ubuntu系统的iso文件 使用Rufus软件将u盘制作为ubuntu系统的安装盘 2.在surface go 2上操作 禁用快速启动 在 Windows 中&#xff0c;禁用“电源选项”中的“快速启动”>选择电源按钮的功能 禁用 Bitlocker 在 Wi…...

Java:SpringMVC的使用(1)

目录第一章、SpringMVC基本了解1.1 概述1.2 SpringMVC处理请求原理简图第二章、SpringMVC搭建框架1、搭建SpringMVC框架1.1 创建工程【web工程】1.2 导入jar包1.3 编写配置文件(1) web.xml注册DispatcherServlet(2) springmvc.xml(3) index.html1.4 编写请求处理器【Controller…...

自动化测试岗位求职简历编写规范+注意事项,让你的简历脱颖而出

目录 前言 1.个人信息 2.教育背景(写最高学历) 3.个人技能(按精通/掌握/熟练/了解层次来写) 4.工作经历 5.工作经验/项目经历 6.自我评价 总结 前言 挑选一个阅读舒适度不错的模板 HR和面试官看的简历多&#xff0c;都是快速阅读&#xff0c;舒适度特别重要&#xff1b…...

C 字符串

在 C 语言中&#xff0c;字符串实际上是使用空字符 \0 结尾的一维字符数组。因此&#xff0c;\0 是用于标记字符串的结束。空字符&#xff08;Null character&#xff09;又称结束符&#xff0c;缩写 NUL&#xff0c;是一个数值为 0 的控制字符&#xff0c;\0 是转义字符&#…...

<6>-MySQL表的增删查改

目录 一&#xff0c;create&#xff08;创建表&#xff09; 二&#xff0c;retrieve&#xff08;查询表&#xff09; 1&#xff0c;select列 2&#xff0c;where条件 三&#xff0c;update&#xff08;更新表&#xff09; 四&#xff0c;delete&#xff08;删除表&#xf…...

逻辑回归:给不确定性划界的分类大师

想象你是一名医生。面对患者的检查报告&#xff08;肿瘤大小、血液指标&#xff09;&#xff0c;你需要做出一个**决定性判断**&#xff1a;恶性还是良性&#xff1f;这种“非黑即白”的抉择&#xff0c;正是**逻辑回归&#xff08;Logistic Regression&#xff09;** 的战场&a…...

线程与协程

1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指&#xff1a;像函数调用/返回一样轻量地完成任务切换。 举例说明&#xff1a; 当你在程序中写一个函数调用&#xff1a; funcA() 然后 funcA 执行完后返回&…...

vue3 字体颜色设置的多种方式

在Vue 3中设置字体颜色可以通过多种方式实现&#xff0c;这取决于你是想在组件内部直接设置&#xff0c;还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法&#xff1a; 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...

MySQL中【正则表达式】用法

MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现&#xff08;两者等价&#xff09;&#xff0c;用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例&#xff1a; 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...

Web 架构之 CDN 加速原理与落地实践

文章目录 一、思维导图二、正文内容&#xff08;一&#xff09;CDN 基础概念1. 定义2. 组成部分 &#xff08;二&#xff09;CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 &#xff08;三&#xff09;CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 &#xf…...

Xen Server服务器释放磁盘空间

disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...

Fabric V2.5 通用溯源系统——增加图片上传与下载功能

fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...

CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)

漏洞概览 漏洞名称&#xff1a;Apache Flink REST API 任意文件读取漏洞CVE编号&#xff1a;CVE-2020-17519CVSS评分&#xff1a;7.5影响版本&#xff1a;Apache Flink 1.11.0、1.11.1、1.11.2修复版本&#xff1a;≥ 1.11.3 或 ≥ 1.12.0漏洞类型&#xff1a;路径遍历&#x…...

从 GreenPlum 到镜舟数据库:杭银消费金融湖仓一体转型实践

作者&#xff1a;吴岐诗&#xff0c;杭银消费金融大数据应用开发工程师 本文整理自杭银消费金融大数据应用开发工程师在StarRocks Summit Asia 2024的分享 引言&#xff1a;融合数据湖与数仓的创新之路 在数字金融时代&#xff0c;数据已成为金融机构的核心竞争力。杭银消费金…...