JVM_01 内存结构(深入理解+图解)

本文笔记参考创智播客JVM课程 和《深入理解Java虚拟机第三版》

一、JVM 入门介绍

1.1 JVM 定义

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

java程序从源代码到运行一般有下面3步:
在这里插入图片描述

JVM 好处:

  • 一次编译,到处运行(JVM屏蔽了字节码跟底层操作系统的差异,对外提供一致的运行环境。达到平台无关性)
  • 自动内存管理,垃圾回收功能
  • 数组下标越界越界检查(C++没有)
  • 多态

1.2 比较 JVM JRE JDK

在这里插入图片描述
JVM 是运行 Java 字节码的虚拟机。JVM屏蔽了字节码跟底层操作系统的差异,对外提供一致的运行环境。达到平台无关性

JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。

JDK 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。
在这里插入图片描述
本笔记使用的是HotSpot版本

二、内存结构

整体架构图

学习顺序如下图:由简到难
在这里插入图片描述

1、程序计数器(寄存器)

Program Counter Register

在这里插入图片描述

1.1 作用与特点

  • 作用:是记住下一条jvm指令的执行地址。
  • 特点:
    是线程私有的 (每条线程都有自己的程序计数器)
    不会存在内存溢出 (唯一一个在<<Java虚拟机规范>>中不会存在内存溢出区域)

在这里插入图片描述

1.2 JVM指令执行流程:

  • 每一条二进制字节码(JVM指令)通过解释器翻译成机器码就可以交给CPU执行了。
  • 当一条指令执行完后,解释器会到程序计数器取得下一条jvm指令执行地址。
  • 程序计数器物理层面寄存器实现
  • 因此程序计数器的作用:用于是保存下一条jvm指令的执行地址!

2、Java虚拟机栈

Java Virtual Machine Stack

2.1 定义

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

在这里插入图片描述

在这里插入图片描述

2.1.1 演示代码
/**
 * 演示栈帧
 */
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;
    }
}

Debug看下执行过程
在这里插入图片描述
此时栈顶部的栈帧为活动栈帧,当方法2执行结束时,它占用的内存随着栈帧出栈被释放掉。
在这里插入图片描述
继续执行main方法结束。栈为空。程序结束
在这里插入图片描述

2.1.2 面试问题
  • 1.垃圾回收是否涉及栈内存

    • 不需要:因为栈内存是一次次方法调用产生的栈帧内存,而栈帧内存在每次方法调用结束后会弹出栈,自动回收内存。垃圾回收只是回收堆内存中的无用对象。
  • 2.栈内存分配越大越好吗
    在这里插入图片描述

    • 不是。因为物理内存的大小是一定的,栈内存越大,可以支持更多的递归调用,反而会让可执行的线程数变少。
    • 举例:如果物理内存是500M(假设),如果一个线程所能分配的栈内存为1M的话,那么理论上可以有500个线程。而如果一个线程分配栈内存占2M的话,那么最多只能有250个线程同时执行!
    • 所以一般采用系统默认的栈内存大小
  • 3.方法内的局部变量是否线程安全?
    我们通过一段代码来解释

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

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

假如有2个线程都来调用这个方法,会不会造成x值的混乱呢?

  • 不会 因为x变量为方法内的局部变量,一个线程对应一个栈,线程内方法调用都会产生新的栈帧。我们结合一个图来看
  • 在这里插入图片描述
    如果变量x改为static int x。那么结果就不一样了。
    在这里插入图片描述
    • 总结:**变量如果是共享的就需要考虑线程安全,如果是每个线程私有的就不用考虑线程安全。

接下来看一个案例

package cn.itcast.jvm.t1.stack;

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

    public static void m1() {
        // sb 作为方法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 作为方法m2()外部的传递来的参数,sb 不在方法m2()的作用范围内
        // 不是线程私有的 多个线程共享了同一个对象---> 非线程安全
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }

    public static StringBuilder m3() {
        // sb 作为方法m3()内部的局部变量,是线程私有的
        StringBuilder sb = new StringBuilder();// sb 为引用类型的变量
        sb.append(1);
        sb.append(2);
        sb.append(3);
        return sb;// 然而方法m3()将sb返回,sb逃离了方法m3()的作用范围,且sb是引用类型的变量
        // 其他线程也可以拿到该变量的 ---> 非线程安全
        // 如果sb是非引用类型,即基本类型(int/char/float...)变量的话,逃离m3()作用范围后,则不会存在线程安全
    }
}

