1. JVM-内存结构

JVM简介

1. 什么是JVM?

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

2. JVM的好处

  1. 一次编写,到处运行的基石【重点】
  2. 自动内存管理,垃圾回收功能【重点】
  3. 数据下标越界检查
  4. 多态,面向对象编程

3. JVM、JRE、JDK三者比较

image.png

4. 学习JVM有什么用?

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

5. JVM组成有哪些?

JVM的内存结构

image.png

JVM类加载过程

image.png

6. 常见的JVM

image.png

JVM内存结构

  1. 程序计数器
  2. 虚拟机栈
  3. 本地方法栈
  4. 方法区(元空间)

1. 程序计数器 PC Register

image.png

1. 定义

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

2. 作用

  • 作用:记住下一条jvm指令的执行地址
  • 特点:线程私有的; 不存在内存溢出,也是JVM规范中唯一没有OutOfMemoryError的区域
  • 二进制字节码:JVM指令 —> 解释器 —> 机器码 —> CPU
  • 程序计数器:记住下一条jvm指令的执行地址,硬件方面通过【寄存器】实现

示例:二进制字节码:JVM指令 java源代码

image.png

2. 虚拟机栈 JVM Stacks

image.png
先了解一下栈的数据结构

  • 栈Stack,先进后出FILO
  • 栈-线程运行需要的内存空间
  • 栈帧-每个方法运行时需要的内存
  • 栈-由多个栈帧组成

1. 定义

Java Virtual Machine Stacks (Java虚拟机栈)

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

2. 栈问题

1. 垃圾回收是否涉及栈内存?

栈内存不涉及垃圾回收

2. 栈内存分配越大越好?

栈内存不是越大越好,如果设置过大,会影响可用线程的数量;比如-Xss1m、-Xss2m,在总内存不变的情况下,可用线程数量会减少

3. 方法内的局部变量是否线程安全?

方法内的局部变量是线程安全(不逃离方法的作用域为前提),因为方法内的局部变量各自在自已独立的内存中;如果是static int 就是线程共享的,就不是线程安全;主要看变量是否是线程共享、还是线程私有
注意:

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

3. 栈内存溢出

java.lang.StackOverflowError
注:可以使用 -Xss调整栈内存的大小,示例:-Xss256k
栈帧过多导致栈内存溢出,比如:递归没有设置结束条件,我们生产环境推荐尽量不使用递归
栈帧过大导致栈内存溢出,比如:方法写得太长,局部变量太多,导致栈帧撑爆了栈内存,这种情况比较少

4. 线程运行诊断

1. cpu占用过高,如何诊断
  1. top定位哪个进程对cpu的占用过高

image.png

  1. ps H -eo pid,tid,%cpu 查看linux所有进程、线程、CPU消耗情况

image.png

  1. ps H -eo pid,tid,%cpu | grep 进程id 用ps命令进一步定位哪个线程引起的CPU占用过高

image.png

  1. jstack 进程pid 查看线程信息,jstack输出的线程id(nid)是16进制的,需要将十进制的线程id转成16进制; 可以根据16进制的线程id找到有问题的线程,进一步定位问题代码的源码行号

image.png
通过上述方式找到了源代码CPU消耗过高的文件及行号
image.png

2. 程序运行很长时间没有结果,如何诊断

nohup java -cp /root/JvmLearn-1.0-SNAPSHOT.jar com.jvm.stack.T07_StackDeadLock &
运行一个java程序,很长时间没有结果,使用jstack 线程id查看是否发生了死锁
image.png
image.png

3. 本地方法栈 Native Method Stacks

本地方法栈不是Java编写的代码,是C/C++编写的,通过C/C++调用底层
image.png
java调用c或c++的代码来调用底层

4. 堆 Heap

image.png

1. 定义

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

2. 堆内存溢出

java.lang.OutOfMemoryError
代码参考:cn.itcast.jvm.t1.heap.Demo1_5

/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */
 
public class T01_HeapOutOfMemoryError {
 
    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++;
                TimeUnit.MILLISECONDS.sleep(2000);
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
 
}

生产建议

