JVM -- JVM内存结构:程序计数器、虚拟机栈、本地方法栈、堆、方法区(二)

阅读前可参考

https://blog.csdn.net/MinggeQingchun/article/details/126947384

JVM的内存结构大致分为五个部分,分别是程序计数器、虚拟机栈、本地方法栈、堆和方法区。除此之外,还有由堆中引用的JVM外的直接内存

JVM8官网文档地址

The Java® Virtual Machine Specification

JVM8官网Options参数配置文档

 https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

 Java HotSpot VM Options

JDK1.8官网地址 

https://docs.oracle.com/javase/8/docs/index.html

JDK1.6官网地址

https://docs.oracle.com/javase/6/docs/index.html

JDK = JRE + 开发工具集(例如Javac编译工具等)
JRE = JVM + Java SE标准类库

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

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

特点:

(1)线程私有

(2)不会存在内存溢出

程序计数器(Program Counter Register)是JVM中一块较小的内存区域,保存着当前线程执行的虚拟机字节码指令的内存地址(可以看作当前线程所执行的字节码的行号指示器)

JVM的多线程是通过线程轮流切换并分配CPU执行时间片的方式来实现的,任何一个时刻,一个CPU都只会执行一条线程中的指令。为了保证线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程间的程序计数器独立存储,互不影响

此区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域,因为程序计数器是由虚拟机内部维护的,不需要开发者进行操作 

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

解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行

多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行 

二、虚拟机栈 Java Virtual Machine Stacks

每个线程运行时所需要的内存,称为虚拟机栈

每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

 虚拟机栈(Java Virtual Machine Stacks)是线程隔离的,每创建一个线程时就会对应创建一个Java栈,即每个线程都有自己独立的虚拟机栈

这个栈中又会对应包含多个栈帧,每调用一个方法时就会往栈中创建并压入一个栈帧,栈帧存储局部变量表、操作栈、动态链接、方法出口等信息,每一个方法从调用到最终返回结果的过程,就对应一个栈帧从入栈到出栈的过程

虚拟机栈是一个后入先出的数据结构,线程运行过程中,只有处于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法,当前活动帧栈始终是虚拟机栈的栈顶元素

局部变量表存放了编译期可知的各种基本数据类型和对象引用类型。通常我们所说的“栈内存”指的就是局部变量表这一部分。

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,运行期间不会改变局部变量表的大小。

64位的long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个

如下,方法调用生成3个栈帧 

  

栈内存并不涉及垃圾回收,栈内存的产生就是方法一次一次调用产生的栈帧内存,而栈帧内存在每次方法被调用后都会被弹出栈,自动就被回收掉,不需要垃圾回收管理 

1、栈内存溢出 StackOverflowError

1、在固定大小的情况下,JVM会为每个线程的虚拟机栈分配一定的内存大小(-Xss参数),因此虚拟机栈能够容纳的栈帧数量是有限的,若栈帧不断进栈而不出栈,最终会导致当前线程虚拟机栈的内存空间耗尽,会抛出StackOverflowError异常(栈帧过多,栈帧过大都会导致栈内存溢出

2、在动态扩展的情况下,当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时,就会抛出OutOfMemoryError异常 

1、递归调用方法自身

运行如下代码

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

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

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

报错如下:

java.lang.StackOverflowError

VM options 

可配置configurations参数 VM options(内部配置参数)

如:vm中 

-Xms512m -Xmx512m -XX:PermSize=64M -XX:MaxPermSize=256m

每一项以空格隔开

参数说明

-Xms768m:设置JVM初始堆内存为768m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存

-Xmx768m:设置JVM最大堆内存为768m。
-Xss128k:设置每个线程的栈大小。JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K。应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。
-Xmn2g:设置年轻代大小为2G。在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到JVM垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的3/8。
-XX:NewSize=1024m:设置年轻代初始值为1024M。
-XX:MaxNewSize=1024m:设置年轻代最大值为1024M。
-XX:PermSize=256m:设置持久代初始值为256M。
-XX:MaxPermSize=256m:设置持久代最大值为256M。
-XX:NewRatio=4:设置年轻代(包括1个Eden和2个Survivor区)与年老代的比值。表示年轻代比年老代为1:4。
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的比值。表示2个Survivor区(JVM堆内存年轻代中默认有2个大小相等的Survivor区)与1个Eden区的比值为2:4,即1个Survivor区占整个年轻代大小的1/6。
-XX:MaxTenuringThreshold=7:表示一个对象如果在Survivor区(救助空间)移动了7次还没有被垃圾回收就进入年老代。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,对于需要大量常驻内存的应用,这样做可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代被垃圾回收的概率,减少Full GC的频率,这样做可以在某种程度上提高服务稳定性。
标准参数,所有JVM都必须支持这些参数的功能,而且向后兼容;例如:
-client——设置JVM使用Client模式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或开发调试;在32位环境下直接运行Java程序默认启用该模式。
-server——设置JVM使Server模式,特点是启动速度比较慢,但运行时性能和内存管理效率很高,适用于生产环境。在具有64位能力的JDK环境下默认启用该模式。
非标准参数(-X),默认JVM实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容;
非稳定参数(-XX),此类参数各个JVM实现会有所不同,将来可能会不被支持,需要慎重使用

2、CPU占用过高 

首先运行一个java文件得到 .class文件,将其上传到VM虚拟机

/**
 * cpu 占用过高
 */
public class Stack3CPUFull {
    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();
    }
}
# 后台运行java程序
nohup java Stack3CPUFull &

