【黑马JVM(1)】虚拟机栈,堆,常量池的认识及内存溢出排查

JVM/JRE/JDK

  • JVM: Java Virtual Machine的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算
    机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
    JVM 保正确执行 .class 文
    件,实现在 Linux、Windows、MacOS 等平台上运行。
  • JRE Java Runtime Environment的缩写,JVM 标准加上实现的一大堆基础类库就组成了Java的运行环境
    提供运行Java应用程序所必须的软件环境等。

JDK Java Development Kit的缩写,提供了一些非常好用的小工具,比如 javac、java、jar 等。
提供Java开发工具包

在这里插入图片描述
JVM好处:

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

示例

简单看下Java程序的执行过程:
在这里插入图片描述

在这里插入图片描述

运行下面代码:遵循的就是 Java 语言规范。其中,调用了System.out 等模块,也就是 JRE 里提供的类库。

public class HelloTest {
	public static void main(String[] args) {
		System.out.println("Hello World");
		}
}

使用 JDK 的工具 javac 进行编译后,会产生 HelloWorld 的字节码。javap查看下字节码内容:
在这里插入图片描述

控制端输入:javap -v HelloTest

javap 将class反编译
v显示基本信息

查看main方法内:

0 getstatic #2 <java/lang/System.out> // getstatic 获取静态字段的值
3 ldc #3 <Hello World> // ldc 常量池中的常量值入栈
5 invokevirtual #4 <java/io/PrintStream.println> // invokevirtual 运行时方法绑定调用方法
8 return //void 函数返回

Java 虚拟机采用基于栈的架构,其指令由操作码和操作数组成。这些字节码指令,就叫作 opcode。其中,getstatic、ldc、invokevirtual、return 等,就是 opcode。
JVM 就是靠解析 opcode 和操作数来完成程序的执行的。当使用 Java 命令运行 .class 文件的时候,实际上就相当于启动了一个 JVM 进程。
然后 JVM 会翻译这些字节码,它有两种执行方式。

  • 常见的就是解释执行,将 opcode + 操作数翻译成机器代码;
  • 另外一种执行方式就是 JIT,即时编译,会在一定条件下将字节码编译成机器码之后再执行。

JVM内存管理

JVM整体架构

JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
在这里插入图片描述

程序计数器

Program Counter Register 程序计数器记住下一条jvm指令要执行的地址。(Java对硬件的抽象物理上通过寄存器实现(读取速度快))

当前指令所在的内存地址+当前指令所占用的字节数=下一条指令所在的内存地址

虚拟机指令的执行流程: 拿到一条指令,通过解释器翻译成机器码,才能交给CPU运行,程序计数器就是记住下一条jvm指令的执行地址。
在这里插入图片描述
在这里插入图片描述
观察这张图,不难发现每个指令前都有一个数字,这是指令的偏移地址,简称为行号,程序计数器的作用就是存储下一条指令的行号。
  以上图为例,当第一条指令执行时,JVM会将下一条指令的行号“3”放入程序计数器中。当第一条指令执行完时,解释器会根据程序计数器中的行号拿到下一条指令,继续运行。

为什么要多此一举先把下一条指令的行号放入程序计数器中,直接取指令然后执行不是更简单吗?
  JVM是支持多个线程同时运行的,这就涉及到CPU的调度问题了。线程甲正执行的好好的, 大哥CPU告诉甲说,你累了,我陪会儿乙,甲只好乖乖休息。一段时间后,大哥回来了,这时甲就可以根据程序计数器中的行号取到下一条指令接着执行。
  这里有一点要注意,因为程序计数器记录的是行号,是会重复的,所以多个线程不能同时用一个,不然就乱了。所以程序计数器是线程私有的。
  
特点:

  • 是线程私有的(每个线程执行的指令地址不一样)
  • 不会存在内存溢,程序计数器中的行号永远只会有一个,当前指令执行时,会拿下一条指令的行号替换当前的行号。因此就不存在内存溢出问题。

查看二进制字节码:

1.安装插件:
在这里插入图片描述
2.查看

在这里插入图片描述

虚拟机栈

Java Virtual Machine Stacks 虚拟机栈,线程运行时所需要的内存空间。
一个线程对应一个虚拟机栈,多个线程对应多个虚拟机栈。
一个虚拟机栈,由多个栈帧(Frame)组成。一个栈帧对应着一次方法调用,方法调用时所需要的内存为栈帧,存储参数,局部变量,返回地址等。
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

垃圾回收是否涉及栈内存?
不涉及,栈内存是一次次方法调用时所产生的栈帧内存,栈帧内存在每次方法调用结束后会弹出栈,自动回收掉。
垃圾回收只会回收掉对内存中的内存占用,栈内存不需要。

