JVM学习02:内存结构

news2024/11/15 4:03:13

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/341283.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring中IOC框架结构是什么?都包含那些模块,各个模块具体是什么样的

继续整理记录这段时间来的收获&#xff0c;详细代码可在我的Gitee仓库Java设计模式克隆下载学习使用&#xff01; 7.自定义Spring框架 7.1 Spring框架使用回顾 7.1.1 数据访问层 定义UserDaoMapper接口及实现类 public interface UserMapper { public void add(); } pu…

k8s默认StorageClass,解决pvc一直处于“Pending”

文章目录报错详情排查思路查看 pvc 详细属性查看 nfs-provisioner pod日志解决方案报错详情 排查思路 查看 pvc 详细属性 [rootk8s-master01 /opt/zadig]# kubectl describe pvc pvc-sc Name: pvc-sc Namespace: default StorageClass: nfs-yinwu Status: …

代码随想录算法训练营第27天|● 93.复原IP地址 ● 78.子集 ● 90.子集II

93.复原IP地址 看完题后的思路 典型分割问题略lue略剪枝条件 sub&#xff1a; 1&#xff09; 不是一位首字母为0 2&#xff09;大于三位 3&#xff09;介于0-255之间 4) 当已分割得到3个时&#xff0c;第四个直接从startIndex到末尾就行 代码 ArrayList<String> slist…

剑指Offer 第28天 复杂链表的赋值

复杂链表的复制_牛客题霸_牛客网 描述 输入一个复杂链表&#xff08;每个节点中有节点值&#xff0c;以及两个指针&#xff0c;一个指向下一个节点&#xff0c;另一个特殊指针random指向一个随机节点&#xff09;&#xff0c;请对此链表进行深拷贝&#xff0c;并返回拷贝后的头…

(免费分享)基于 SpringBoot 的高校宿舍管理系统带论文

项目描述 系统代码质量高&#xff0c;功能强大&#xff0c;带论文。 系统的功能主要有&#xff1a; &#xff08;1&#xff09;基本信息管理 基本信息分为学生信息和宿舍信息两部分&#xff0c;其功能是负责维护这些信息&#xff0c;对 它们进行增删查改等操作。 &#x…

UART通讯简介

UART全称Universal AsynchronousReceiver/Transmitter&#xff0c;通用异步收发传输器。 一、工作原理 和其它串口一样&#xff0c;数据按照二进制从低位到高位一位一位的传输&#xff0c;能将要传输的数据在串行通信与并行通信之间加以转换&#xff0c;能够灵活地与外部设备进…

网络编程(未完待续)

网络编程 文章目录网络编程前置概念1- 字节序高低地址与高低字节高低地址&#xff1a;高低字节字节序大端小端例子代码判断当前机器是大端还是小端为何要有字节序字节序转换函数需要字节序转换的时机例子一例子二2- IP地址转换函数早期(不用管)举例现在与字节序转换函数相比:**…

Open Street Map—2023年水系数据

之前文章我们给大家分享了从OSM地图下载的道路数据&#xff08;可查看之前推送的文章&#xff09;&#xff0c; 这一篇我们给大家带来的是从OSM地图下载的水系数据&#xff01;我们下载了全国范围&#xff08;包括港澳台&#xff09;的水系数据&#xff0c;下载时间为2023年2月…

硬件篇-配置

写在最前 这已经可以成为垃圾佬配置了。。。 机箱->239元 机箱选用的itx迷你机箱&#xff0c;为了后期nas方便拓展选了4盘位&#xff0c;该机箱还是比较符合我的预期的&#xff0c;颇有种麻雀虽小五脏俱全的感觉&#xff0c;机箱可以安装matx主板和itx主板&#xff0c;还是…

聊聊MySQL中的事务,MVCC

事务我们知道&#xff0c;事务具有四大特性——ACIDA atomicity 指的是原子性C consistency 指的是一致性I isolation 指的是隔离性D durability 指的是持久性四大特性的实现原理原子性原子性在这指的是整个事务操作&#xff0c;要么同时成功要么同时失败。让它变成一个整体。同…