该问题答案:

  • 如果方法内局部变量没有逃离方法的作用范围,它是线程安全
  • 如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题

2.2 栈内存溢出

什么情况下会导致栈内存溢出?

  • 分两种情况
    • 栈帧过多导致栈内存溢出
    • 栈帧过大导致栈内存溢出
2.2.1 栈帧过多导致栈内存溢出

抛出java.lang.StackOverflowError栈内存溢出异常

在这里插入图片描述

  • 栈帧过多导致栈内存溢出情况很多见,列如没有设置好结束条件,无限递归
2.2.2 栈帧过大导致栈内存溢出

在这里插入图片描述

  • 栈帧所占用内存过大超过了栈内存,这种情况少见。
    举2个案例
    案例一
    方法递归调用导致栈内存溢出
    在这里插入图片描述
    最终结果调用了23624次,即有23624个栈帧产生。

我们通过修改栈内存大小
在这里插入图片描述
修改的运行结果为4150次,远小于原来的23624次!!
在这里插入图片描述
案例二

/**
 * 两个类之间的循环引用问题,导致的栈溢出
 * 
 * 解决方案:打断循环,即在员工emp 中忽略其dept属性,放置递归互相调用
 */
public class Demo04 {

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

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

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

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

        // 输出结果:{"name":"Market","emps":[{"name":"csp"},{"name":"hzw"}]}
        ObjectMapper mapper = new ObjectMapper();// 要导入jackson包
        System.out.println(mapper.writeValueAsString(d));
    }
}

/**
 * 员工
 */
class Emp {
    private String name;
    @JsonIgnore// 忽略该属性:为啥呢?我们来分析一下!
    /**
     * 如果我们不忽略掉员工对象中的部门属性
     * System.out.println(mapper.writeValueAsString(d));
     * 会出现下面的结果:
     * {
     *  "name":"Market","emps":
     *  [c
     *      {"name":"csp",dept:{name:'xxx',emps:'...'}},
     *      ...
     *  ]
     * }
     * 也就是说,输出结果中,部门对象dept的json串中包含员工对象emp,
     * 而员工对象emp 中又包含dept,这样互相包含就无线递归下去,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;
    }
}

2.3 线程运行诊断

2.3.1 案例1:CPU占用过多
  • Linux环境下运行某些程序时,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程
    • top命令可以监测哪个进程对CUP占用过高
      在这里插入图片描述

在这里插入图片描述

  • ps H -eo pid, tid(线程id), %cpu | grep 进程号 刚才通过top查到的进程号 通过ps命令进一步定位是哪个线程引起的cpu占用过高!
    在这里插入图片描述
  • JDK提供的工具jstack查看哪个线程出现问题
    • jstack 进程id:可以查看这个进程中所有的线程nid(为16进制),所以tid =32665(tid为线程id)要转换为16进制。其16进制为7f99在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
2.3.2 案例2:程序运行很长时间没有结果
  • 先运行程序
    在这里插入图片描述
    通过jstack 进程id 查看线程问题
    在这里插入图片描述
    通过jstack定位了代码问题出现的代码问题
    在这里插入图片描述
    这样我们通过了jstack排查了一个死锁问题。

3、本地方法栈

在这里插入图片描述
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是:

  • 虚拟机栈为虚拟机执行java方法(字节码)服务
  • 本地方法栈为虚拟机使用到的本地(Native)方法服务
  • 他们都是线程私有的

本地方法运行时所使用的内存即为本地方法栈
这样的本地方法有很多比如java.lang.Object其中就有许多native方法,比如clone()
在这里插入图片描述
一些带有native 关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法!
所以本地方法栈作用就是为本地方法运行时提供内存空间。

前面讲的程序计数器虚拟机栈本地方法栈都是线程私有的。

接下来讲的方法区线程共享的区域。

4、堆

4.1 定义

Heap 堆

  • 通过 new 关键字,创建对象都会使用堆内存特点
  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制(回收无用对象)

4.2 堆内存溢出

当在使用的对象过多就会导致堆内存溢出现象

4.2.1 案例:演示堆内存溢出

一般报java.lang.OutOfMemoryError异常即为内存溢出

报java.lang.OutOfMemoryError: Java heap space

在这里插入图片描述
无限循环执行,导致堆内存溢出。有时我们可以通过修改堆内存大小参数值-Xmx(大小),尽早暴露异常问题。
在这里插入图片描述

4.3 堆内存诊断

  • 命令行方式
    1. jps 工具
    查看当前系统中有哪些 java 进程
    2. jmap 工具
    查看堆内存占用情况 jmap - heap 进程id (只能查询某一时刻

  • 图形界面的方式
    3. jconsole 工具
    图形界面的,多功能的监测工具,可以连续监测

4.3.1 诊断工具使用案例
/**
 * 演示堆内存
 */
public class Demo1_4 {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000);//等待30秒 用jps查看java进程 jmap 工具查看堆内存占用情况
        byte[] array = new byte[1024 * 1024 * 10]; // 10 MB  我们new 10MB内存对象
        System.out.println("2...");
        Thread.sleep(20000);//等待20秒 用jmap查看堆内存占用情况
        array = null;//将引用对象设为null,不再引用byte数组对象,可以被垃圾回收
        System.gc();//进行垃圾回收
        System.out.println("3...");//查看此时堆内存的情况
        Thread.sleep(1000000L);
    }
}
4.3.1.1. jmp工具使用