生产环境建议:如果内存比较大,内存溢出不会那么快的暴露;这时,我们可以将堆内存调小,让内存溢出尽早暴露
-Xmx 设置堆内存空间的最大值 示例:-Xmx8m
-Xms 设置对内存空间的初始值 示例:-Xms8m

3. 堆内存诊断

  • jps工具:查看当前系统中有哪些java进程
  • jmap工具:查看堆内存占用情况 jmap -heap pid(进程id)(只能查询某一个时刻,堆内存的占用情况)
  • jstack 工具:线程监控
  • jconsole工具:图形界面的,多功能的检测工具,可以连续监测
  • jvisualvm工具:图形界面的,多功能的检测工具,可以连续监测;还有dump

代码参考:cn.itcast.jvm.t1.heap.Demo1_4

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);
    }
}
  1. 运行程序,jps找到运行的该程序进程id
E:\BaiduNetDisk\Download\资料--jvm黑马\代码\jvm>jps
63904 RemoteMavenServer36
1348
48964 HbteSaasQmsApplication
51844 HbteSaasPurchaseApplication
51112 HbteSaasGatewayApplication
62728 Launcher
40812 Launcher
53228 HbteSaasProduceApplication
47504 Jps
54096 HbteSaasStoreApplication
57104 SharpApplication
48308 HbteSaasPlatformApplication
48628 HbteSaasAuthApplication
49204 Launcher
65172 Demo1_4
62908 Launcher
  1. 分别在打印1、2、3的时刻使用 jmap -heap pid(进程id)查看堆内存的变化
E:\BaiduNetDisk\Download\资料--jvm黑马\代码\jvm>jmap -heap 65172
Attaching to process ID 65172, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.181-b13

using thread-local object allocation.
Parallel GC with 6 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4246732800 (4050.0MB)
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1415577600 (1350.0MB)
   OldSize                  = 177733632 (169.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 66584576 (63.5MB)
   used     = 5327016 (5.080238342285156MB)
   free     = 61257560 (58.419761657714844MB)
   8.000375342181348% used
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 177733632 (169.5MB)
   used     = 0 (0.0MB)
   free     = 177733632 (169.5MB)
   0.0% used

1736 interned Strings occupying 177872 bytes.

E:\BaiduNetDisk\Download\资料--jvm黑马\代码\jvm>jmap -heap 65172
Attaching to process ID 65172, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.181-b13

using thread-local object allocation.
Parallel GC with 6 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4246732800 (4050.0MB)
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1415577600 (1350.0MB)
   OldSize                  = 177733632 (169.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 66584576 (63.5MB)
   used     = 15812792 (15.080253601074219MB)
   free     = 50771784 (48.41974639892578MB)
   23.748430867833417% used
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 177733632 (169.5MB)
   used     = 0 (0.0MB)
   free     = 177733632 (169.5MB)
   0.0% used

1737 interned Strings occupying 177920 bytes.

E:\BaiduNetDisk\Download\资料--jvm黑马\代码\jvm>jmap -heap 65172
Attaching to process ID 65172, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.181-b13

using thread-local object allocation.
Parallel GC with 6 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4246732800 (4050.0MB)
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1415577600 (1350.0MB)
   OldSize                  = 177733632 (169.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 66584576 (63.5MB)
   used     = 2663424 (2.5400390625MB)
   free     = 63921152 (60.9599609375MB)
   4.0000615157480315% used
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 177733632 (169.5MB)
   used     = 766440 (0.7309341430664062MB)
   free     = 176967192 (168.7690658569336MB)
   0.43122958293003316% used

1723 interned Strings occupying 176928 bytes.
  1. 程序运行后,在控制台上使用jconsole,连接被监控的进程(jsonsole之间监控进程)

image.png
image.png
image.png
image.png
image.png
image.png

1. 垃圾回收后,内存占用任然很高,排查方式

代码参考:cn.itcast.jvm.t1.heap.Demo1_13

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

解决方式:
jvisualvm 可以使用dump,查找最大的对象堆转储 dump(基于上述问题,使用工具进行查看); 在测试环境下,我们可以开启dump文件记录,然后将dump文件导入到jvisualvm工具查看,占用最多的内存的对象是哪些。
image.png
image.png
image.png
通过堆转储,下载dump文件并用jvisualvm分析dump文件,查看大对象,查找问题,排查原始代码
参考:
Linux下获取java应用的Dump文件
linux环境下导出项目的堆栈dump文件

5. 方法区 Method Area (元空间 Metaspace)

image.png

1. 定义

  • 线程共享
  • 在JVM启动时创建,在逻辑上属于堆的一部分(看厂商实现)
  • 方法区也有可能内存溢出

2. 组成

image.png

3. 方法区内存溢出

1.8 以前会导致永久代内存溢出

  • 演示永久代内存溢出: java.lang.OutOfMemoryError: PerGen space
  • -XX:MaxPerSize=8m

1.8 之后会导致元空间内存溢出(系统内存)

  • 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
  • -XX:MaxMetaspaceSize=8m
1. 元空间内存溢出演示案例

自己将元空间的大小设置得很小,动态加载的类又很多,导致元空间OOM
Jdk1.8 参考代码:cn.itcast.jvm.t1.metaspace.Demo1_8
image.png

自己在电脑上测试,要调到20M左右才会出现元空间溢出的问题,并且循环次数要调大

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=20m
 */
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 < 10000000; 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);
        }
    }
}