# top命令查看CPU使用情况;定位哪个进程对cpu的占用过高
top 

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

# 根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
jstack 进程id

将线程ID 32665  转换为16进制数 7F99 即可定位占用CPU过高的线程,以及执行代码错误所在行数

3、线程私锁(程序长时间运行未得到返回结果)

/**
 * 线程死锁
 */
class A{};
class B{};
public class Stack4ThreadDeadLock {
    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();
    }
}

三、本地方法栈 Navite Method Stacks

native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法

常见 root类Object 中就有很多 navite修饰的方法

本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点以及都能抛出StackOverflowError和OutOfMemoryError异常

不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。 HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的

四、堆 Heap

JVM管理的最大的一块内存区域,存放着对象的实例,是线程共享区(通过 new 关键字,创建对象都会使用堆内存)

 堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”

JAVA堆的分类:

(1)从内存回收的角度上看,可分为新生代(Eden空间,From Survivor空间、To Survivor空间)及老年代(Tenured Gen)

  • 堆内存被划分为两块,一块的年轻代,另一块是老年代

  • 年轻代又分为Edensurvivor。他俩空间大小比例默认为8:2,

  • 幸存区又分为s0(From Spaces1(To Space。这两个空间大小是一模一样的,就是一对双胞胎,他俩是1:1的比例

年轻代又分为Eden和Survivor区。Survivor区由From Space和To Space组成。Eden区占大容量,Survivor两个区占小容量,默认比例是 8:1:1

老年代和年轻代默认比例是 2:1

(2)从内存分配的角度上看,为了解决分配内存时的线程安全性问题,线程共享的JAVA堆中可能划分出多个线程私有的分配缓冲区(TLAB)

JAVA堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

可通过参数 -Xmx -Xms 来指定运行时堆内存的大小,堆内存空间不足也会抛OutOfMemoryError异常

(一)堆内存溢出 OutOfMemoryError

/**
 * 堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */
public class Heap1OutOfMemoryError {
    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);
        }
    }
}

java.lang.OutOfMemoryError: Java heap space 

(二)堆内存诊断

/**
 * 堆内存
 */
public class Heap2Memory {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        //在此休眠30s 是为了 输出 jmap - heap 进程id 命令
        Thread.sleep(30000);

        byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb

        System.out.println("2...");
        Thread.sleep(30000);

        array = null;

        System.gc();
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}

1、jps 工具

查看当前系统中有哪些 java 进程

jps

2、jmap 工具

查看堆内存占用情况

分别在控制台输出了 1、2、3之后 分别执行 jmap - heap 进程id 命令

jmap - heap 进程id

可能会遇到报错:

Error attaching to process: sun.jvm.hotspot.runtime.VMVersionMismatchException: Supported versions are 25.291-b10. Target VM is 25.342-b07
sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.runtime.VMVersionMismatchException: Supported versions are 25.291-b10. Target VM is 25.342-b07

解决方案:

1、使用时要指定路径 

