JVM内存模型

1、程序计数器

1.1、定义

image-20211208094716014

Program Counter Register 程序计数器 ,通过计算机CPU的寄存器(速度极快)实现,也是对计算机物理硬件的屏蔽与抽象

1.2、作用

是记住下一条JVM指令的执行地址

二进制字节码(前方的数字可看作指令的地址)         Java源代码
0: getstatic #20                    		// PrintStream out = System.out;
3: astore_1                         		// --
    
4: aload_1                          		// out.println(1);
5: iconst_1 								// --
6: invokevirtual #26 						// --
    
9: aload_1 									// out.println(2);
10: iconst_2 								// --
11: invokevirtual #26 						// --
    
14: aload_1 								// out.println(3);
15: iconst_3 								// --
16: invokevirtual #26 						// --
    
19: aload_1 								// out.println(4);
20: iconst_4 								// --
21: invokevirtual #26 						// --
    
24: aload_1 								// out.println(5);
25: iconst_5 								// --
26: invokevirtual #26 						// --
    
29: return
  • 上面右侧的Java源代码编译后会编程左边的二进制字节码,它们都是JVM中的一系列指令,而Java跨平台的基础就是这一系列的二进制指令,所有平台都长这样
  • 这些指令还不能直接交给CPU来执行,还必须经过解释器(JVM执行引擎之一,负责把每一个JVM指令解释为机器码,机器码便可以交给CPU直接执行)
  • 如第一行,当getstatic指令放入解释器执行后,程序计数器便会将astore_1指令的地址3放入,这样重复的执行操作

1.3、特点

  • 是线程私有的,每一个线程都会有自己的程序计数器
    • 当一个线程时间片用完后,当前执行到哪一行指定,会在程序计数器中保存下来,然后切换到另一个线程执行,当这个线程再次执行的时候,就还能根据当前程序计数器中保存的指令行,来决定从哪里开始执行
  • JVM规范中唯一一个不会存在内存溢出的区域

2、虚拟机栈

2.1、定义

image-20211208122837918

Java Virtual Machine StacksJava 虚拟机栈) ,即线程运行所需要的内存空间

  • 栈帧(Frame):线程执行代码的时候是从上而下的,而代码都是由一个个方法组成,所以我们将一个个方法所需要的内存空间称为一个个栈帧,而Java虚拟机栈中存放的单位便是栈帧,栈帧中包含参数,局部变量,返回地址,当一个方法被调用后,就会将其压入Java虚拟机栈,当这个方法执行完毕,就会释放这个方法所占用的内存,让其出栈,如果一个方法A压栈后,其中又调用了另一个方法B,然后又会将方法B压栈,如果方法B中又调用了另一个方法C,同样还会将方法C压栈…,最后从栈顶开始调用方法链执行,…方法C执行完毕出栈,执行方法B,执行完毕出栈,执行方法A,执行完毕再出栈。每个线程都只能有一个活动栈帧,对应当前正在执行的那个方法

2.2、演示栈帧

使用下面示例代码,将断点打到main方法里

/**
 * @author PengHuanZhi
 * @date 2021年12月08日 18:41
 */
public class Frames {
    public static void main(String[] args) throws InterruptedException {
        method1();
    }

    private static void method1() {
        int i = method2(1, 2);
        System.out.println(i);
    }

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

观察调试控制台

image-20211208184315709

图中的Frames便是对应于咱们的Java虚拟机栈帧,当前代码执行到Main方法,还未进入method1方法,所以栈帧集合只有一个main方法,现在进入method1,再次观察

image-20211208184404597

同理,再次进入method2

image-20211208184624676

method2方法中出来,再观察,可以发现,method2被弹出去啦

image-20211208185052928

2.3、几个问题

垃圾回收是否设计栈内存?

  • 栈内存即一次次的方法调用,所产生的栈帧内存,而栈帧内存在每一次方法调用完毕都会弹出栈,会被自动回收掉,所以根部不需要垃圾回收去管理栈内存

栈内存分配越大越好么?

