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;  // hellohellohellohello
                i++;
            }
        } 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 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 对象控制的。

原理分析:

  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 GC
        System.in.read();
    }
}

结果分析

配置参数可以禁用显式的垃圾回收System.gc(),此时ByteBuffer无法被回收进而导致直接内存无法释放。此时可以通过直接使用unsafe.freeMemory()进行主动释放内存。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值