D:\JDK\jdk1.8.0_291\bin\jmap -heap 21352

或

C:\Users\zhangm\.jdks\corretto-1.8.0_342\bin\jmap

2、保持命令java -version的JDK,与程序运行的JDK是同一个 

3次输出如下: 

D:\Java\JavaProject\jvm-demo\myjvm>jps
23092 RemoteMavenServer36
9460 Jps
20664 Launcher
23640 Heap2Memory
6040 Launcher
13020 RemoteMavenServer36
15852

D:\Java\JavaProject\jvm-demo\myjvm>C:\Users\zhangm\.jdks\corretto-1.8.0_342\bin\jmap -heap 23640
Attaching to process ID 23640, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.342-b07

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

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 6377439232 (6082.0MB)
   NewSize                  = 133169152 (127.0MB)
   MaxNewSize               = 2125463552 (2027.0MB)
   OldSize                  = 267386880 (255.0MB)
   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 = 100663296 (96.0MB)
   used     = 6074064 (5.7926788330078125MB)
   free     = 94589232 (90.20732116699219MB)
   6.034040451049805% used
From Space:
   capacity = 16252928 (15.5MB)
   used     = 0 (0.0MB)
   free     = 16252928 (15.5MB)
   0.0% used
To Space:
   capacity = 16252928 (15.5MB)
   used     = 0 (0.0MB)
   free     = 16252928 (15.5MB)
   0.0% used
PS Old Generation
   capacity = 267386880 (255.0MB)
   used     = 0 (0.0MB)
   free     = 267386880 (255.0MB)
   0.0% used

1706 interned Strings occupying 175328 bytes.

D:\Java\JavaProject\jvm-demo\myjvm>C:\Users\zhangm\.jdks\corretto-1.8.0_342\bin\jmap -heap 23640
Attaching to process ID 23640, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.342-b07

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

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 6377439232 (6082.0MB)
   NewSize                  = 133169152 (127.0MB)
   MaxNewSize               = 2125463552 (2027.0MB)
   OldSize                  = 267386880 (255.0MB)
   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 = 100663296 (96.0MB)
   used     = 16559840 (15.792694091796875MB)
   free     = 84103456 (80.20730590820312MB)
   16.45072301228841% used
From Space:
   capacity = 16252928 (15.5MB)
   used     = 0 (0.0MB)
   free     = 16252928 (15.5MB)
   0.0% used
To Space:
   capacity = 16252928 (15.5MB)
   used     = 0 (0.0MB)
   free     = 16252928 (15.5MB)
   0.0% used
PS Old Generation
   capacity = 267386880 (255.0MB)
   used     = 0 (0.0MB)
   free     = 267386880 (255.0MB)
   0.0% used

1707 interned Strings occupying 175376 bytes.

D:\Java\JavaProject\jvm-demo\myjvm>C:\Users\zhangm\.jdks\corretto-1.8.0_342\bin\jmap -heap 23640
Attaching to process ID 23640, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.342-b07

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

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 6377439232 (6082.0MB)
   NewSize                  = 133169152 (127.0MB)
   MaxNewSize               = 2125463552 (2027.0MB)
   OldSize                  = 267386880 (255.0MB)
   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 = 100663296 (96.0MB)
   used     = 4026576 (3.8400421142578125MB)
   free     = 96636720 (92.15995788574219MB)
   4.000043869018555% used
From Space:
   capacity = 16252928 (15.5MB)
   used     = 0 (0.0MB)
   free     = 16252928 (15.5MB)
   0.0% used
To Space:
   capacity = 16252928 (15.5MB)
   used     = 0 (0.0MB)
   free     = 16252928 (15.5MB)
   0.0% used
PS Old Generation
   capacity = 267386880 (255.0MB)
   used     = 830432 (0.791961669921875MB)
   free     = 266556448 (254.20803833007812MB)
   0.31057320389093135% used

1691 interned Strings occupying 174248 bytes.

 

3、jconsole 工具

图形界面的,多功能的监测工具,可以连续监测

运行程序后,在控制台输出如下命令

jconsole

 

4、 jvisualvm工具

五、方法区 Method Area

官网地址

Chapter 2. The Structure of the Java Virtual Machine