栈内存分配越大越好吗?

-Xss 设置栈帧大小

操作系统默认栈大小:
在这里插入图片描述
每个线程在创建的时候都会创建一个虚拟机栈,而物理内存是固定的,栈内存划分的越大, 可分配的线程数就越少。

方法内的局部遍历是否线程安全?
如果方法内部局部变量没有逃离方法的作用范围它就是安全的,是线程私有的 (栈帧独有),不会产生在多个线程下产生线程干扰。
​ 如果局部变量引用了对象,并逃离方法的作用范围,能被其他线程访问到,就不是线程安全的。

在这里插入图片描述

栈内存溢出

1.栈帧过多导致栈内存溢出(递归调用)

2.栈帧过大导致栈内存溢出

在这里插入图片描述

栈帧设置为256k,只循环3237次
在这里插入图片描述

线程诊断 top/ps -H/jstack

案例一:CPU占用过多

nohup java cc.java &

nohup 是 no hang up 的缩写,意思是不挂断运行,一直运行下去
& 是在后台运行的意思

top 命令检测进程对CPU的使用,定位哪个进程对cpu占用过高

在这里插入图片描述

ps H -eo pid,tid,%cpu

ps 查看线程情况
H 打印进程数
eo 输出要查看的内容 pid(进程ID) tid (线程id) %cpu
在这里插入图片描述

ps H -eo pid,tid,%cpu | grep 32655 定位哪个线程对CPU占位过高

在这里插入图片描述

jstack 32655(进程id) 会输出相关堆栈信息

jstack输出的线程信息是16进制的,我们将tid 32665 用计算器转为10进制可以看到是 7f99
线程的状态是runnable,显示第8行有问题,可以定位到Java源代码
在这里插入图片描述

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

运行一段程序,想输出一个结果,但是一直不输出,可以查看相关堆栈信息

jstack pid

jstack最后一段输出中,发现Found one Java-Level deadlock,找到死锁,定位到Java源码的Thread-1的29行和Thread-0的21行。
在这里插入图片描述

本地方法栈

Native Method Stacks:java虚拟机调用本地方法时,所提供的内存空间。
本地方法定义:不是由java代码写的方法,由C或C++所写(因为c/c++方便和操作系统打交道,Java间接的通过调用本地方法来进行工作)。

本地方法例如:Object类,native修饰符等,Java的基础类库或执行引擎里都会调用本地方法。

Heap:通过 new 关键字创建对象都会使用堆内存。

特点

  • 是线程共享的,所以堆中的对象都需要考虑线程安全的问题
    虚拟机栈中的局部变量都是线程私有的,只要它的局部变量不逃逸出方法的作用范围,就是线程安全的,但是堆中的对象,要考虑线程安全,因为它们是线程共享的。当然也有例外,后面讲解。
  • 堆里面有一个垃圾回收机制
    堆中不再被引用的对象,就会当成垃圾进行回收,以释放空闲的内存,不至于内存被创建的对象给撑爆。

堆内存溢出

当一个对象不被使用时,就可以成为所谓垃圾被回收掉,也就是它占用的内存会被释放掉。怎么还可能出现堆内存耗尽呢?

可以被回收的一个对象的条件是没人在使用它,但是如果不断的产生对象,而产生的这些新对象,仍然被使用,就意味着这些对象不能作为垃圾,这样的对象达到一定的数量,就会导致堆内存被耗尽,即堆内存溢出。

代码示例:

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

初始的时候,a变量引用"hello",第一次变量,会把hello放入List集合中,接下来做一个字符串的拼接,变成了两个hello,组成了一个新的字符串,再循环的时候,会把两个hello的字符串对象。再追加到list集合,再做一次的拼接。以此类推,不断地把拼接的Hello对象加入list集合中,list的作用范围是从声明开始,一直都是有效范围,所以不能被垃圾回收掉,随着这个字符串就越来越多,它就会把堆空间占满。
在这里插入图片描述

Xmx 设置堆内存大小

控制堆空间的一个最大值,改小之后只循环17次。
在这里插入图片描述
在这里插入图片描述

堆内存诊断

案例一:jps/jmap/jconsole工具使用

代码示例:

public class Demo1_4 {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000); //停留时间敲命令
        byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
        System.out.println("2...");
        Thread.sleep(20000);
        array = null; //变量数组不再引用
        System.gc();//进行垃圾回收
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}

jps 查看当前系统中有哪些java进程,并展示进程号

在这里插入图片描述

jmap 查看进程在堆内存占用情况,只能查询某个时刻