  • 对于Java程序,我们可以手动指定Java的栈内存大小:

image-20211208185904430

-Xss1m
-Xss1024k
-Xss1048576
  • 强调,栈,划分的越多,反而会让线程数逐渐减少,因为物理内存的大小是一定的,如果一个线程给它分配的内存越多,那么整个物理内存所能承载的线程数就会变少,划分的多了,只能提升方法递归调用的能力,不会提高运行效率,一般使用默认的栈内存大小就可以了

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

  • 要考虑这个问题,可以参考这个变量是否是多个线程所共享的即可,对于每一个线程执行同一个方法时,都会在自己的栈中新建一个对应的栈帧, 而各个栈帧所用内存是独立唯一的,其中都会存在一个独立的局部变量,所以方法内部的局部变量是线程安全的。如果变量是static的,则每一个栈帧都共享此变量,则会出现线程安全的问题,下面我们看一段代码,再思考一下
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;
}
  • m1根据上面的结论,很容易判断,是线程安全的
  • m2,由于StringBuilder对象是一个方法参数,它是通过其他地方传递过来的,所以它可能会被多个线程使用到,故而不是线程安全的
  • m3中是StringBuilder被当作返回出返回了,则它可能会被其他线程接收访问,故而也不是线程安全的

结论那是:如果方法内局部变量没有逃离方法的作用访问,它是线程安全的

2.4、栈内存溢出

在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  • 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

2.5、线程运行诊断

案例1、CPU占用过多

/**
 * @author PengHuanZhi
 * @date 2021年12月09日 9:37
 */
public class WhileTrueDemo {
    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();
    }
}
  • 将上面代码放在一个Linux操作系统环境下,然后编译运行:
javac WhileTrueDemo.java
java WhileTrueDemo
  • top命令观察系统Cpu占用情况:

image-20211209122334094

  • 可以看到是java的进程占用CPU近百分百,使用ps命令查看哪一个线程占用这么高
ps H -eo pid,tid,%cpu

image-20211209122557606

  • 可以看到是7048进程的7063线程导致CPU占用近百分百,使用jstack+进程id找到问题的进程,进一步找到问题代码的源码行号
jstack 7048
  • 由于jstack显示的线程号是16进制,所以这里将7063转换为16进制:1B97

image-20211209123022519

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

/**
 * @author PengHuanZhi
 * @date 2021年12月09日 12:52
 */
public class DeadLockDemo {
    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();
    }
}

class A {
};

class B {
};
  • 同样将上述代码放在Linux操作系统中,然后先编译(javac)再运行(使用nohub java可以直接返回进程号,然后后台执行)
javac DeadLockDemo.java
nohub java DeadLockDemo &

image-20211209125839485

  • 再次使用jstack观察:
jstack 7756

image-20211209125943722

3、本地方法栈

image-20211209130020816

Java虚拟机调用本地方法时,需要给本地方法提供的一块内存空间,本地方法(Native method),不是由Java代码编写的,Java是不能直接和操作系统底层API打交道,需要依赖使用C/C++等语言编写的方法来与操作系统API交互

  • 本地方法都是使用native关键字修饰的,比如Object类中的clone方法

image-20211209130430542

4、堆

image-20211209130517679

4.1、定义

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

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 当对象不再被人使用,就会被垃圾回收机制回收

4.2、堆内存溢出

/**
 * @author PengHuanZhi
 * @date 2021年12月09日 13:10
 */
public class OutOfMemoryDemo {
    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a);
                a = a + a;
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

执行后观察控制台:

image-20211209131224043

  • 可以看见由于堆空间内存不足导致了内存溢出

  • 也可以通过调整系统参数,动态指定堆内存大小

-Xmx2048m

对于线上服务器的内存一般会很大,在项目初期可能不太会出现内存溢出的问题,如果想要尽早暴露出这个问题,可以先尝试将堆内存设置的小一些

4.3、堆内存诊断