方法区与 Java 堆一样,是各个线程共享内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

在JDK8以前,它的对方法区的实现叫做永久代,它就是使用了堆的一部分,作为方法区

而在JDK8以后,移除了永久代的实现,换了一种元空间的实现,元空间使用了操作系统的一部分(一些内存 )作为了方法区,而不再是堆的一部分

(一)方法区结构

 (二)方法区内存溢出

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

永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
-XX:MaxPermSize=8m

2、1.8 之后会导致元空间内存溢出

元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=8m

 (三)运行时常量池

1、常量池

常量池也可以称为Class常量池,每个.java文件经过编译后生成.class文件,每个.class文件里面都包含了一个常量池,这个常量池是在Class文件里面定义的,.java文件编译后就不会在变了,也不能修改,所以称之为静态常量池

常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息

2、运行时常量池

常量池是 *.class 文件中的。当类的字节码被加载到内存中后,他的常量池信息就会集中放入到一块内存,这块内存就称为运行时常量池,并且把里面的符号地址为真实地址

运行时常量池和class文件的常量池是一一对应的,它就是class文件的常量池来构建的。

运行时常量池中有两种类型,分别是symbolic references符号引用和static constants静态常量。

其中静态常量不需要后续解析,而符号引用需要进一步进行解析处理

静态常量,符号引用

String site="www.com"

字符串"www.com"可以看做是一个静态常量,因为它是不会变化的,是什么样的就展示什么样的。

而上面的字符串的名字“site”就是符号引用,需要在运行期间进行解析,因为site的值是可以变化的,我们不能在第一时间确定其真正的值,需要在动态运行中进行解析

我们编写一个 Hello World的基本java程序,运行编译成 .class字节码文件,在控制台中 切换到 .class文件所在目录,执行

javap -v HelloWorld

控制台输出如下:

D:\Java\JavaProject\jvm-demo\myjvm>cd out/production/myjvm/com/mycompany

D:\Java\JavaProject\jvm-demo\myjvm\out\production\myjvm\com\mycompany>javap -v HelloWorld
警告: 二进制文件HelloWorld包含com.mycompany.HelloWorld
Classfile /D:/Java/JavaProject/jvm-demo/myjvm/out/production/myjvm/com/mycompany/HelloWorld.class
  Last modified 2022-9-27; size 562 bytes
  MD5 checksum 56139c042931911e7cea84a4ece0987c
  Compiled from "HelloWorld.java"
public class com.mycompany.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            // com/mycompany/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               Lcom/mycompany/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               com/mycompany/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 com.mycompany.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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/mycompany/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 5: 0
        line 6: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

3、静态常量

运行时常量池中的静态常量是从class文件中的constant_pool构建的。可以分为两部分:

String常量数字常量

(1)String常量

String常量是对String对象的引用,是从class中的CONSTANT_String_info结构体构建的

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

string_index对应的class常量池的内容是一个CONSTANT_Utf8_info结构体 

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

CONSTANT_Utf8_info是要创建的String对象的变种UTF-8编码 

(2)数字常量

数字常量是从class文件中的CONSTANT_Integer_info, CONSTANT_Float_info, CONSTANT_Long_info和 CONSTANT_Double_info 构建

4、符号引用

符号引用也是从class中的constant_pool中构建的。

对class和interface的符号引用来自于CONSTANT_Class_info。

对class和interface中字段的引用来自于CONSTANT_Fieldref_info。

class中方法的引用来自于CONSTANT_Methodref_info。

interface中方法的引用来自于CONSTANT_InterfaceMethodref_info。

对方法句柄的引用来自于CONSTANT_MethodHandle_info。

对方法类型的引用来自于CONSTANT_MethodType_info。

对动态计算常量的符号引用来自于CONSTANT_MethodType_info。

对动态计算的call site的引用来自于CONSTANT_InvokeDynamic_info

(四)StringTable

1、StringTable的特性

常量池中的字符串仅是符号,第一次用到时才变为对象

利用串池的机制,来避免重复创建字符串对象

字符串变量拼接的原理是 StringBuilder (1.8)

字符串常量拼接的原理是编译期优化

intern 方法,主动将串池中还没有的字符串对象放入串池

【1】1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串 池中的对象的引用返回

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

