JVM篇 笔记1 :内存结构


视频简介

来自:https://www.bilibili.com/video/BV1yE411Z7AP?from=search&seid=10706557432304126843&spm_id_from=333.337.0.0


JVM篇 笔记2:垃圾回收

https://blog.csdn.net/qq_46539281/article/details/120174805


JVM篇 笔记3:类加载与字节码技术

https://blog.csdn.net/qq_46539281/article/details/120174860

JVM篇 笔记4:内存模型

https://blog.csdn.net/qq_46539281/article/details/120174885


什么是JVM?

定义:

Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境)

好处:

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检擦
  • 多态

比较:

JVM JRE JDK的区别
在这里插入图片描述


学习JVM有什么用?

用处:

  • 面试
  • 理解底层的实现原理
  • 中高级程序员的必备技能

常见的JVM

JVM是一个规范

在这里插入图片描述

主要学习的是这个HotSpot 这个,这个也是最著名的。


学习路线

在这里插入图片描述

.class文件 先被 类加载器 加载进入JVM内存结构进行执行。

  • 类放在方法区 Method Area
  • 类创建的实例对象放在 堆Heap
  • 堆里面的对象在调用方法时,会用到 虚拟机栈程序计数器本地方法栈
  • 方法执行时,每行代码 由 执行引擎中的 解释器 进行逐行 执行
  • 方法里面的热点代码(被频繁调用的代码),会使用 即时编译器 进行优化。
  • 垃圾回收 会对 里面 一些不再引用的对象,进行垃圾回收。
  • java代码不太容易实现的功能,需要调用底层系统的功能,需要借助 本地方法接口

一. 内存结构

1. 程序计数器

在这里插入图片描述

1.1 定义

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

作用:是记住下一条jvm指令的执行地址

特点

  • 是线程私有的
  • 不会存在内存溢出(规定的)

作用解释:

解释器:将拿到的二进制字节码 转化为 机器码
CPU : 执行机器码
程序计数器:记住下一条JVM 指令的地址

程序计数器(概念)在物理上实现:寄存器

在这里插入图片描述
图中二进制字节码前的数字就i是 地址

执行流程:将地址为0的指令,发送给解释器,同时将将下一条指令 的地址4 ,给程序计数器 ,解释器 将 字节码 转化为 机器码后,发送给cpu.执行完后, 解释器 从 程序计数器 中拿取执行 指令 的地址 , 根据地址拿取指令, 将下一条指令的地址,存放到 程序计数器中。
反复执行,直到结束。


特点解释

是线程私有的 (一个线程对应一个程序计数器

在这里插入图片描述

根据多线程的案例,当有多个线程在执行的时候,CPU随机执行其中一个A线程,A线程还未执行完毕。然后执行其他的B线程,当重新执行线程A时,会继续接着上一次未执行的代码的地方进行执行。

那么,计算机就是通过线程中程序计数器,来记录 还未执行的代码的地址。


2. 虚拟机栈

在这里插入图片描述

2.1 定义

Java Virtual Machine Stacks (Java 虚拟机栈)

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

在这里插入图片描述
先进后出,后进先出

  • 栈-- 线程运行需要的内存空间
  • 里面的每一个元素叫: 栈帧
  • 一个栈帧对应一次方法的调用。
  • 栈帧:每个方法运行时,需要的内存

在这里插入图片描述

案例:

/**
 * 演示栈帧
 */
public class Demo1_1 {
    public static void main(String[] args) throws InterruptedException {
        method1();
    }

    private static void method1() {
        method2(1, 2);
    }

    private static int method2(int a, int b) {
        int c =  a + b;
        return c;
    }
}

在这里插入图片描述

右边的参数:就是栈帧中需要的变量。同时需要内存。

问题解析:

  1. 垃圾回收是否涉及栈内存? 不
栈帧内存,当对应的方法执行完毕后,就释放该栈帧内存, gc主要是回收的堆 里面对应不用对象的内存
  1. 栈内存分配越大越好吗?
栈内存分配的越大,反而会将线程数变少,一个计算机的物理内存是一定的,一个栈对应一个线程。
 栈内存分配的越大,不会增加运行的效率的。
  1. 方法内的局部变量是否线程安全?
1.  如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
2. 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

代码解释1:

/**
 * 局部变量的线程安全问题
 */
public class Demo1_18 {

    // 多个线程同时执行此方法
    static void m1() {
        int x = 0;
        for (int i = 0; i < 5000; i++) {
            x++;
        }
        System.out.println(x);
    }
}

在这里插入图片描述
不会出现线程问题。

在这里插入图片描述
会出现安全问题的。


代码解释2:

/**
 * 局部变量的线程安全问题
 */
public class Demo1_17 {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append(4);
        sb.append(5);
        sb.append(6);
        new Thread(()->{
            m2(sb);
        }).start();
    }

    public static void m1() {		//单独看这个方法,不会出现线程安全问题。
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }

    public static void m2(StringBuilder sb) {	//不是安全的,不是该方法私有
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }

    public static StringBuilder m3() {		//也不是线程安全的,因为 返回了 这个对象。
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        return sb;
    }
}