在这里插入图片描述
新创建的对象,都会使用eden Space区,used代表使用内存,capacity代表总容量
执行到1,查看堆内存使用情况:
在这里插入图片描述
执行到2,查看堆内存使用情况:增加了10M,就是创建的byte数组
在这里插入图片描述

执行到3,查看堆内存使用情况:包含一些不用的信息都回收掉了
在这里插入图片描述

jconsole工具

图形界面,多功能检测工具,可以连续监测,还可以查看内存占用,线程情况,类加载数量等信息。

启动代码,控制台输入jconsole
在这里插入图片描述

选择进程,直接连接
在这里插入图片描述
在这里插入图片描述

可以看到堆内存的使用情况:
在这里插入图片描述
在这里插入图片描述

还可以检测线程死锁
在这里插入图片描述

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

程序运行起来之后,并不知道程序代码怎么编写的,可以借助一些工具来查看一下。

jps 查看进程id

在这里插入图片描述

jmap - heap 13556 查看堆内存使用情况

在这里插入图片描述

jconsole 执行jc,但是堆内存还是200MB空间

是不是有一些由于我们的一些编程失误,导致了很一些对象始终被引用而无法释放他们的内存呢
在这里插入图片描述

使用 jvisualvm 工具

在这里插入图片描述

与jconsole类似,也可以监测一个内存的占用,执行垃圾回收,检测线程,查看堆各个组成成分。
在这里插入图片描述
堆dump工具,抓取堆的当前快照,可以进一步对里面的一些详细内容进行分析。这是jmap,jconsole 工具所不具备的。
在这里插入图片描述

查找保留前20个内存最大的对象,点进去查询细节:
在这里插入图片描述
可以看到student对象中big属性,是一个byte数组占用空间大约是一兆左右。每个student 对象占用一兆的内存,两百个对象占内存两百多兆。
排查出问题:student对象以及list导致内存占用比较高。而且这个对象是长时间使用的,导致垃圾回收没办法回收他们的内存。
在这里插入图片描述
查看Java源码:
student对象的属性的大小是1兆。循环了两百次,都加到了一个list 中,而这个list 由于一直在main方法里,而main方法一直没结束,所以都是在它的生存范围内,所以一直没能被回收,就导致内存占用居高不下。