JDK1.8

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class StringTable1 {
    public static void main(String[] args) {
        // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
        // ldc #2 会把 a 符号变为 "a" 字符串对象
        // ldc #3 会把 b 符号变为 "b" 字符串对象
        // ldc #4 会把 ab 符号变为 "ab" 字符串对象

        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
    }
}
D:\Java\JavaProject\jvm-demo\myjvm>cd out/production/myjvm/com/mycompany/stringtable
D:\Java\JavaProject\jvm-demo\myjvm\out\production\myjvm\com\mycompany\stringtable>javap -v Stringtable1
警告: 二进制文件Stringtable1包含com.mycompany.stringtable.StringTable1
Classfile /D:/Java/JavaProject/jvm-demo/myjvm/out/production/myjvm/com/mycompany/stringtable/Stringtable1.class
  Last modified 2022-9-27; size 1045 bytes
  MD5 checksum 92716b83ac90d0a1d2798c17959679f0
  Compiled from "StringTable1.java"
public class com.mycompany.stringtable.StringTable1
  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            // com/mycompany/stringtable/StringTable1
  #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               Lcom/mycompany/stringtable/StringTable1;
  #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               StringTable1.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               com/mycompany/stringtable/StringTable1
  #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 com.mycompany.stringtable.StringTable1();
    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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/mycompany/stringtable/StringTable1;

  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 5: 0
        line 6: 3
        line 8: 6
        line 9: 9
        line 10: 29
        line 12: 33
        line 13: 50
        line 14: 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
          stack = [ class java/io/PrintStream, int ]
        frame_type = 79 /* same_locals_1_stack_item */
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */

          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: "StringTable1.java"

 intern() 方法

1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

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

JDK1.8

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

        // 对比 s == x false
        //String x = "ab";
        String s = new String("a") + new String("b");

        // 堆  new String("a")   new String("b") new String("ab")
        //1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
        //1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
        String s2 = s.intern();

        // 对比 s == x true
        String x = "ab";

        System.out.println( s2 == x);//true
        System.out.println( s == x );
    }
}
/**
 * 字符串相关分析
 */
public class StringTable3 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b"; // 字符串拼接 ----> "ab";javac 在编译器的优化,结果在编译器已经确定的
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() ----> new String("ab")
        String s5 = "ab";
        /*
        JDK1.8:intern()方法将这个字符串对象尝试放入StringTable串池中;如果有不会放入,如果没有则放入串池中,会把串池对象返回
        JDK1.6:intern()方法将这个字符串对象尝试放入StringTable串池中;如果有不会放入,如果没有则会复制一个对象放入串池中,会把串池对象返回
        * */
        String s6 = s4.intern();

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



        // 堆中 new String("c") ; new String("d") ;new StringBuilder().append("c").append("d").toString() ----> new String("cd")
        String x2 = new String("c") + new String("d");

        //对比 x1 == x2 false
//        String x1 = "cd";
//        x2.intern();

        //调换最后两行代码位置(true) 对比 x1 == x2 true
        //JDK1.8:intern()方法将这个字符串对象尝试放入StringTable串池中;如果有不会放入,如果没有则放入串池中,会把串池对象返回
        x2.intern();
        String x1 = "cd";

        //JDK1.8 如果调换最后两行代码位置(true)
        System.out.println(x1 == x2);

    }
}

JDK1.6

public class StringTable2 {
    public static void main(String[] args) {

        // 串池中 ["ab", "a", "b"]
        //对比 s == x false
        //String x = "ab";
        String s = new String("a") + new String("b");

        // 堆  new String("a")   new String("b") new String("ab")
        //1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
        //1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
        String s2 = s.intern();
        // s 拷贝一份,放入串池(一个新对象;s指向的"ab"地址和s2指向的"ab"地址不是同一份)

        // 串池中 ["a", "b","ab"]
        //对比 s == x false
        String x = "ab";

        System.out.println( s2 == x);//true
        System.out.println( s == x );//false
    }
}
/**
 * 字符串相关分析
 */