2.2 栈内存溢出

栈内存溢出的原因:

  • 栈帧过多导致栈内存溢出
  • 栈帧过大导致栈内存溢出

栈帧过多导致栈内存溢出的情况:(递归调用会出现,自己调用自己)
在这里插入图片描述
方法一 调用方法二 , 方法二调用方法三,递归的形式调用下去,直到栈内存不够。


栈帧过大导致栈内存溢出的情况:(不太容易出现)
在这里插入图片描述
栈帧里面,一般存放的是变量,等,,所以不太容易出现这个问题。


案例1:栈帧过多导致栈内存溢出

/**
 * 演示栈内存溢出 java.lang.StackOverflowError
 * -Xss256k
 */
public class Demo1_2 {
    private static int count;

    public static void main(String[] args) {
        try {
            method1();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }

    private static void method1() {
        count++;
        method1();
    }
}

案例2:自己没那么傻,写个死递归。出现栈内存溢出,还有第三方库导致的。

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Arrays;
import java.util.List;

/**
 * json 数据转换
 */
public class Demo1_19 {

    public static void main(String[] args) throws JsonProcessingException {
        Dept d = new Dept();
        d.setName("Market");

        Emp e1 = new Emp();
        e1.setName("zhang");
        e1.setDept(d);

        Emp e2 = new Emp();
        e2.setName("li");
        e2.setDept(d);

        d.setEmps(Arrays.asList(e1, e2));

        // { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
        ObjectMapper mapper = new ObjectMapper();
        System.out.println(mapper.writeValueAsString(d));
    }
}

class Emp {
    private String name;
    @JsonIgnore   添加这个注解,是解决该问题的方法。
    private Dept dept;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Dept getDept() {
        return dept;
    }

    public void setDept(Dept dept) {
        this.dept = dept;
    }
}
class Dept {
    private String name;
    private List<Emp> emps;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Emp> getEmps() {
        return emps;
    }

    public void setEmps(List<Emp> emps) {
        this.emps = emps;
    }
}


2.3 线程运行诊断

案例1:CPU占用过多(某个Java代码出问题了)

定位:

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack 进程id
    • 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
/**
 * 演示 cpu 占用过高
 */
public class Demo1_16 {