image.png

如果设置元空间的大小为8m会提示空间太小

image.png
image.png

如果设置为12m,会报OOM,提示压缩类空间

image.png
image.png

2. 生产环境出现元空间内存溢出问题,应该锁定这些方面

虽然我们自己编写的程序没有大量使用动态加载类,但如果我们在使用外部一些框架时,可能大量动态加载类,就可能会导致元空间内存溢出。
场景(动态加载类),如果框架使用不合理也会导致方法区内存溢出

  • spring
  • mybatis

4. 运行时常量池

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

简单来说,类编译后,常量池是在class字节码文件中的,虚拟机指令通过符号地址找自己要使用到的常量信息(类名,方法名,参数类型,字面量),当class文件被jvm虚拟机加载后,就会将创建class类模板对象,并为其分配内存,这时calss文件里面虚拟机的指令要找常量池中的常量信息就需要在内存里面去找了,符号地址也就转换成真实的内存地址,直接通过内存地址去找加载到内存中的常量池信息
image.png

1. 字符串常量池JVM字节码方面原理演示

代码示例:cn.itcast.jvm.t5.HelloWorld

// 二进制字节码(类基本信息,常量池,类方法定义(包含了虚拟机指令))
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

将上述编译好的class文件进行反汇编:Javap -v HelloWord.class 反编译结果如下:

E:\学习文档\jvm>cd E:\学习文档\jvm\out\production\jvm\cn\itcast\jvm\t5

E:\学习文档\jvm\out\production\jvm\cn\itcast\jvm\t5>javap -v HelloWorld.class
Classfile /E:/学习文档/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
  Last modified 2022-7-6; 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
    flags: ACC_PUBLIC
    Code:
      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"

5. StringTable(串池-字符串常量池)

StringTable面试题

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");
String x1 = "cd";
x2.intern();

//问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1==x2);
1. StringTable常量池与串池的关系

代码参考:cn.itcast.jvm.t1.stringtable.Demo1_22

// 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 == s4);//打印false
        System.out.println(s3 == s5);//打印true
    }
}

javap -v Demo1_22.class 反编译如下:

E:\学习文档\jvm\out\production\jvm\cn\itcast\jvm\t1\stringtable>javap -v Demo1_22.class
Classfile /E:/学习文档/jvm/out/production/jvm/cn/itcast/jvm/t1/stringtable/Demo1_22.class
  Last modified 2022-7-6; size 1039 bytes
  MD5 checksum 8815b8f391f7f5387dc065e4053bf5d1
  Compiled from "Demo1_22.java"