/**
 * 演示查看对象个数 堆转储 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];
}

在实际的生产环境下分析的手段和排查的方式也是类似。通过堆转储功能(堆dump)把内存快照抓下来。再分析对内存占用最大的那些对象,就可以得出一些有用的结论,再去分析和排查你的原始代码就可以了。

方法区

jvm中对方法区的定义:Chapter 2. The Structure of the Java Virtual Machine

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

方法区是JVM所有线程共享的区域,它存储了与类结构相关的信息:运行时常量池,成员变量,方法数据,成员方法、构造方法的代码。它的大小就决定了系统可以保存多少种类。
  方法区在虚拟机启动时创建,它在逻辑上是堆的组成部分(具体实现上是否为堆的一部分视厂商而定,如永久代,元空间都是其实现)。JVM规范并不强制方法区的位置。

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类会出现OOM错误。

在JDK 7以前,习惯上把方法区称为永久代(习惯上),而到了JDK8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替。这两个最大的区别就是:元空间不在虚拟机设置的内存中,而是使用本地内存。

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

方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
在这里插入图片描述

方法区内存溢出

代码示例:

/**
 * 演示元空间内存溢出 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);
        }
    }
}

运行之后并没有出现溢出,因为Java8之后使用元空间,使用的是系统内存,所以设置元空间初始大小为1.8
在这里插入图片描述

-XX:MaxMetaspaceSize=8m 设置原空间初始大小 1.8

在这里插入图片描述
1.8环境,Metaspace代表元空间导致的OOM

-XX:MaxMetaspaceSize=8m 设置元空间初始大小

在这里插入图片描述
1.6环境,PermGen代表是永久代导致的OOM。

XX:MaxPermSize=8m 设置永久代初始大小

在这里插入图片描述

常量池

JVM指令参考:https://blog.csdn.net/A598853607/article/details/125026953

通过一段代码认识常量池:

示例代码:

public class HelloWorld {
    public HelloWorld() {
    }

    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

hello world要运行,需要先编译成二进制字节码。
字节码由三部分组成:类的基本信息,类的常量池,类中的一些方法定义(包含虚拟机指令)。

javap -v helloWord 查看二进制反编译后内容

PS D:\java\idea\IdeaProject2\jvm\out\production\jvm\cn\itcast\jvm\t5> javap -v HelloWorld
警告: 二进制文件HelloWorld包含cn.itcast.jvm.t5.HelloWorld
Classfile /D:/java/idea/IdeaProject2/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
  Last modified 2023-2-15; size 567 bytes
  MD5 checksum 8efebdac91aa496515fa1c161184e354
  Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // cn/itcast/jvm/t5/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcn/itcast/jvm/t5/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               cn/itcast/jvm/t5/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public cn.itcast.jvm.t5.HelloWorld();
    descriptor: ()V
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t5/HelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

类的基本信息:
在这里插入图片描述
常量池:
在这里插入图片描述
方法定义:
在这里插入图片描述
查看虚拟机指令:虚拟机指令后的注释是java程序帮忙加上的。解释器在翻译虚拟机指令的时候,只看到没加注释的这几行指令。
在这里插入图片描述
在这里插入图片描述
常量池就是一张常量表,jvm虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量(字符串和基本数据类型)等信息。
运行时常量池就是存在 *.class 文件中的,当该类被加载到虚拟机以后,它的常量池中的信息会被加载到运行时常量池中(在内存中的位置)。并把符号地址变为内存中的真实地址。

StringTable

在运行时常量池中,有一块逻辑区域是StringTable(串池),用于存放字面量,串池实际上是一个hash表,不能扩容。当jvm指令在执行时,当执行到字面量信息时,会去串池中查找是否有该字面量,如果没有该字面量将其放入到串池中。

示例一:字面量创建字符串

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
    }
}

在这里插入图片描述

常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
当运行到此方法,执行到对应的代码时(jvm执行 ldc #2) 才会把 a 符号变为 “a” 字符串对象,并将对象放入串池(StringTable[]) 中,StringTable是一个哈希表,长度固定,“a”就是哈希表的key。一开始的时候,会根据“a”到串池中找其对象,一开始是没有的,所以就会创建一个并放入串池中。串池为 [“a”]。执行到指令ldc #3时,会和上面一样,生成一个“b”对象并放入串池中,串池变为[“a”, “b”]。同样地,后面会生成“ab”对象并放入串池中。串池变为[“a”, “b”, “ab”]。

小结一下:字面量创建字符串对象是懒惰的,即只有执行到相应代码才会创建相应对象(和一般的类不同)并放入串池中。如果串池中已经有了,就直接使用串池中的对象(让引用变量指向已有的对象)。串池中的对象只会存在一份,也就是只会有一个“a”对象。

普通的java对象在类加载的时候就会生成并放入堆中,而这种方式生成的String不同,只有当执行到新建String的代码时才会生成字符串对象。

astore_0 将引用类型或returnAddress类型值存入局部变量0

示例二:字符串变量拼接

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1+s2;
		System.out.println(s3 == s4);  // false
    }
}

通过虚拟机指令明白:
行号为9的指令是个new,就说明s4的创建方式和s1、s2、s3不同,它是在堆里新建了一个对象,前面根据字面量创建的则是在串池中生成了字符串对象。通过后面的注释明白new了一个StringBuilder对象。接着看17,21,可以发现“s1 + s2”的方式是通过StringBuilder对象调用append方法实现的。最后看24,调用了toString方法生成了新的字符串对象。

所以:s1+s2的执行流程:new StringBuilder().append(“a”).append(“b”).toString()
在这里插入图片描述
StringBuilder.toString(),创建一个新的String对象,存入ab
在这里插入图片描述
说明:当两个字符串变量拼接时,jvm会创建一个StringBuilder对象,利用其append方法实现变量的拼接。最后再通过其toString方法生成一个新的String对象。

最后输出结果,发现s3不等于s4,这说明s3指向串池中的“ab”对象,s4指向堆中的“ab”对象。这是两个不同的对象。

示例三:字符串常量拼接

public class Demo1_22 {

    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";
        System.out.println(s3 == s5);//true

    }
}

在这里插入图片描述
行号为6时,串池中没有ab对象,就创建ab对象放入串池,执行到29行时要去常量池中找4号位置的ab符号,发现串池中已存在,就不会创建新的对象,直接引用即可。
为什么会以这种方式创建s5,其实是编译期的优化。y因为编译器发现这是两个常量a,b相加,结果在编译期间就可以确定为ab,不会再变。s4 = s1+s2,因为s1和s2是变量,在运行时被引用的值可能被修改,结果不能确定,所以要在运行时使用StringBuilder动态拼接。

示例三:intern方法

 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
    }

在这里插入图片描述
通过虚拟机指令明白:
第一行代码看行号为0的指令,往串池中添加ab字符,
第二行代码从行号为3的指令new StringBuilder对象,调用他的空参构造器,行号10new String对象,行号14将a字符添加到串池中,实例化String对象,值为a,调用StringBilder的append方法,再new一个String对象,实例化值为b,StringBuilder拼接ab,调用toString方法。

intern方法的作用就是在尝试把堆中对象放入串池中。如果串池中已有,会返回串池中的对象。并且s调用intern方法后依旧指向堆中的对象。如果串池中没有,会在串池中创建一个“ab”对象并返回,并且会让s指向串池中的“ab”对象。
  
在jdk1.6,当一个String调用intern方法时,如果串池中没有,会将堆中的字符串对象复制一份放到串池中,最后返回串池中刚加入的对象。

StringTable特性

常量池中的字符串仅是符号,第一次用到时才变为对象
利用串池的机制,可以避免重复创建字符串对象
字符串变量拼接的原理是StringBuilder(jdk1.8)
字符串常量拼接的原理是编译器优化
可以使用intern方法,主动将串池中还没有的字符串对象放入串池

StringTable优化

  • -XX:StringTableSize=1009 调整StringTable桶个数,最小为1009,如果系统内字符串较多,可以适当增大该值
  • 如果有大量字符串且有重复。使用intern方法,将字符串入池减少字符串个数,节约堆内存的使用
public class Demo1_25 {

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

未入池之前内存对比:
在这里插入图片描述
在这里插入图片描述
入池之后内存对比:

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

直接内存

直接内存:Direct Memory,不属于Java虚拟机的内存管理,属于操作系统的内存。

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

IO/DirectBuffer拷贝文件

/**
 * 演示 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使用传统阻塞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);
    }
}

Java并不具备磁盘读写的能力,要调用操作系统的提供的方法。涉及到CPU要从用户态,切换到内核态。
在这里插入图片描述
切换到内存态后,就可以由CPU的函数读取磁盘文件的内容,读取数据后内核态会在操作系统内存中画出一块缓冲区,称为系统缓存区,磁盘内容会先读入到系统缓冲区中,系统缓冲区Java代码是不能运行的,Java会在堆内存中分配一块Java的缓冲区,对应Java中new byte[_1Mb],Java代码要能读取到流中的数据,必须要从系统缓存区中的数据间接的读入到Java缓冲区,再到用户态,Java调用输出流的写入操作,重复读写操作,将文件赋值到目标位置。
在这里插入图片描述
因为Java堆内存中有块缓冲区,系统内存中由块系统缓存区,数据必然要存两份,造成不必要的数据复制,效率不是很高。

使用ByteBuffer之后,allocateDirect方法会分配一块直接内存,会在操作系统中画出一块缓冲区,Java代码和系统都可以直接访问,磁盘文件读取会读取到直接内存,Java代码也可以直接访问直接内存,减少了一次复制操作。
在这里插入图片描述

直接内存溢出

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 对方法区的实现称为元空间
    }
}

在这里插入图片描述

显式的垃圾回收gc对内存占用的影响

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垃圾回收后。。。
在这里插入图片描述

直接内存分配的底层原理:Unsafe

ByteBuffer底层分配和释放直接内存的类型为Unsafe类,jdk内部使用Unsafe类,不建议程序员使用。

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

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存,返回base代表刚分配的内存地址
        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);
        }
    }
}

分配和回收原理
使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
ByteBuffer 的实现类内部,使用了 Cleaner (虚引用类型)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存。

垃圾回收对Java中无用的对象是自动释放的,不需手动调用方法,直接内存必须主动调用freeMemory()方法,才能完成对直接内存的释放。

JVM参数

-XX:+PrintStringTableStatistics 打印字符串表的统计信息,可以精准看到串池中实例个数和占用大小信息
-XX:+PrintGCDetails -verbose:gc 打印垃圾回收信息,次数已经花费时间
 -XX:StringTableSize=1009 调整StringTable桶个数,最小为1009,如果系统内字符串较多,可以适当增大该值
  -XX:+DisableExplicitGC 禁用掉显式的垃圾回收

 在jdk8下设置-XX:MaxMetaspaceSize=8m 设置元空间初始大小
 在jdk6下设置 -XX:MaxPermSize=10m 设置永久代初始大小

-Xmn10M 设置年轻代大小。整个堆大小=年轻代大小 + 年老代大小 + 常量池。
 -Xsx500m 
-Xms500m 初始堆内存大小,设定程序启动时占用内存大小
-Xmx500m  最大堆内存,设定程序运行期间最大可占用的内存大小
-Xss256k  设置单个线程栈大小,一般默认512~1024kb,设置栈帧大小

 -XX:+UseSerialGC

JVM参数参考:https://blog.csdn.net/dsydly/article/details/106303058

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值