public class StringTable3 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b"; // 字符串拼接 ----> "ab";javac 在编译器的优化,结果在编译器已经确定的
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() ----> new String("ab")
        String s5 = "ab";
        /*
        JDK1.8:intern()方法将这个字符串对象尝试放入StringTable串池中;如果有不会放入,如果没有则放入串池中,会把串池对象返回
        JDK1.6:intern()方法将这个字符串对象尝试放入StringTable串池中;如果有不会放入,如果没有则会复制一个对象放入串池中,会把串池对象返回
        * */
        String s6 = s4.intern();

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

        // 堆中 new String("c") ; new String("d") ;new StringBuilder().append("c").append("d").toString() ----> new String("cd")
        String x2 = new String("c") + new String("d");

        // 对比 x1 == x2 false
        String x1 = "cd";
        x2.intern();

        //如果调换最后两行代码位置 对比 x1 == x2 false
//        x2.intern();
//        String x1 = "cd";

        //JDK1.6 如果调换最后两行代码位置(false)
        System.out.println(x1 == x2);//false
    }
}

2、StringTable位置

jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中

JDK1.8 

/**
 * StringTable 位置
 * 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
 *      java.lang.OutOfMemoryError: Java heap space
 *      单独设置 -Xmx10m 报错
 *      Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
 *      java.lang.OutOfMemoryError: GC overhead limit exceeded ,超出了GC开销限制。科普了一下,这个是JDK6新添的错误类型。是发生在GC占用大量时间为释放很小空间的时候发生的,是一种保护机制。一般是因为堆太小,导致异常的原因:没有足够的内存。
 *      官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常
 * 在jdk6下设置 -XX:MaxPermSize=10m
 *      java.lang.OutOfMemoryError: PermGen space 
 */
public class StringTable4Location {
    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: GC overhead limit exceeded

超出了GC开销限制。科普了一下,这个是JDK6新添的错误类型。是发生在GC占用大量时间为释放很小空间的时候发生的,是一种保护机制。一般是因为堆太小,导致异常的原因:没有足够的内存 

官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常 

java.lang.OutOfMemoryError: Java heap space

JDK1.6 

java.lang.OutOfMemoryError: PermGen space 

3、StringTable 垃圾回收

/**
 * StringTable 垃圾回收
 * 在在JDK1.8下VM设置
 *      -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 *          -Xmx10m          设置虚拟机堆内存大小
 *          -XX:+PrintStringTableStatistics  打印字符串表的统计信息
 *          -XX:+PrintGCDetails -verbose:gc  打印垃圾回收详细信息参数
 * 在JDK1.6下VM设置
 *      -XX:MaxPermSize=10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 *          -XX:MaxPermSize=10m      设置虚拟机堆内存大小
 *          -XX:+PrintStringTableStatistics    打印字符串表的统计信息
 *          -XX:+PrintGCDetails -verbose:gc    打印垃圾回收详细信息参数
 */
public class StringTable5GC {
    // 字符串常量池中默认1688个字符串 
    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);
        }

    }
}

JDK1.8

(1)try中什么都不做

Heap
 PSYoungGen      total 2560K, used 727K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 11% used [0x00000000ffd00000,0x00000000ffd3bc80,0x00000000fff00000)
  from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 379K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 5% used [0x00000000ff600000,0x00000000ff65efb8,0x00000000ffd00000)
 Metaspace       used 3214K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 336K, capacity 392K, committed 512K, reserved 1048576K
Disconnected from the target VM, address: '127.0.0.1:62455', transport: 'socket'
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     13428 =    322272 bytes, avg  24.000
Number of literals      :     13428 =    605144 bytes, avg  45.066
Total footprint         :           =   1087504 bytes
Average bucket size     :     0.671
Variance of bucket size :     0.668
Std. dev. of bucket size:     0.817
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      1688 =     40512 bytes, avg  24.000
Number of literals      :      1688 =    174104 bytes, avg 103.142
Total footprint         :           =    694720 bytes
Average bucket size     :     0.028
Variance of bucket size :     0.028
Std. dev. of bucket size:     0.168
Maximum bucket size     :         3

字符串常量池中默认1688个字符串 

(2)try中循环100次,字符串常量数量 + 100

(3)try中循环100000次,触发了垃圾回收机制GC,字符串只有28000+个

JDK1.6

(1)try中什么都不做