public class cn.itcast.jvm.t1.stringtable.Demo1_22
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#36        // java/lang/Object."<init>":()V
   #2 = String             #37            // a
   #3 = String             #38            // b
   #4 = String             #39            // ab
   #5 = Class              #40            // java/lang/StringBuilder
   #6 = Methodref          #5.#36         // java/lang/StringBuilder."<init>":()V
   #7 = Methodref          #5.#41         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = Methodref          #5.#42         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Fieldref           #43.#44        // java/lang/System.out:Ljava/io/PrintStream;
  #10 = Methodref          #45.#46        // java/io/PrintStream.println:(Z)V
  #11 = Class              #47            // cn/itcast/jvm/t1/stringtable/Demo1_22
  #12 = Class              #48            // java/lang/Object
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               LocalVariableTable
  #18 = Utf8               this
  #19 = Utf8               Lcn/itcast/jvm/t1/stringtable/Demo1_22;
  #20 = Utf8               main
  #21 = Utf8               ([Ljava/lang/String;)V
  #22 = Utf8               args
  #23 = Utf8               [Ljava/lang/String;
  #24 = Utf8               s1
  #25 = Utf8               Ljava/lang/String;
  #26 = Utf8               s2
  #27 = Utf8               s3
  #28 = Utf8               s4
  #29 = Utf8               s5
  #30 = Utf8               StackMapTable
  #31 = Class              #23            // "[Ljava/lang/String;"
  #32 = Class              #49            // java/lang/String
  #33 = Class              #50            // java/io/PrintStream
  #34 = Utf8               SourceFile
  #35 = Utf8               Demo1_22.java
  #36 = NameAndType        #13:#14        // "<init>":()V
  #37 = Utf8               a
  #38 = Utf8               b
  #39 = Utf8               ab
  #40 = Utf8               java/lang/StringBuilder
  #41 = NameAndType        #51:#52        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #42 = NameAndType        #53:#54        // toString:()Ljava/lang/String;
  #43 = Class              #55            // java/lang/System
  #44 = NameAndType        #56:#57        // out:Ljava/io/PrintStream;
  #45 = Class              #50            // java/io/PrintStream
  #46 = NameAndType        #58:#59        // println:(Z)V
  #47 = Utf8               cn/itcast/jvm/t1/stringtable/Demo1_22
  #48 = Utf8               java/lang/Object
  #49 = Utf8               java/lang/String
  #50 = Utf8               java/io/PrintStream
  #51 = Utf8               append
  #52 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #53 = Utf8               toString
  #54 = Utf8               ()Ljava/lang/String;
  #55 = Utf8               java/lang/System
  #56 = Utf8               out
  #57 = Utf8               Ljava/io/PrintStream;
  #58 = Utf8               println
  #59 = Utf8               (Z)V
{
  public cn.itcast.jvm.t1.stringtable.Demo1_22();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      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/t1/stringtable/Demo1_22;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=6, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        27: astore        4
        29: ldc           #4                  // String ab
        31: astore        5
        33: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        36: aload_3
        37: aload         4
        39: if_acmpne     46
        42: iconst_1
        43: goto          47
        46: iconst_0
        47: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
        50: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        53: aload_3
        54: aload         5
        56: if_acmpne     63
        59: iconst_1
        60: goto          64
        63: iconst_0
        64: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
        67: return
      LineNumberTable:
        line 11: 0
        line 12: 3
        line 13: 6
        line 14: 9
        line 15: 29
        line 17: 33
        line 18: 50
        line 19: 67
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      68     0  args   [Ljava/lang/String;
            3      65     1    s1   Ljava/lang/String;
            6      62     2    s2   Ljava/lang/String;
            9      59     3    s3   Ljava/lang/String;
           29      39     4    s4   Ljava/lang/String;
           33      35     5    s5   Ljava/lang/String;
      StackMapTable: number_of_entries = 4
        frame_type = 255 /* full_frame */
          offset_delta = 46
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
        frame_type = 79 /* same_locals_1_stack_item */
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
}
SourceFile: "Demo1_22.java"

image.png
执行到对应的虚拟机指令,需要加载class文件常量池中对应的字符串到内存中的运行时常量池中时,会到StringTable中去找这个需要加载的字符串在StringTable中有没有,如果没有就放进去,有就不再放入,使用已经存在的字符串常量

2. StringTable 字符串延迟加载

演示字符串字面量也是延迟成为对象

/**
* 演示字符串字面量也是【延迟】成为对象的
*/
public class TestString {
    public static void main(String[] args) {
        int x = args.length;
        System.out.println(); // 字符串个数 2238
        
        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"); // 字符串个数 2248
        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); // 字符串个数 2248
    }
}

