JVM_1_内存管理

程序计数器

image-20210828161306005

定义

Program Counter Register 程序计数器(寄存器)

image-20210828163248313

  • 作用:记住下一条指令的执行地址
  • 特点
    • 是线程私有的
    • 不会存在内存溢出

计算机在运行程序时,对于多线程来说,会给每一个内存分配一个时间片。当时间片结束,切换线程时,每个线程都会有其私有的程序计数器来保存它当前的状态。当该线程再次抢到时间片时,便可以通过程序计数器接着继续运行。

作用

image-20210828162256972

程序计数器作用:记住下一条jvm指令的执行地址

  • 在JAVA程序运行时,java源代码会被编译成二进制字节码,作为操纵jvm的jvm指令,然后交给解释器,解释器将其解释为机器码,交给cpu去运行。与此同时,程序计数器会记住下一条jvm指令的执行地址,然后交给解释器继续运行。
  • 这些jvm指令是通用的,用来直接操纵虚拟机的。所以无论是在window、linux还是macos,只要有虚拟机,java代码都能在其上面运行。这也是java程序跨平台的原因。
  • 程序计数器是jvm中的一个角色。程序计数器是通过计算机中的寄存器来实现的,寄存器访问速度快,存取速度快。

虚拟机栈

image-20210828163636801

image-20210828164111685

栈:线程运行需要的内存空间

栈帧:每个方法运行时需要的内存

一个栈由多个栈帧组成,每个栈帧都为所调用的方法分配了内存空间,比如方法的参数,局部变量,返回地址等。

在线程运行时,不同的方法在执行过程中,被依次压入栈中。待执行结束后,弹出栈,释放内存。

定义

Java Virtual Machine Stacks(Java虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应则当前正在执行的那个方法。

image-20210828165515023

问题辨析

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

    不涉及。栈内存是方法调用是分配的,在方法结束调用后,就将栈帧弹出栈了,释放了内存。

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

    并不是。系统的物理内存是一定的,栈空间越大,会导致线程数越少。栈空间越大,也并不会让程序更快,只是有更大的栈空间,能让你做更多次的递归调用。

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

    • 判断是否安全,即看这些变量对于多个线程是共享的还是私有的。
    • 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
    • 如果是局部变量引用了对象,并逃离方法的作用方法,需要考虑线程安全

案例分析1

image-20210828170638501

image-20210828170729020

对于局部变量int x 来说,它是方法内的局部变量,在多线程下,每个线程会为其运行的方法分配栈和栈帧,栈是线程私有的,方法中的局部变量不会受其他线程影响。

image-20210828170618242

每个线程读取static变量到线程的工作内存中,然后再进行计算,最后将修改以后的变量的值同步到static变量中

案例分析2

image-20210828171600734

  • m1方法线程安全:局部变量在方法内,线程私有。
  • m2方法线程不安全:变量是通过应用类型拿到的,那么与此同时其他线程也可能拿到这个应用然后对其进行修改。
  • m3方法线程不安全:局部变量虽然声明在方法内,但是在最后确返回出去了。返回出去后可能被其他线程引用进行修改。

栈内存溢出

栈空间调整参数

-Xss空间大小
-Xss8M
  1. 栈帧过多导致栈内存溢出
  2. 栈帧过大导致栈内存溢出
image-20210828172259517

当程序递归调用次数太多时,会超出栈的空间,导致栈内存溢出。

image-20210828172432094

方法携带的参数等占用内存太多,导致栈帧过大,使栈内存溢出。

演示1(栈帧过多)

image-20210828172803288

递归调用次数太多。

演示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

image-20210828175513175

# 使用ps查看线程的运行情况
# -eo 后的参数是想要查看的参数信息,pid进程号,tid线程号,%cpu cpu占用率
ps H -eo pid,tid,%cpu | grep 32655

image-20210828180026634

32665线程有大问题。

# 输出进程内的所有信息,线程号用16进制表示的
# 32665线程换算成6进制为7f99
jstack 32655

image-20210828180606154

演示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();
    }

}

本地方法栈

image-20210829093158776

image-20210829093335182

native 本地方法

本地方法栈,给本地方法的运行提供一个空间。jvm类并不是所有的方法都是java代码编写的,有些底层的方法就是通过c/c++实现的。而java可以调用这些底层方法来完成一些功能。在java调用这些底层方法时,就是运行在本地方法栈中。

image-20210829093645420

定义

Heap堆

  • 通过new关键字,创建对象都会使用堆内存

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

堆内存溢出

OutOfMemoryError:java heap space 堆内存溢出

堆空间调整参数

-Xmx空间大小
-Xmx4G

案例

image-20210829100332978

案例代码

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);
        }
    }
}

在实际生产中,对于堆内存溢出问题,可能宾士那么容易检测出来。因为堆内存空间比较大,在运行时,一时间还不会使其溢出。

所以为了使堆内存问题尽早暴露出来,可以在测试时,将堆内存空间调整小一些。

堆内存诊断

  1. jps工具
    • 查看当前系统中有哪些java进程
  2. jmap工具
    • 查看某一时刻堆内存占用情况
    • jhsdb jmap --heap --pid 进程id
  3. jconsole工具
    • 图形界面的,多功能的监测工具,可以连续监测
  4. 堆内存调整指令参数
    • -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 指令查看内存情况

程序运行后

image-20210829161824726

97751为该进程的pid,调用命令

jhsdb jmap --heap --pid 97751 
image-20210829162305968

具体的堆内存占用在Heap Usage