堆内存诊断命令大致有如下几个

  • jps

    • jps可以查看当前系统中有那些Java进程
  • jmap

    • 可以查看堆内存占用情况
  • jconsole

    • 带有图形化界面,是一个多功能的检测工具,可以连续检测

使用下面的Demo我们来实战一下

/**
 * @author PengHuanZhi
 * @date 2021年12月09日 13:21
 */
public class MonitorDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000);
        // 10 Mb
        byte[] array = new byte[1024 * 1024 * 10];
        System.out.println("2...");
        Thread.sleep(20000);
        array = null;
        System.gc();
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}

4.3.1、jmap

  • 第一个sleep睡眠30S之前,我们将当前进程占用Id记下来,并用jmap对这个进程的堆内存情况记录一下

image-20211209183723520

PS D:\IDEA WorkSpace\JVMProject> jmap -heap 18808
Attaching to process ID 18808, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12

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

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 8568963072 (8172.0MB)
   NewSize                  = 178782208 (170.5MB)
   MaxNewSize               = 2856321024 (2724.0MB)
   OldSize                  = 358088704 (341.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 = 134742016 (128.5MB)
   used     = 10779448 (10.280082702636719MB)
   free     = 123962568 (118.21991729736328MB)
   8.000064360028574% used
From Space:
   capacity = 22020096 (21.0MB)
   used     = 0 (0.0MB)
   free     = 22020096 (21.0MB)
   0.0% used
To Space:
   capacity = 22020096 (21.0MB)
   used     = 0 (0.0MB)
   free     = 22020096 (21.0MB)
   0.0% used
PS Old Generation
   capacity = 358088704 (341.5MB)
   used     = 0 (0.0MB)
   free     = 358088704 (341.5MB)
   0.0% used

3174 interned Strings occupying 260400 bytes.
  • 等待第一次睡眠结束第二次睡眠开始,再次用jmap查看一下堆内存使用情况
PS D:\IDEA WorkSpace\JVMProject> jmap -heap 18808
Attaching to process ID 18808, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12

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

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 8568963072 (8172.0MB)
   NewSize                  = 178782208 (170.5MB)
   MaxNewSize               = 2856321024 (2724.0MB)
   OldSize                  = 358088704 (341.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 = 134742016 (128.5MB)
   used     = 21265224 (20.28009796142578MB)
   free     = 113476792 (108.21990203857422MB)
   15.782177401887768% used
From Space:
   capacity = 22020096 (21.0MB)
   used     = 0 (0.0MB)
   free     = 22020096 (21.0MB)
   0.0% used
To Space:
   capacity = 22020096 (21.0MB)
   used     = 0 (0.0MB)
   free     = 22020096 (21.0MB)
   0.0% used
PS Old Generation
   capacity = 358088704 (341.5MB)
   used     = 0 (0.0MB)
   free     = 358088704 (341.5MB)
   0.0% used

3175 interned Strings occupying 260448 bytes.
  • 最后一次sleep时,再次用jmap查看一下
PS D:\IDEA WorkSpace\JVMProject> jmap -heap 18808
Attaching to process ID 18808, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12

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

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 8568963072 (8172.0MB)
   NewSize                  = 178782208 (170.5MB)
   MaxNewSize               = 2856321024 (2724.0MB)
   OldSize                  = 358088704 (341.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 = 134742016 (128.5MB)
   used     = 2694856 (2.5700149536132812MB)
   free     = 132047160 (125.92998504638672MB)
   2.000011637053137% used
From Space:
   capacity = 22020096 (21.0MB)
   used     = 0 (0.0MB)
   free     = 22020096 (21.0MB)
   0.0% used
To Space:
   capacity = 22020096 (21.0MB)
   used     = 0 (0.0MB)
   free     = 22020096 (21.0MB)
   0.0% used
PS Old Generation
   capacity = 358088704 (341.5MB)
   used     = 973384 (0.9282913208007812MB)
   free     = 357115320 (340.5717086791992MB)
   0.2718276195609901% used

3161 interned Strings occupying 259456 bytes.

4.3.2、jconsole

  • 还是这个Demo,重新运行,然后在控制台输入jconsole,在弹出的页面选择本地进程Demo进程,然后直接点击连接

image-20211209184723963

  • 然后再选择不安全的连接

image-20211209184801509

  • 就可以动态的观察当前堆内存的使用情况了

image-20211209184955152

4.3.3、jvisualvm

有些情况下,在垃圾回收后,内存占用仍然很高,接下来,我们运行下面的Demo,然后使用jps+jmap查看一下当前堆内存使用情况,我们需要关注的是最后一行PS Old Generation(后面会解释,这里先略过)

PS D:\IDEA WorkSpace\JVMProject> jmap -heap 18848
Attaching to process ID 18848, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12

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

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 8568963072 (8172.0MB)
   NewSize                  = 178782208 (170.5MB)
   MaxNewSize               = 2856321024 (2724.0MB)
   OldSize                  = 358088704 (341.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 = 134742016 (128.5MB)
   used     = 108979624 (103.93106842041016MB)
   free     = 25762392 (24.568931579589844MB)
   80.88020888747872% used
From Space:
   capacity = 22020096 (21.0MB)
   used     = 21815664 (20.805038452148438MB)
   free     = 204432 (0.1949615478515625MB)
   99.07161167689732% used
To Space:
   capacity = 22020096 (21.0MB)
   used     = 0 (0.0MB)
   free     = 22020096 (21.0MB)
   0.0% used
PS Old Generation
   capacity = 358088704 (341.5MB)
   used     = 105915984 (101.00935363769531MB)
   free     = 252172720 (240.4906463623047MB)
   29.578141621579885% used

5442 interned Strings occupying 449456 bytes.
  • 再使用jconsole工具,我们选择内存页面,点击执行GC垃圾回收

image-20211209190220147

  • 再次观察jmap日志的PS Old Generation使用情况,发现还是占用非常高
PS D:\IDEA WorkSpace\JVMProject> jmap -heap 18848
Attaching to process ID 18848, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12

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

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 8568963072 (8172.0MB)
   NewSize                  = 178782208 (170.5MB)
   MaxNewSize               = 2856321024 (2724.0MB)
   OldSize                  = 358088704 (341.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 = 269484032 (257.0MB)
   used     = 9529640 (9.088172912597656MB)
   free     = 259954392 (247.91182708740234MB)
   3.536254051594419% used
From Space:
   capacity = 22020096 (21.0MB)
   used     = 0 (0.0MB)
   free     = 22020096 (21.0MB)
   0.0% used
To Space:
   capacity = 22020096 (21.0MB)
   used     = 0 (0.0MB)
   free     = 22020096 (21.0MB)
   0.0% used
PS Old Generation
   capacity = 358088704 (341.5MB)
   used     = 212021304 (202.19927215576172MB)
   free     = 146067400 (139.30072784423828MB)
   59.20915729304882% used

5502 interned Strings occupying 454520 bytes.
  • 可以看到垃圾回收后,堆内存还是没有得到有效释放,这时候借助另一个工具jvisualvm,在控制台输入该命令,然后在弹出的页面选择Demo进程

image-20211209190613783

  • 可以看到这个工具也可以以图像形式动态展示堆内存使用情况,点击右边的堆Dump,可以对当前堆内存使用情况做一个快照,然后在快照中检查堆中对象的使用情况

image-20211209190751198

  • 可以看到其中的ArrayList占用内存很高

image-20211209190828797

  • 点击查看详情

image-20211209190929102

  • 可以看到一个元素就有1M,总计是200M左右

image-20211209191058085

5、方法区

image-20211209201622970

5.1、定义

网上对方法区的定义不太清晰,这里给出官方权威的定义:点击跳转

image-20211209202548108

将其大致理解归纳如下

  • 所有线程所共享
  • 存储了类的结构的相关信息,包含,类的成员变量,方法数据,成员方法,构造方法的代码,以及运行时常量池(后面会单独说明)
  • 方法区会在JVM启动的时候创建
  • 逻辑上面其实是堆的组成部分(但是并不强制定义方法区的位置,所以不同的JVM实现厂商的实现方式也不一样,所以只能说是逻辑上)
  • 如果创建方法区的时候,内存不够了,也会抛出OutOfMemory堆栈溢出错误

5.2、组成

这里以OracleHotspot JVM为例

image-20211209202705842

  • 1.6方法区采用永久代实现,里面会保存一个类中的若干信息,其中的StringTable常量表(后面会讲)保存在运行时常量池中
  • 1.8后方法区采用了元空间实现,它保存在了操作系统的本地内存中,其中类的若干信息也随之保存在本地内存中,但是StringTable常量表确保存在了堆内存中

5.3、方法区内存溢出

  • 1.8以前会导致永久代内存溢出
  • 1.8以后会导致元空间内存溢出

我们使用下面的代码来演示

/**
 * @author PengHuanZhi
 * @date 2021年12月09日 20:33
 */
public class MethodOutOfMemory extends ClassLoader {
    public static void main(String[] args) {
        int j = 0;
        try {
            MethodOutOfMemory test = new MethodOutOfMemory();
            for (int i = 0; i < 100000; 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);
            }
        } finally {
            System.out.println(j);
        }
    }
}
  • 继承ClassLoader可以使我们的类可以用代码来加载类的字节码
  • ClassWriter可以用来生成类的字节码

参数意义分别是

  • Opcodes.V1.8标识Java版本号
  • Opcodes.ACC_PUBLIC标识访问修饰符为public
  • “Class”+i为类的名称
  • null包名
  • **“java/lang/Object”**类的父类
  • null要实现的接口名称

由于当下我运行的环境为JDK1.8,方法区使用的是操作系统的元空间,内存会直接使用我们操作系统的物理内存,很难演示内存溢出的问题,我们可以配置一个系统参数来限制最大元空间大小:

-XX:MaxPermSize=16m

image-20211209204843263

  • 如果是1.8之前采用的永久代实现,则配置参数会有变化:
-XX:MaxPermSize=16m

5.4、运行时常量池

给出如下一个简单打印表达式

public static void main(String[] args) {
    System.out.println("HelloWorld!");
}
  • 这段代码运行之前,肯定需要编译为一个二进制字节码,其中大致有三部分组成,如类基本信息,类方法定义,常量池

讲编译后的class文件使用javap反编译:

javap -v HelloWorld

结果如下

Classfile /root/HelloWorld.class
  Last modified 2021-12-9; size 425 bytes
  MD5 checksum a3776b6ec5315d41074a229033ec3c42
  Compiled from "HelloWorld.java"
  //类方法定义
public class HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
  //常量池
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // HelloWorld!
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // HelloWorld
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               HelloWorld.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               HelloWorld!
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               HelloWorld
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
  //方法定义
{
  public 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 1: 0

  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 HelloWorld!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
}
SourceFile: "HelloWorld.java"

image-20211209211221917

定义:

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

5.5、StringTable

俗称串池

5.5.1、常量池与串池的关系

package com.phz;

/**
 * @author PengHuanZhi
 * @date 2021年12月09日 21:19
 */
public class Demo {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
    }
}

讲上述代码编译然后再反编译:

  • 常量池:

image-20211210093000119

  • 程序指令

image-20211209212937570

  • 常量池最初存在于字节码文件中,当其运行时,就会被加载进入运行时常量池中,但只是加载进入运行时常量池,这些信息还没有成为一个对象,仅仅为运行时常量池中的符号,当程序去引用它时,如String s1 = “a”,才会把a符号变为一个字符串对象,然后再准备一个空间StringTable,讲刚才的符号存入StringTable中(用到了才会创建,类似于懒加载),最后就会是:[“a”,”b”,”ab”]

5.5.2、字符串变量拼接

将上一个Demo中添加一行

package com.phz;

/**
 * @author PengHuanZhi
 * @date 2021年12月09日 21:19
 */
public class Demo {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
    }
}

同样进行编译反编译观察

image-20211210094902450

我们进入StringBuilder类的toString方法,可以看到,StringBuilder最终是new出来的一个新的String对象

image-20211210095024654

也就是说,之前的s3,是存在于我们的串池中,而新的s4,因为是new出来的,所以是存在于堆中,两者虽然都是ab,但是如果用s3 == s4 应该返回false

5.5.3、编译期优化

现在再将上面加的那行代码改为String s4 = “a” + “b”;

String s4 = “a” + “b”;

编译后再次反编译查看

image-20211210123143370

可以发现,现在就直接就去常量池中找已经存在的ab,这时候调用s3 == s4 返回的则是true,这其实是Javac在编译期间的优化,因为结果已经在编译期间被确定了,都是常量,而上一步的s1+s2都是变量,所以需要重新new

5.5.4、字符串延迟加载

执行下面这段代码

public static void main(String[] args) {
    System.out.println();
    System.out.println("1");
    System.out.println("2");
    System.out.println("3");
    System.out.println("1");
    System.out.println("2");
    System.out.println("3");
}

使用IDEA自带的断点调试工具,开启调试,程序执行打印1前,观察内存中的String个数为2131

image-20211210124417115

然后走完前三行代码

image-20211210124436399

再将代码走完

image-20211210124458105

也就是说前面1~3已经在串池中创建好了,后面再使用就不会创建了

5.5.5、intern

可以主动将串池中还没有的字符串对象主动放入串池

public static void main(String[] args) {
    //new 出来的在堆中
    String s1 = new String("a");
    String s2 = new String("b");
    //调用了StringBuilder的append两次,然后调用toString来创建一个String赋予s3
    String s3 = s1 + s2;
    //串池中ab不等于堆中的s3
    //System.out.println(s3 == "ab");
    //尝试将ab放入串池,如果有则不会放入,如果没有则会放入,不管有无都会将串池中的对象返回出来
    String intern = s3.intern();
    //相等
    System.out.println(intern == "ab");
}

如果有则不会放入,如果没有则会放入,不管有无都会将串池中的对象返回出来

区别如下:

  • JDK1.8之后:如果没有,放入串池,那么**s3 == “ab”**返回真,如果已经有了,就不会再放,**s3 == “ab”**则会返回假
  • JDK1.8之前:如果没有,new一个新的,将新的放入串池,那么**s3 == “ab”**返回真,如果已经有了,就不会再放,**s3 == “ab”**则会返回假

5.6、StringTable面试题

public static void main(String[] args) {
    String s1 = "a";
    String s2 = "b";
    String s3 = "a" + "b";  // ab 串池中
    String s4 = s1 + s2;    // StringBuilder =》 new String("ab") 堆中
    String s5 = "ab";       //串池
    String s6 = s4.intern();//ab已存在串池,s4仍然在堆,s6为串池中的ab

    // 问
    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.7、StringTable位置

  • 永久代的垃圾回收效率很低,只有在Full GC(老年代空间不足)的时候才会触发,由于一个应用程序中常量会有很多,都放在了串池中,如果回收效率不高,很容易造成内存不足的情况
  • 而1.8将串池放在了堆中,堆的垃圾回收在Minor GC的时候就会触发,效率会高很多

5.8、StringTable垃圾回收

运行如下Demo

/**
 * @author PengHuanZhi
 * @date 2021年12月10日 15:48
 */
public class StringTableGCDemo {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

运行之前,我们添加几个运行参数:

-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

其中:

  • -Xmx10m:设置虚拟机堆内存最大值
  • -XX:+PrintStringTableStatistics:打印串池的统计信息
  • -XX:+PrintGCDetails-verbose:gc:可以显示垃圾回收的详细信息

观察一下输出结果

image-20211210160145001

初始的一些字符串常量会有方法名,类名等信息

现在我们在Try-Catch语句中加入下面代码

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

再次观察控制台

image-20211210160430281

此时还没有触发垃圾回收,尝试将循环次数增加至10000,按道理这里的Entry应该变为一万多,再次观察

image-20211210160827481

可以发现,当分配失败后,会触发垃圾回收

5.9、StringTable性能调优

因为StringTable底层采用Hash表存储的,所以如果想要StringTable的性能更高,可以尝试将Hash桶的个数变多,这样Hash碰撞的几率就会更小,链表长度也会更短,调整StringTableHash桶个数也是需要配置虚拟机参数的:

-XX:StringTableSize=桶个数

一般如果程序中常量非常多,那么调整StringTable的桶个数是很有必要的

还有一种情况就是,程序中出现了大量的字符串,而这些字符串可能会出现重复的问题,那么我们可以使用intern将其入池

6、直接内存

6.1、定义

Direct Memory,常见于NIO操作,用于数据缓冲区,分配回收成本较高,但是读写性能高,不受JVM内存回收管理,所以需要合理使用,否则容易造成内存泄漏

6.2、直接内存与GC

使用下面的Demo我们来演示一下如何使用直接内存

/**
 * @author PengHuanZhi
 * @date 2021年12月10日 20:32
 */
public class DirectGCDemo {
    //1G
    static int SIZE = 1024 * 1024 * 1024;

    /**
     * -XX:+DisableExplicitGC 显式的
     */
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(SIZE);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        // 显式的垃圾回收,Full GC
        System.gc();
        System.in.read();
    }
}

将程序运行起来,观察系统任务管理器:

image-20211210203555327

控制台回车,开始执行垃圾回收,观察该直接内存会不会被释放

image-20211210203620517

发现被释放了,不是直接内存不会被GC管理吗,怎么还被释放了

为了解释这个问题,这里先介绍一下JDK中一个很底层的类UnSafe,它参与我们JDK分配和释放直接内存的工作,但是它不能直接拿来使用,毕竟只是JDK内部自己使用的东西,我们做程序一般情况不要用它,它内部边保存了自身的一个静态的私有对象,我们用反射把它拿出来:

image-20211210204242740

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

    // 释放内存
    unsafe.freeMemory(base);
    System.in.read();
}
private static Unsafe getUnsafe() {
    try {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(null);
    } catch (Exception e) {
        throw new RuntimeException();
    }
}

同样的方式,运行

image-20211210204732589

回车释放

image-20211210204755625

我们来扒一扒ByteBuffer是如何给我们释放直接内存的,进入allocateDirect方法

image-20211210204905868

进入这个构造方法,便可以直接看到调用了unsafe对象去分配了一块直接内存

image-20211210204924990

那什么时候调用直接内存的释放呢,要知道需要直接调用unsafefreeMemory才能释放,我们程序并没有手动去释放,我们观察最后面的cleaner

image-20211210205115257

其中第二个参数Deallocator 我们进去看看是个什么

image-20211210205233488

发现这是一个Runnable的实现类,run方法中便是释放直接内存,那么谁执行这个Runnable呢,回到前面,Cleaner这个类有点特殊,是Java中的一个虚引用类型,它有一个特点,当它关联的对象被垃圾回收时,就会执行它里面的runnable事件

image-20211210205653213

这个Cleaner运行位置不在Main,而在Java中的ReferenceHandler的一个守护线程中,专门去检测虚引用对象

6.3、关闭显式GC

在程序中,我们可以手动调用System.GC执行垃圾回收,但是这一操作很耗费性能,为了避免程序员自己经常在代码中显式执行垃圾回收,我们可以配置下面这个参数来屏蔽显式GC

-XX:+DisableExplicitGC

但是这样对于我们使用的直接内存就很不友好,我们只能等待自身触发垃圾回收的时候才能正确释放,但是这样会导致我们有一段时间内,直接内存使用率很高,所以一个比较合理的解决方式便是,直接使用Unsafe对象,手动来管理直接内存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值