调试到此处 字符串个数是2238

image.png

运行到此处,加载了10个字符串到串池StringTable,字符串个数2248

image.png

运行到这里,串池StringTable中已经加载了刚才的10个字符串,所以没有继续加载了,字符串个数仍然是2248,也证明了字符串是延迟加载到串池中的

image.png

6. StringTable特性

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

StringTable_intern_1.8

代码参考:cn.itcast.jvm.t1.stringtable.Demo1_23

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);
        System.out.println( s == x );
    }

}

image.png

7. StringTable位置

  • JDK1.6版本,字符串常量池是在永久代中;
  • JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
  • JDK1.8开始,取消了Java方法区,取而代之的是位于直接内存的元空间(metaSpace)。

JDK1.6 与 JDK1.8字符串常量池对比

1. JDK1.8 字符串常量池在堆中实例验证

代码参考:cn.itcast.jvm.t1.stringtable.Demo1_6

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

设置JVM的启动参数

-Xmx10m 设置堆内存的最大大小为10m
-XX:-UseGCOverheadLimit 不限制GC的运行时间
参考:
JVM -XX: 参数介绍
JVM优化之 -Xss -Xms -Xmx -Xmn 参数设置

只设置堆内存的大小为10m时,查看运行结果

image.png
image.png
只设置10M的堆内存空间,出现了oom,但是提示信息是超出了GC的开销限制
image.png
JVM官方文档对这个参数做了说明,-XX:+UseGCOverheadLimit 开启GC的开销限制,如果98%的时间回收的内存还不到内存占比的2%时,就会任务JVM以及无可救药了,提示超出了GC的开销限制

为了能看到堆溢出的提示,我们关闭GC的开销限制

image.png
image.png
关闭了GC的开销限制后,可以看到堆内存移溢出了

8. StringTable垃圾回收

因为在jdk1.8中,字符串常量池是放在堆中,如果堆空间不足,字符串常量池也会进行垃圾回收
代码参考:cn.itcast.jvm.t1.stringtable.Demo1_7

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

    }
}

JVM参数设置
image.png

先循环100次,让100个字符串入池

image.png
1875个字符串出了程序遍历入池的100个,其他的是程序运行时用到的比如类名,方法名等字符串常量也存在于字符串常量池中,由于遍历的次数较少,入池的字符串不多,没有触发垃圾回收

现在遍历10000次,让10000个字符串入池

image.png
让10000个字符串入池后,10M的堆内存不够了,触发了垃圾回收,字符串常量池中没有用到的字符串就被垃圾回收了,所以在字符串常量池中只有8540个字符串常量了

9. StringTable 性能调优(案例)

代码参考:cn.itcast.jvm.t1.stringtable.Demo1_24

  • 调整 -XX:StringTableSize=桶个数
  • 考虑将字符串对象是否入池
1. 使用-XX:StringTableSize=大小参数增加桶的数量使StringTable性能增加案例
/**
 * 演示串池大小对性能的影响
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=10009
 *
 * 字符串常量池默认桶数组大小为:60013,对字符串常量池调优主要是调节桶数据大小;
 * 如果字符串数量较多,则需要将桶的个数调大些,以减少查询复杂度(hash碰撞几率)
 * hash碰撞少了,每个hash桶放的数据就不会太多,分布均匀
 */
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);
        }
    }
}

将StringTable桶调小些,示例操作如下:

image.png
image.png

将StringTable桶调大些,示例操作如下:

image.png
image.png
结论:放入同样多的字符串,将StringTable的桶调大些,耗时较少

2. 使用字符串常量池对字符串较多的场景减少内存占用案例
/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
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());
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

说明一下:jvm/linux.words 大约单词量在479829个,上述代码运行结果截图,如下:

读取大约48万单词堆内存占用大小耗时
未放入字符串池约300兆较短
放入字符串池约100兆较长

运行结果1:未放入字符串常量池中,运行情况截图

address.add(line);

image.png
D:\IDEAWorkSpace\jvm_study\jvm\linux.words
image.png
运行结果2:放入字符串常量池中,运行情况截图

address.add(line.intern());

image.png
image.png
结论:字符串入池节约了堆内存的使用

6. 直接内存 Direct Memory