若依管理系统搭建运行环境--基于SpringBootVue前端分离版

若依框架搭建运行环境-如何下载一、技术支持&#xff1a;二、Redis安装及运行三 目录结构四、导入数据库五 修改配置文件1.application-druid.yml文件 &#xff08;路径&#xff1b;RuoYi-Vue-master\ruoyi-admin\src\main\resources\application-druid.yml&#xff09;2.appli…

越界访问数组

越界访问是指访问&#xff08;操作修改&#xff09;了不属于自己的空间 我们以如下代码为例&#xff1a;此代码在vs中进行 #include <stdio.h> int main() {int i 0;int arr[] {1,2,3,4,5,6,7,8,9,10};for(i0; i<12; i){arr[i] 0;printf("hello\n");}r…

阿里云平台与MQTTX软件通信

阿里云平台与MQTTX软件通信 上一篇文章介绍了如何创建阿里云物联网平台以及MQTT.fx软件通信配置&#xff1a;https://blog.csdn.net/weixin_46251230/article/details/128993864 但MQTT.fx软件需要许可证才能使用&#xff0c;所以使用另一款软件MQTTX来代替 MQTTX软件下载 官…

【C++】类与对象(二)

前言 在前一章时我们已经介绍了类与对象的基本知识&#xff0c;包括类的概念与定义&#xff0c;以及类的访问限定符&#xff0c;类的实例化&#xff0c;类的大小的计算&#xff0c;以及C语言必须传递的this指针&#xff08;C中不需要我们传递&#xff0c;编译器自动帮我们实现&…

CSP-《I‘m stuck!》-感悟

题目 做题过程 注&#xff1a;黄色高亮表示需要注意的地方&#xff0c;蓝色粗体表示代码思路 好久没有写过代码了&#xff0c;今天做这道编程题&#xff0c;简直是灾难现场。 上午编程完后发现样例没有通过&#xff0c;检查发现算法思路出现了问题&#xff1a;我计数了S不能到…

【每日一题Day116】LC1138字母板上的路径 | 模拟

字母板上的路径【LC1138】 我们从一块字母板上的位置 (0, 0) 出发&#xff0c;该坐标对应的字符为 board[0][0]。 在本题里&#xff0c;字母板为board ["abcde", "fghij", "klmno", "pqrst", "uvwxy", "z"]&…

BFS的使用(acwing提高课之搜索)

bfsBFS1. 多源bfs2.最小步数模型1.魔板2.八数码问题3.双端队列广搜4.双向广搜5.A*算法BFS bfs是搜索算法里面最基础的算法&#xff0c;对于队首的点&#xff0c;每次搜索其周围所有的点&#xff0c;然后将其入队。队列里面的点具有两个特性&#xff1a; &#xff08;1&#xf…

OpenWrt路由器设置IPv6域名动态解析,同时实现IPv4设备访问IPv6节点

文章目录0、前言1、准备工作2、详细步骤2.1、OpenWrt路由器软件包安装2.2、防火墙放行入站数据&#xff08;修改为“接受”并保存应用&#xff09;2.3、路由器做好ipv6设置&#xff08;略&#xff09;2.4、域名解析服务商对域名的解析设置2.5、路由器中动态域名插件的设置3、关…

23.2.12 LC每日一题 —— 极尽地高效利用题目中所提供的有效信息

文章目录23.2.12 LC每日一题 —— 极尽地高效利用题目中所提供的有效信息题目链接&#xff1a;题目大意&#xff1a;注意&#xff1a;示例&#xff1a;参考代码&#xff08;py3&#xff09;&#xff1a;总结23.2.12 LC每日一题 —— 极尽地高效利用题目中所提供的有效信息 题目…

Redis内存存储效率问题

目录 内存碎片是如何形成的&#xff1f; 如何判断是否有内存碎片&#xff1f; 如何清理内存碎片&#xff1f; INFO命令 实习期间&#xff0c;了解到&#xff0c;企业级开发中多个项目使用Redis&#xff0c;运行Redis实例的有可能是同一台物理机器&#xff0c;那么&#xff…