1.当new byte[]前jps查看进程id,jmap -heap 进程id 查看堆内存占用情况
在这里插入图片描述
通过jmap工具我们可以看到一些堆内存的配置信息
在这里插入图片描述
3个阶段堆内存占用过程图:
在这里插入图片描述

4.3.1.2. 图形界面 jconsole工具使用

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

打印1时,未创建byte数组前,堆内存占用约20MB
在这里插入图片描述
打印2时,byte[]数组已创建,此时堆内存占用约为30MB
在这里插入图片描述

打印3时,gc垃圾回收,堆内存降到了10MB以下。
在这里插入图片描述
以上就是jconsole工具的使用

4.3.2 案例(垃圾回收后,内存占用仍然很高)

**案例描述:**比如一个程序在多次垃圾回收后,它的内存占用仍然很高!
我先运行一个程序,假如我们并不知道这个程序是如何写的。(模拟真实开发场景)
1.现在我们通过一个诊断工具jps查看程序进程id
在这里插入图片描述
2.得到进程id为18732,再通过jmap工具查看该进程堆内存占用情况。
在这里插入图片描述
在这里插入图片描述

说明总共的内存占用大概为200MB左右
3.现在我们执行一次垃圾回收看看效果
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
发现内存占用下降了很多,但是仍然有220MB多
,我们再次jmap一下在这里插入图片描述
在这里插入图片描述
发现并没有什么很大变化。新生代内存减少了3MB左右。

4.3.2.1 jvisualvm工具