    public static void main(String[] args) {
        new Thread(null, () -> {
            System.out.println("1...");
            while(true) {

            }
        }, "thread1").start();


        new Thread(null, () -> {
            System.out.println("2...");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread2").start();

        new Thread(null, () -> {
            System.out.println("3...");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread3").start();
    }
}

案例2:程序运行很长时间没有结果

原因:可能是线程死锁导致的。

/**
 * 演示线程死锁
 */
class A{};
class B{};
public class Demo1_3 {
    static A a = new A();
    static B b = new B();


    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (a) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (b) {
                synchronized (a) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
    }

}


3. 本地方法栈

在这里插入图片描述

java虚拟机调用本地方法时,需要给本地方法提供内存空间。本地方法运行的时候,使用的内存就是本地方法栈


4. 堆

以上三个,虚拟机栈程序计数器本地方法栈 都是属于线程私有的。
下面两个,堆,方法区是属于,所有线程共有的

在这里插入图片描述

4.1 定义

Heap 堆

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

特点

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

4.2 堆内存溢出

出现的原因:一直创建对象,且一直有线程使用,那么,垃圾回收机制就回收不了。最后,导致 堆内存溢出


代码案例:

import java.util.ArrayList;
import java.util.List;

/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */
public class Demo1_5 {

    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

在这里插入图片描述

4.3 堆内存诊断

cmd中使用:

  1. jps 工具
    查看当前系统中有哪些 java 进程
  2. jmap 工具
    查看堆内存占用情况 jmap - heap 进程id
  3. jconsole 工具
    图形界面的,多功能的监测工具,可以连续监测

案例1:
/**
 * 演示堆内存
 */
public class Demo1_4 {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000);
        byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
        System.out.println("2...");
        Thread.sleep(30000);
        array = null;
        System.gc();
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}

在这里插入图片描述
打印 出 1 的时候 ,堆的使用内存情况:
在这里插入图片描述
打印 出 2 的时候 ,堆的使用内存情况:
在这里插入图片描述
打印 出 3 的时候 ,堆的使用内存情况:
在这里插入图片描述


还是该案例:cmd使用jconsole命令 会出现以下界面
在这里插入图片描述

案例2:垃圾回收后,内存占用仍然很高

使用工具进行排查 jvisualvm , 图形界面工具

import java.util.ArrayList;
import java.util.List;

/**
 * 演示查看对象个数 堆转储 dump
 */
public class Demo1_13 {

    public static void main(String[] args) throws InterruptedException {
        List<Student> students = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            students.add(new Student());
//            Student student = new Student();
        }
        Thread.sleep(1000000000L);
    }
}
class Student {
    private byte[] big = new byte[1024*1024];
}


5. 方法区

在这里插入图片描述

5.1 定义

方法区是所有JVM线程共享的区域。存储了跟类的结构有关的信息。有成员变量,方法数据,成员方法和构造方法的代码部分。

方法区是在JVM启动时被创建。方法区逻辑上是堆,但是规定生产时,不强制一定要使用堆。

在这里插入图片描述

5.3 方法区内存溢出

  • 1.8 以前(1.6)会导致永久代内存溢出
  • 1.8 之后会导致元空间内存溢出,元空间使用的是系统内存。

演示元空间内存溢出
为了方便演示:设置JVM参数:-XX:MaxMetaspaceSize=8m,因为元空间使用的系统的内存,足够大,所以需要设置参数。

import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}

在这里插入图片描述

演示永久代内存溢出 1.6

使用1.6的jdk,进行执行,需要设置jvm参数 -XX:MaxPermSize=8m

import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxPermSize=8m
 */
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}

在这里插入图片描述

常用场景:

  • spring:通过反射,进行加载class对象。
  • Mybatis

5.4 运行时常量池

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

在这里插入图片描述
上图中的参数是 -v 不是 - c

反编译
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
常量池就是一张表,存储了类型,值等信息。


5.5 StringTable (串值)

先看几道面试题:

String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);

为了更好的理解上述题,看下面的案例

5.5.1 案例1
// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容

public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
        String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab

        System.out.println(s3 == s5);

    }
}


5.5.2 案例2:延迟实例化
/**
 * 演示字符串字面量也是【延迟】成为对象的
 */
public class TestString {
    public static void main(String[] args) {
        int x = args.length;
        System.out.println(); // 字符串个数 2275

        System.out.print("1");
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print("1"); // 字符串个数 2285
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print(x); // 字符串个数
    }
}

这里引用了常量池中的字面值,就会放入到串池中去。根据key找value, StringTable就是一个hash表,没有则添加,有则直接使用,不会存储。


5.5.3 StringTable 特性
  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串
      池中的对象返回

    • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,
      放入串池, 会把串池中的对象返回


jdk = 1.8

public class Demo1_23 {

    //  ["ab", "a", "b"]
    public static void main(String[] args) {

        String x = "ab";
        String s = new String("a") + new String("b");   // 堆  new String("a")   new String("b") new String("ab")

        String s2 = s.intern(); 
        // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

        System.out.println( s2 == x);	// true
        System.out.println( s == x );	//  false
    }

}


jdk = 1.6

public class Demo1_23 {

    //  ["ab", "a", "b"]
    public static void main(String[] args) {

        
        String s = new String("a") + new String("b");   // 堆  new String("a")   new String("b") new String("ab")

        String s2 = s.intern(); 
        // 将这个字符串对象复制一份放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

		String x = "ab";

        System.out.println( s2 == x);	// true
        System.out.println( s == x );	// false
    }

}

这个代码放在1.8 环境下,就是 true , true 了。


5.5.4 面试题解答
/**
 * 演示字符串相关面试题
 */
public class Demo1_21 {

    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b"; // ab
        String s4 = s1 + s2;   // new String("ab")
        String s5 = "ab";
        String s6 = s4.intern();

// 问
        System.out.println(s3 == s4); // false
        System.out.println(s3 == s5); // true
        System.out.println(s3 == s6); // true

        String x2 = new String("c") + new String("d"); // new String("cd")
        x2.intern();
        String x1 = "cd";

// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
        System.out.println(x1 == x2);
    }
}