image-20210829162710121

在程序打印了 2… 后,再次

jhsdb jmap --heap --pid 97751 
image-20210829163002318

按理说应该增加10M,此处有些疑惑

在打印了 3… 之后,代表着已经被垃圾回收了

jhsdb jmap --heap --pid 97751 
image-20210829163513488

jconsole诊断堆内存

image-20210829171115106

但是在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];
}

image-20210829173250363

经过测试,在执行了垃圾回收后,堆内存占用还是居高不下。

于是点击 堆dump 拿取快照,分析详情

image-20210829173611313

点击查看

image-20210829173857763

由源代码可知,确实是Student类的原因。

student数组一直在循环引用,没有被垃圾回收。

方法区

image-20210829174141846

定义

image-20210829174456310

方法区是一种规范,永久代元空间都只是它的实现。

组成

image-20210829174916457

方法区内存溢出

元空间大小调整参数

-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

image-20210829175944594

运行结果 OutOfMemoryError: Metaspace

image-20210829175453427

场景

  • spring
  • mybatis

都用到了cglib技术,字节码的动态生成技术,动态加载类,动态生成类,运行期间,经常会产生大量的类,可能会产生方法区溢出。

运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

案例

// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

通过反编译这段程序来学习相关的知识

用到的命令

  1. 先进入到java源文件的目录
cd 目标目录
  1. 将HelloWorld.java 编译成 HelloWorld.class
javac HelloWorld.java
  1. 反编译HelloWorld.class
javap -v HelloWorld.class

结果如下

类基本信息

image-20210829200548475

常量池

image-20210829200834772

类方法定义

image-20210829201138637

方法运行流程

  • 对于主方法来说,解释器依次执行指令。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";
 
}

运行时常量池

image-20210907173027797

方法区

image-20210907172922091

局部变量表

image-20210907173105090

  1. 常量池最初存在于字节码中,运行时会被加载到运行时常量池中。这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象

  2. 字符串的加载是懒惰的,当执行到具体的引用时,才会创建对象。如String s1 = “a”。创建了s1对象。

  3. 同时会创建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);


    }
}

image-20210907171257030

image-20210907171658785

可以看到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);
    }
}

image-20210907172354228

字符串延迟加载

/**
 * 演示字符串字面量也是【延迟】成为对象的
 */
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
    }
}

image-20210907174352586

image-20210907174544915

image-20210907174717219

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
    }

}
  1. 执行到第10行String s2 = s.intern(); 时,将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回。由于String x = “ab”;StringTable中有"ab"了,所以s不会放在常量池。但是会吧常量池的"ab"返回回来给s2。
  2. 所以 s2 == x 即 常量池的"ab"
  3. 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
    }

}
  1. 执行到第10行String s2 = s.intern(); 时,将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回。此时StringTable还没有"ab",所以s会放在常量池。同时常量池的"ab"返回回来给s2。
  2. 所以 s2 == x 即 常量池的"ab"
  3. 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位置

image-20210908163006245

在1.6之前。StringTable在方法区永久代中,但是放在这之中,虚拟机只会在full GC时才会对StringTable进行垃圾回收。

但是其实StringTable的操作是非常频繁的,如果没有即使进行垃圾回收,容易造成永久代空间不足。

在1.8后,StringTable放在了堆中,使其垃圾回收的效率更高。

案例

1.6下

虚拟机参数

-XX:MaxPermSize=10m  //永久代空间设置

image-20210908163504238

image-20210908163536082

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);
        }
    }
}

image-20210908164506315

可以看到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);
        }
    }
}

image-20210908171104909

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);
        }
    }
}

image-20210908171200024

for (int j = 0; j < 100000; j++) {
    String.valueOf(j).intern();
    i++;
}

image-20210908171340239

image-20210908171418752

清理了一些未引用的字符串常量。

StringTable性能调优

  • 调整 -XX:StringTableSize=桶个数
  • 考虑是否将字符串对象放入池中,即用intern

参数调优

StringTable的数据结构实现是哈希表,调优即对哈希表的桶的个数的调整。

jvm参数

-XX:StringTableSize=1009

image-20210908172615756

由于入池时候会先去查找StringTable中有无这个字符串,hash表的的寻找,桶个数越多越快。

image-20210908172757892

image-20210908172852198

当桶个数为1009时,耗费12097毫秒

image-20210908173023784

intern调优

image-20210908174101833

直接内存

定义

不是虚拟机的内存,是系统内存。Direct Memory

  • 常见于NIO操作时,用于数据缓存区
  • 分配回收成本过高,但读写性能高
  • 不受JVM内存回收管理

基本使用

java操作磁盘文件

image-20210908192753325

当java读取磁盘文件时,会从用户态切换到内核态,才能去操作系统内存。读取时,系统内存先开辟一块缓存空间,磁盘文件分块读取。然后java虚拟机内存再开辟缓存空间new Byte[]来读取系统内存的文件。由于有从系统内存读取到java虚拟机的内存,所以效率较低。

NIO操作磁盘文件

image-20210908192818850

读取磁盘文件时,会有一块直接内存,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);
        }
    }

}

image-20210911094441455

分配和释放原理

  • 使用了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源码

image-20210911095759844

ByteBuffer分配直接内存是通过new 了一个DirectByteBuffer

image-20210911095914572

在DirectByteBuffer内部可以看到,确实是用Unsafe类去分配内存。

image-20210911100146768

DirectByteBuffer的run方法释放了直接内存。

image-20210911100658821

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值