0
Heap
 def new generation   total 4928K, used 1243K [0x10030000, 0x10580000, 0x15580000)
  eden space 4416K,  28% used [0x10030000, 0x10166d20, 0x10480000)
  from space 512K,   0% used [0x10480000, 0x10480000, 0x10500000)
  to   space 512K,   0% used [0x10500000, 0x10500000, 0x10580000)
 tenured generation   total 10944K, used 0K [0x15580000, 0x16030000, 0x20030000)
   the space 10944K,   0% used [0x15580000, 0x15580000, 0x15580200, 0x16030000)
 compacting perm gen  total 12288K, used 2537K [0x20030000, 0x20c30000, 0x20c30000)
   the space 12288K,  20% used [0x20030000, 0x202aa608, 0x202aa800, 0x20c30000)
No shared spaces configured.
Disconnected from the target VM, address: '127.0.0.1:63518', transport: 'socket'
SymbolTable statistics:
Number of buckets       :   20011
Average bucket size     :       0
Variance of bucket size :       0
Std. dev. of bucket size:       1
Maximum bucket size     :       6
StringTable statistics:
Number of buckets       :    1009
Average bucket size     :       1
Variance of bucket size :       1
Std. dev. of bucket size:       1
Maximum bucket size     :       7

(2)try中循环100000次,触发了垃圾回收机制GC,字符串只有28000+个

4、StringTable 性能调优

1、调整 -XX:StringTableSize=桶个数

设置桶大小(桶即数组索引下标元素);JDK1.6默认为1009,JDK1.7之后默认为60013,JDK1.8开始1009是可以设置的最小值

字符串常量池底层为HashTable(HashTable类实现一个哈希表,该哈希表将键映射到相应值;HashTable底层与HashMap原理相同;JDK1.6 数组+单向链表;JDK1.8 数组 + 单向链表 + 红黑树),合理增大常量池大小会解决Hash冲突问题

桶个数越大,查找该数组索引下标的链表或红黑树元素效率越高(该链表上元素越少,遍历时间越短)

拷贝一个含有近48万个字符串的文本文件,按照默认配置运行,花费336毫秒

/**
 * StringTableSize串池大小对性能的影响
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 *      -Xms500m    设置堆内存最小值
 *      -Xmx500m    设置堆内存最大值
 *      -XX:+PrintStringTableStatistics     字符串常量池统计信息
 *      -XX:StringTableSize=1009
 *          设置桶大小(桶即数组索引下标元素);JDK1.6默认为1009,JDK1.7之后默认为60013,JDK1.8开始1009是可以设置的最小值
 *          字符串常量池底层为HashTable(HashTable类实现一个哈希表,该哈希表将键映射到相应值;HashTable底层与HashMap原理相同;JDK1.6 数组+单向链表;JDK1.8 数组 + 单向链表 + 红黑树),合理增大常量池大小会解决Hash冲突问题
 */
public class StringTable6Optimize {
    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("myjvm/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("花费时间:" + (System.nanoTime() - start) / 1000000 + "毫秒");
        }
    }
}

设置VM参数,设置 桶大小 StringTableSize = 1009 最小值,花费8066毫秒

设置VM参数,设置 桶大小 StringTableSize = 200000,花费314毫秒

2、将一些字符串对象是否入池

public class StringTable7OptimizeIntern {
    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("myjvm/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("花费时间:" + (System.nanoTime() - start) / 1000000 + "毫秒");
            }
        }
        //System.in.read();
    }
}

六、直接内存 Direct Memory

Direct Memory 直接内存不属于JVM管理,是操作系统内存

(1)常见于NIO操作时,用于数据缓冲区(比如ByteBuffer使用的是直接内存)

(2)分配、回收成本较高,但读写性能高

(3)不受 JVM 内存回收管理

(一)文件(IO)读写过程

1、传统方式

Java本身不具备磁盘的读写能力,要想实现磁盘读写,必须调用操作系统提供的函数(即本地方法)在这里CPU的状态改变从用户态(Java)切换到内核态(system)【调用系统提供的函数后】