一种可视化展现虚拟机情况的工具。
在这里插入图片描述
连接该进程
在这里插入图片描述
在这里插入图片描述
该工具也具备jconsole功能,检测内存使用情况。
其中重点介绍堆Dump:可以截取当前时刻堆内存占用实际情况的快照。(有哪些类型的对象,每个对象的个数
在这里插入图片描述
其中有个查找功能,可以查找指定前多少个最大内存的对象信息
在这里插入图片描述
在这里插入图片描述
发现当前堆内存占用最大的对象为ArrayLIst对象。我们点进去查看详情。
在这里插入图片描述
发现ArrayList中存了200个Student对象。
在这里插入图片描述

而每个Student对象内存了一个byte[]数组,大小约为1MB。
所以得出来是ArrayList对象导致了程序占用高,而且无法回收,现在分析源代码,看看是否是该对象导致的。

/**
 * 演示查看对象个数 堆转储 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++) {//存入了200个Student对象
            students.add(new Student());
//            Student student = new Student();
        }
        Thread.sleep(1000000000L);
    }
}
class Student {
    private byte[] big = new byte[1024*1024];//大小为1MB
}

显然是ArrayList对象导致的问题。

4.3.3 总结:

实际生产环境下,问题排查也是类似的,也是通过堆转储 dump将此刻堆内存占用情况抓取下来,就可以发现究竟是哪些对象占用过大,然后去分析排查源代码问题所在。

5.方法区

5.1 定义

根据jdk1.8中对方法区的定义描述:

  • 方法区:和Java堆一样,是线程共享的区域。
  • 它存储类的结构相关的信息,例如运行时常量池字段方法数据,以及方法构造函数的代码,包括和实例初始化以及接口初始化中使用的特殊方法
  • 方法区是在虚拟机启动时创建的
  • 尽管方法区逻辑上的一部分,但简单的实现可能选择不垃圾收集或压缩它。此《Java虚拟机规范》不强制指定方法区域的位置或用于管理已编译代码的策略。(JDK1.8以前,HotSpot使用永久代来实现方法区,但是其他虚拟机实现不存在“永久代”的概念。直到JDK1.8抛弃了“永久代”的概念,改用在本地内存中实现的元空间(Meta-space)来代替)
  • 方法区可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区不需要连续的内存
  • 如果方法区中的内存无法满足分配请求,Java虚拟机将抛出OutOfMemoryError(内存溢出)。

5.2 组成

HotSpot在1.6和1.8的组成
在这里插入图片描述
JDK1.6HotSpot永久代实现方法区,此时由JVM管理。到了JDK1.8后,抛弃了永久代,用本地内存实现的**元空间(Meta space)**代替。此时不再由jvm去管理它的内存结构,而是有操作系统管理。

5.3演示方法区内存溢出

5.3.1JDK1.6由于产生了多个类导致元空间内存溢出

在这里插入图片描述

5.3.2JDK1.8下由于产生了多个类导致元空间内存溢出

在这里插入图片描述

5.3.3 总结
  • 1.8以前会导致永久代内存溢出
  • 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
  • -XX:MaxPermSize=8m
  • 1.8之后会导致元空间内存溢出
  • 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
  • -XX:MaxMetaspaceSize=8m

实际开发中导致内存溢出的场景
这些框架都用了一些字节码技术

  • spring :SpringAop会使用cglib生产被代理对象的子类作为代理
  • mybatis:用cglib产生mapper接口的实现类对象

5.4 运行时常量池

二进制字节码(类的基本信息,常量池,类方法定义包括了虚拟机指令)
下面我们通过一段程序了解这些信息。
在这里插入图片描述
我们反编译这个程序,查看二进制字节码的详细信息
在这里插入图片描述
类的基本信息
在这里插入图片描述
常量池
在这里插入图片描述
类方法定义
在这里插入图片描述
虚拟机指令
在这里插入图片描述

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

5.5 StringTable

运行时常量池中比较重要的组成部分:字符串常量池(StringTable/String Pool)
其结构就是哈希表(hashtable),不能扩容。
String有两种赋值方式

  • 第一种是通过“字面量”赋值。

String str = “Hello”;

  • 第二种是通过new关键字创建新对象。

String str = new String(“Hello”);

我们来分析一段代码

//StringTable [ "a"  , "b" , "ab" ]  hashtable 结构 不能扩容
public static void main(String[] args) {
	// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象 放入StringTable中
    // ldc #3 会把 b 符号变为 "b" 字符串对象 放入StringTable中
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象 放入StringTable中
	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 == s4);//false 因为s4实际上是new String("ab")
        System.out.println(s3 == s5);//true
}

底层原理图
在这里插入图片描述
总结

  • 字面量创建字符串会先在字符串池中找,看是否有相等的对象,没有的话就在中创建,把地址驻留在字符串池;有的话则直接用池中的引用,避免重复创建对象。
  • new关键字创建时,前面的操作和字面量创建一样,只不过最后在运行时会创建一个新对象,变量所引用的都是这个新对象的地址。

5.6 StringTable特性

  • 常量池中的字符串仅是符号第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
 		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 s2返回的是串池中的"ab"
        System.out.println( s == x );//fasle s地址为堆中地址
5.6.1 intern()

字符常量池有关的intern()方法。
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池

  • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回。
  • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回,复制的对象与原对象地址不一样。
5.6.2 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 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")在堆中创建对象
         
        String x1 = "cd";
        x2.intern();//串池已经有“cd”加入失败

        System.out.println(x1 == x2);//false
        // 问,如果调换了【最后两行代码】的位置呢
        	//x2.intern();//串池没有"cd"加入x2的对象的引用
         	//String x1 = "cd";
        	// System.out.println(x1 == x2);//true
        //如果是1.6呢?
        	//x2.intern();//串池没有"cd"加入x2的对象的拷贝(与x2对象不同)
         	//String x1 = "cd";
        	// System.out.println(x1 == x2);//false
    }
}

5.7 StringTable位置

字符串常量池的位置跟JDK版本有关。
在这里插入图片描述

  • jdk1.6 StringTable是常量池的一部分。存储在永久代当中。
  • jdk1.7 StringTable转移到了Java堆中
  • jdk 1.8 元空间代替了永久代
演示jdk1.6-1.8的StringTable内存溢出
1.jdk1.6环境下运行

将-XX:MaxPermSize=10m,永久代内存设置小一点

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

运行结果:java.lang.OutOfMemoryError:PermGen space****永久代内存不足导致内存溢出
在这里插入图片描述

2.jdk1.8环境下运行

设置 -Xmx10m -XX:-UseGCOverheadLimit
在这里插入图片描述
结果:java.lang.OutOfMemoryError:Java heap space堆空间不足导致内存溢出

结论

从这两个案例也证明了字符串常量池1.8用的是堆空间,1.6用的是永久代。

5.8 StringTable垃圾回收

我们可能认为StringTable存的都是字符串常量,所有不存在垃圾回收。但事实是存在垃圾回收的。

5.8.1 案例

演示 StringTable 垃圾回收
设置参数
-Xmx10m(设置虚拟机堆内存最大值)
-XX:+PrintStringTableStatistics (打印字符串表的统计信息,看到串池中字符串情况)
-XX:+PrintGCDetails -verbose:gc(打印垃圾回收的详细信息)
在这里插入图片描述
当try中没有任何处理是。

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

    }
}

运行可以看到
StringTable的底层是哈希表。数组+链表。数组就是桶buckets。可以看出串池中有1741个字符串
在这里插入图片描述

当在try catch中加入一下代码,运行对比前后串池中字符串常量的的变化

for (int j = 0; j < 100; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }

在这里插入图片描述
此时串池中加了100左右的字符串对象,此时并没有垃圾回收
当加入10000个对象时。

for (int j = 0; j < 10000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }

在这里插入图片描述
结果串池中只有4810个字符串对象。说明GC垃圾回收了。
在这里插入图片描述
由于内存分配失败,触发了垃圾回收。
结论:StringTable会发生垃圾回收

5.9 StringTable性能调优

StringTable底层:

哈希表,哈希表的性能跟它的大小有关,如果哈希表的桶的个数比较多,那么元素相对分散,那么哈希碰撞的几率会减少,查找速度会变快。反之则反。

下面通过案例进行StringTable调优,通过调整桶的个数

5.9.1 调优案例
  • 1.调整-XX:StringTableSIze=桶个数

我们读取一个文件,该文件中有48万个单词,然后加入串池中。
在这里插入图片描述
设置参数StringTableSize=200000
在这里插入图片描述

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


    }

由于没有设置堆内存大小,所以内存充裕,还是很轻松的存下。
在这里插入图片描述
可以看到分配的桶有20万个,平均1个桶放2个字符串对象。速度还是很快的,只花了0.4秒。
在这里插入图片描述
那如果不设置StringTableSize大小。默认6万个
在这里插入图片描述
运行时间约0.6秒,相对多了一点
在这里插入图片描述
那我们把桶大小调成200,StringTableSize=1009(允许的最小值)
在这里插入图片描述
这次运行花费了很久,12秒
在这里插入图片描述
结论:当你系统里字符串常量多的话,那么把StringTableSize设置的大一点。这样有很好的哈希分布,减少哈希冲突,让StringTable的效率得到明显的提升。

这里我们提出一个问题:为什么我们要用StringTable呢?

  • 2.考虑将字符串对象是否入池

这里通过一个案例来看入池与不入池的对比,修改了上一个案例。循环10次读取文件,放入List集合中,防止垃圾回收。

 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);
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();


    }

我们用VisualVm工具看下堆内存占用实际情况
在这里插入图片描述
此时我们开始读取文件
在这里插入图片描述
此时VisualVM的情况,char[],string内存增加了很多,占用300MB左右
在这里插入图片描述
我们修改一段代码,在存入List集合之前将读取的字符串入池。那么返回的就是串池中的对象加入到List集合中,串池外的就被回收掉了

address.add(line.intern());

此时char[]和String加起来大约40MB,而我们之前没有入池操作的时候是300MB。差距还是很明显的。
在这里插入图片描述
结论:如果你的程序中有大量的字符串(存在重复字符串),可以将它入池,来减少对象个数,节约堆内存的占用。

6.直接内存

直接内存操作系统的内存

直接内存并不属于Java虚拟机的内存管理,而是系统内存

6.1定义

Direct Memory

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

6.2 案例

演示直接内存的使用以及性能上是否比传统IO操作高很多。分别用NIO和IO读写文件,对比效率.

这里简单介绍下IO和NIO操作.

在标准的IO当中,都是基于字节流/字符流进行操作的,而在NIO中则是是基于Channel和Buffer进行操作

在这里插入图片描述
Channel用于在字节缓冲区和位于通道另一侧的实体(通常是文件或者套接字)之间以便有效的进行数据传输。借助通道,可以用最小的总开销来访问操作系统本身的I/O服务。

通道必须结合Buffer使用,不能直接向通道中读/写数据,其结构如下图:
在这里插入图片描述

6.2.1 简述FileChannel的使用

1.创建FileChannel

InputStream,OutputStream,RandomAccessFile可通过getChannel()方法获取FileChannel实例

2.从FileChannel读取数据

通过调用Channel的read()方法可将Channel中的数据读取到Buffer中

3.FileChannel写入数据

通过Channel的write()方法可想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
        directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
    }

    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用了3秒,NIO用了不到1秒。所有说NIO读写效率非常高。
在这里插入图片描述

6.3 直接内存读写大文件效率高的原因

在这里插入图片描述
过程分析
用户态(java)切换到内核态(System)
Java语言本身并不具备读写能力,需要调用操作系统的函数(本地方法native Method)即用户态(java)切换到内核态(System)。
内核态中首先用CPU函数去读取磁盘文件的内容,会在操作系统内存中划出一个系统缓冲区,系统缓存区java是不能运行的。所有会在Java堆内存中分配java缓冲区byte[]。java从系统缓冲区读取数据到java缓冲区中,然后切换到用户态调用读写操作。最终进行文件的读写。
**问题:**系统内存有缓冲区,java堆也有缓冲区。那么读写必然涉及到数据得存两份。这样就造成了不必要的复制,效率因此低。
使用Direct Memory直接内存后
在这里插入图片描述

ByteBuffer.allocateDirect(_1Mb),分配一块直接内存,在操作系统划出一块缓冲区,与上面不同的是,这块缓冲区,java和系统可以直接访问。一块共享的缓冲区。磁盘文件读取到直接内存中,少了java缓冲区的复制操作。所有速度得到了很大提升。

     ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);//分配一块直接内存

因此:直接内存常见于NIO操作,充当数据缓冲区。读写性能高

6.4 直接内存_内存溢出

直接内存不受JVM内存回收管理,那么会不会出现内存溢出情况呢?

6.4.1 案例:演示直接内存溢出
/**
 * 演示直接内存溢出
 */
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);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
        // 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
        //                  jdk8 对方法区的实现称为元空间
    }
}