5.6 StringTable 位置

在这里插入图片描述

以下案例在进行推断出 StringTable 所在的位置(1.6 / 1.8版本中)


import java.util.ArrayList;
import java.util.List;

/**
 * 演示 StringTable 位置
 * 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
 * 在jdk6下设置 -XX:MaxPermSize=10m
 */
public class Demo1_6 {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
            for (int j = 0; j < 260000; j++) {
                list.add(String.valueOf(j).intern());
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

在这里插入图片描述
在这里插入图片描述

5.7 StringTable 垃圾回收

串池中没有被引用的字符常量,也会被gc回收。


-Xmx10m: 设置JVM中堆内存的大小。
-XX:+PrintStringTableStatistics 打印字符串常量,在串池的个数,等信息。
-XX:+PrintGCDetails -verbose:gc 垃圾回收机制的信息。

import java.util.ArrayList;
import java.util.List;

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

在这里插入图片描述

import java.util.ArrayList;
import java.util.List;

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

在这里插入图片描述
内存不足,导致启动了垃圾回收机制。回收了不用的字符串常量
在这里插入图片描述


5.8 StringTable 性能调优

  • 调整 -XX:StringTableSize=桶个数

这个桶的个数,就是hash中表中的数组的个数,根据数据结构中来说,需要插入相同的数据来说,数组的个数越多,查询的速度越快。反之,越慢。

每当应用一个字符串字面值时,先回从 串池(StringTable)中 进行查询,如果有,就返回该值的地址。没有就插入其中。


所以 性能调优就是根据这个桶的数量来的。


linux.words 文件中,有48000多个单词。

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 演示串池大小对性能的影响
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 */
public class Demo1_24 {

    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null) {
                    break;
                }
                line.intern();	//    放入串池中去
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
        }
    }
}

-XX:StringTableSize=1009 值 越大,执行的越快。

性能问题


6. 直接内存

不属于Java虚拟机的内存,属于系统的内存。

6.1 定义

Direct Memory

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

读写性能高,

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 演示 ByteBuffer 作用
 */
public class Demo1_9 {
    static final String FROM = 
    "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
    
    static final String TO = "E:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io();					 // io 用时:1535.586957 1766.963399 1359.240226    15s多
        directBuffer();			 // directBuffer 用时:479.295165 702.291454 562.56592    4s多
    }

    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
            while (true) {
                int len = from.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
    }

    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) / 1000_000.0);
    }
}

在这里插入图片描述

为什么 directBuffer 读写能力 这么高 ?? 下面是原理!!!

普通的java 的 io操作
在这里插入图片描述
使用直接内存的操作

在这里插入图片描述

6.2 直接内存溢出案例


import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;


/**
 * 演示直接内存溢出
 */
public class Demo1_10 {
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);	
				//  这个方法生成直接内存 ,但是byteBuffer 对象是存放在堆中的。
               
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
        // 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
        //                  jdk8 对方法区的实现称为元空间
    }
}

在这里插入图片描述

6.3 直接内存释放原理


直接调用gc() 方法, 直接内存会被释放掉的。不是gc直接回收的,而是间接释放的。

import java.io.IOException;
import java.nio.ByteBuffer;

public class Demo1_26 {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC ,  会影响性能的。
        System.in.read();
    }
}

ByteBuffer的原理

import sun.misc.Unsafe;

import java.io.IOException;
import java.lang.reflect.Field;

/**
 * 直接内存分配的底层原理:Unsafe对象
 */
public class Demo1_27 {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }

    public static Unsafe getUnsafe() {		//  通过反射获得 Unsafe 对象
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

直接内存分配的底层原理:Unsafe对象
long base = unsafe.allocateMemory(_1Gb); 分配直接内存的空间,返回地址
unsafe.freeMemory(base); 根据地址释放直接内存。


6.3 分配和回收原理

1. 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法

2. ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦
	ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调
	用 freeMemory 来释放直接内存

6.4 禁用显式回收对直接内存的影响


-XX:+DisableExplicitGC 禁止显式使用gc方法
import java.io.IOException;
import java.nio.ByteBuffer;

/**
 * 禁用显式回收对直接内存的影响
 */
public class Demo1_26 {
    static int _1Gb = 1024 * 1024 * 1024;

    /*
     * -XX:+DisableExplicitGC 显式的
     */
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC,就没有用了。因为调用该方法会影响性能。
        System.in.read();
    }
}

需要释放直接空间的话,可以直接使用gc 。即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值