内存这边也会有一些相关的操作,当切换到内核态以后,他就可以由CPU的函数,去真正读取磁盘文件的内容,在内核状态时,读取内容后,他会在操作系统内存中划出一块缓冲区,其称之为系统缓冲区,磁盘的内容先读入到系统缓冲区中(分次进行读取),系统的缓冲区Java代码是不能够运行的,所以Java在堆内存中分配一块儿Java的缓冲区,即代码中的new byte[大小]。Java的代码要能访问到刚才读取的那个流中的数据,必须再从系统缓冲区的数据间接再读入到Java缓冲区,然后CPU的状态又切换到用户态了,然后再去调用Java的那个输出流的写入操作,就这样反复进行读写读写,把整个文件复制到目标位置。

由于有两块内存,两块缓冲区,即系统内存和Java堆内存都有缓冲区,那读取的时候必然涉及到这数据存两份,第一次先读到系统缓冲区还不行,因为Java代码访问不到他们,所以把系统缓冲区数据再读入到Java缓冲区中,这样就造成了一种不必要的数据的复制,效率因而不是很高

简言之:Java不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制

2、使用directBuffer

当ByteBuffer调用allocateDirect方法以后,会在操作系统这边划出一块缓冲区,即direct memory,这段区域与之前不一样的地方在于,这个操作系统划出来的内存,Java代码可以直接访问,即系统可以访问他,Java代码也可以访问他,即他对系统内存和Java代码都是可以共享的一段内存区域,这就是直接内存。

即磁盘文件读到直接内存后,Java代码直接访问直接内存,比传统代码少了一次缓冲区里的复制操作,所以速度得到了成倍的提高。这也是直接内存带来的好处,他适合做文件的这种io操作

简言之:直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率

/**
 * ByteBuffer 作用
 */
public class ByteAndDirectBuffer {
    static final String FROM = "D:\\文件\\SQL基本介绍.avi";
    static final String TO = "D:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用时:71.7764;53.8123;107.987
        directBuffer(); // directBuffer 用时:40.2163;30.3857;46.0605
    }

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

(二)内存溢出OOM

java.lang.OutOfMemoryError: Direct buffer memory

/**
 * 直接内存溢出
 * java.lang.OutOfMemoryError: Direct buffer memory
 */
public class Direct2OutOfMemory {
    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 对方法区的实现称为元空间
        * */
    }
}

(三)分配和回收原理

因为直接内存不属于JVM管理,因此使用jmap、jconsole、 jvisualvm等工具是无法监测内存使用情况,需要在“任务管理器”中查看

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

开始分配内存 

释放内存 

注:

在这里有一个误区,不要以为看到 调用了 

System.gc(); // 显式的垃圾回收,Full GC

直接内存被回收,就以为直接内存受JVM管理,其实底层是Unsafe对象起了作用

如果VM中设置了

-XX:+DisableExplicitGC

就会禁用 System.gc(),导致回收失效

由于Java垃圾回收没做,虽然byteBuffer被null了,但由于内存比较充足,所以他还存活着。既然他存活着,他所对应的那块儿直接内存(ByteBuffer.allocateDirect(-1Gb))也没有被回收掉,windows从任务管理器就能看得出(运行上述代码后,任务管理器就出现一个Java进程,这里分配了1G,所以那个进程占用内存也1G)

禁用System.gc()之后,会发现别的代码不受太大影响,但直接内存会受到影响,因为我们不能用显示的方法回收掉Bytebuffer,所以ByteBuffer只能等到真正的垃圾回收时,才会被清理,从而他所对应的那块儿直接内存也会被清理掉。

这就造成了直接内存可能占用较大,长时间得不到释放这样一个现象。直接内存使用情况比较多的时候,对直接内存的管理方式是,释放直接内存时,可以直接调用Unsafe对象的freeMemory方法,最终还是程序员手动的管理直接内存,所以推荐用Unsafe的相关方法

1、Unsafe 对象分配、回收内存

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

/**
 * 直接内存分配的底层原理:Unsafe
 */
public class Direct4Unsafe {
    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.out.println("分配完毕...");
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);
        System.out.println("释放...");
        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);
        }
    }
}

分配内存 

释放回收内存

2、Unsafe分配回收原理

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

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

ByteBuffer.allocateDirect(_1Gb);


public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>{
    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
}


class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer{
    DirectByteBuffer(int cap) {                   // package-private

        ......

        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        
        ......

        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;

    }

    
    private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }

        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
}

猜你喜欢

转载自blog.csdn.net/MinggeQingchun/article/details/127066302