运行结果:

java.lang.OutOfMemoryError: Direct buffer memory

因此直接内存不足也会导致的内存溢出
在这里插入图片描述

6.5 直接内存_释放原理

我们通过一个案例演示直接内存释放原理
我们分配1GB直接内存

ByteBuffer.allocateDirect(_1Gb);

/**
 * 禁用显式回收对直接内存的影响
 */
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();
    }
}

我们不能用java的内存监测工具去监测直接内存的情况,我们可以有任务管理器看java进程对内存的占用情况。
运行前,idea程序占用1G左右内存
在这里插入图片描述
运行程序,idea程序增加了1G内存
在这里插入图片描述
我们回车进行释放
在这里插入图片描述
发现idea又变回了原来的1G。直接内存被释放掉了。直接内存不是不归JVM内存管理吗,怎么被垃圾回收了呢?

6.5.1 释放原理

ByteBuffer底层分配和释放内存相关的类型。
类型:Unsafe

/**
 * 直接内存分配的底层原理: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();
    }
//通过反射获取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);
        }
    }
}

运行程序。idea程序增加了1G内存
在这里插入图片描述
回车释放内存,idea占用明显变小了。
在这里插入图片描述
验证了:

  • 直接内存分配释放是通过Unsafe对象进行管理的,而不是垃圾回收。
  • 直接内存的释放垃圾回收不同,垃圾回收自动释放无用对象,而直接内存需要主动调用unsafe.freeMemory(base),进行内存的释放
6.5.2 ByteBuffer源码分析

在ByteBuffer的allocateDirect()方法的底层的DirectByteBuffer()构造器中调用了unsafe对象分配空间

在这里插入图片描述
直接内存的释放借助了java虚引用的机制
在这里插入图片描述

6.5.3 总结
  • 使用了 Unsafe 对象完成直接内存分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleanerclean 方法调用 freeMemory释放直接内存
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值