1. 定义

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

2. 原理

  • 普通内存
    • 需要从用户态向内核态申请资源,即用户态会创建一个java 缓冲区byte[],内核态会创建系统缓冲区。
  • 直接内存
    • 需要从用户态向内核态申请资源,即内核态会创建一块直接内存direct memory,这块direct memory内存可以在用户态、内核态使用。

通常使用内存(未使用直接内存) VS 直接内存,原理对比图
image.png
分析:

  • 普通内存:因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。
  • 直接内存:直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。

3. 直接内存与传统方式读取大文件耗时对比案例

接下来,我们将对一个大约1.29G大小的视频文件进行读取并写入指定文件中,即复制。代码如下:
代码参考:cn.itcast.jvm.t1.direct.Demo1_9

/**
 * 演示 ByteBuffer 作用
 */
public class Demo1_9 {
    static final String FROM = "E:\BaiduNetDisk\Download\Spring视频-马士兵助理\SpringBoot自动装配.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直接内存directBuffer说明
测试118871.591 ms6335.745 ms没有缓存
测试25710.124 ms5497.707 ms有缓存
测试37355.304 ms5103.806 ms有缓存

4. 直接内存溢出案例

代码参考:cn.itcast.jvm.t1.direct.Demo1_10

/**
 * 演示直接内存溢出
 */
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 对方法区的实现称为元空间
    }
}

直接内存溢出

image.png
ByteBuffer一直分配直接内存,每次100M,36次后,直接内存溢出了

5. 分配和使用原理

代码参考:cn.itcast.jvm.t1.direct.Demo1_26

/**
 *  禁用显式回收对直接内存的影响
 *
 *  因为程序调用System.gc() 会触发full gc,可能会长时间在垃圾回收
 *  
 *  为了避免程序员显示调用System.gc(),  我们一般禁用显式调用System.gc()
 *  禁用显式System.gc(),会对直接内存有影响,为此,我们需要通过unSafe类的freeMemory()方法来释放直接内存
 */
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();
    }
}

image.png

通过任务管理器查看分配了1G内存的java进程

image.png

通过System.gc()显示回收了直接内存

image.png

6. 分配和回收原理及案例演示

代码参考:cn.itcast.jvm.t1.direct.Demo1_27

/**
 * 直接内存分配的底层原理:Unsafe
 *
 * 虚引用关联的对象被回收了,就会触发虚引用对象的clean方法,续而调用Unsafe的freeMemory() 方法
 *
 * 6.3 分配和回收原理
 *
 * 使用了UnSafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
 *
 * ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,
 * 那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
 */
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() {
        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);
        }
    }
}

image.png

运行程序,由unsafe类分配直接内存和释放直接内存

unsafe类分配1G直接内存
image.png
unsafe类释放直接内存
image.png

1. 直接内存回收原理

直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。
第一步:allocateDirect 的实现

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

底层是创建了一个 DirectByteBuffer 对象。
第二步:DirectByteBuffer 类

DirectByteBuffer(int cap) {   // package-private
   
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size); // 申请内存
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); // 通过虚引用,来实现直接内存的释放,this为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。
    att = null;
}

这里调用了一个 Cleaner 的 create 方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。

public void clean() {
    if (remove(this)) {
        try {
            // 都用函数的 run 方法, 释放内存
            this.thunk.run();
        } catch (final Throwable var2) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (System.err != null) {
                        (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                    }

                    System.exit(1);
                    return null;
                }
            });
        }

    }
}

可以看到关键的一行代码, this.thunk.run(),thunk 是 Runnable 对象。run 方法就是回调 Deallocator 中的 run 方法,

public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    // 释放内存
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}
2. 直接内存的回收机制总结
  • 使用了UnSafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
  • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
3. 注意
/**
  * -XX:+DisableExplicitGC 禁用显示的垃圾回收
 */
private static void method() throws IOException {
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
    System.out.println("分配完毕");
    System.in.read();
    System.out.println("开始释放");
    byteBuffer = null;
    System.gc(); // 手动 gc 失效
    System.in.read();
}

一般用 jvm 调优时,会加上下面的参数:

-XX:+DisableExplicitGC  // 禁止显示的GC

意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值