https://blog.csdn.net/MinggeQingchun/article/details/126947384
JVM的内存结构大致分为五个部分,分别是程序计数器、虚拟机栈、本地方法栈、堆和方法区。除此之外,还有由堆中引用的JVM外的直接内存
The Java® Virtual Machine Specification
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

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

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

JDK = JRE + 开发工具集(例如Javac编译工具等)
JRE = JVM + Java SE标准类库
作用:记住下一条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 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行
多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行
每个线程运行时所需要的内存,称为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

虚拟机栈(Java Virtual Machine Stacks)是线程隔离的,每创建一个线程时就会对应创建一个Java栈,即每个线程都有自己独立的虚拟机栈
这个栈中又会对应包含多个栈帧,每调用一个方法时就会往栈中创建并压入一个栈帧,栈帧存储局部变量表、操作栈、动态链接、方法出口等信息,每一个方法从调用到最终返回结果的过程,就对应一个栈帧从入栈到出栈的过程
虚拟机栈是一个后入先出的数据结构,线程运行过程中,只有处于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法,当前活动帧栈始终是虚拟机栈的栈顶元素
局部变量表存放了编译期可知的各种基本数据类型和对象引用类型。通常我们所说的“栈内存”指的就是局部变量表这一部分。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,运行期间不会改变局部变量表的大小。
64位的long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个
如下,方法调用生成3个栈帧
栈内存并不涉及垃圾回收,栈内存的产生就是方法一次一次调用产生的栈帧内存,而栈帧内存在每次方法被调用后都会被弹出栈,自动就被回收掉,不需要垃圾回收管理
1、在固定大小的情况下,JVM会为每个线程的虚拟机栈分配一定的内存大小(-Xss参数),因此虚拟机栈能够容纳的栈帧数量是有限的,若栈帧不断进栈而不出栈,最终会导致当前线程虚拟机栈的内存空间耗尽,会抛出StackOverflowError异常(栈帧过多,栈帧过大都会导致栈内存溢出)
2、在动态扩展的情况下,当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时,就会抛出OutOfMemoryError异常
运行如下代码
- /**
- * 栈内存溢出 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

可配置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实现会有所不同,将来可能会不被支持,需要慎重使用
首先运行一个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过高的线程,以及执行代码错误所在行数

- /**
- * 线程死锁
- */
- 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();
- }
- }

native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法
常见 root类Object 中就有很多 navite修饰的方法
本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点以及都能抛出StackOverflowError和OutOfMemoryError异常
不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。 HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的
JVM管理的最大的一块内存区域,存放着对象的实例,是线程共享区(通过 new 关键字,创建对象都会使用堆内存)
堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”

JAVA堆的分类:
(1)从内存回收的角度上看,可分为新生代(Eden空间,From Survivor空间、To Survivor空间)及老年代(Tenured Gen)
堆内存被划分为
两块,一块的年轻代,另一块是老年代。年轻代又分为
Eden和survivor。他俩空间大小比例默认为8:2,幸存区又分为
s0(From Space)和s1(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异常

- /**
- * 堆内存溢出 java.lang.OutOfMemoryError: Java heap space
- * -Xmx8m
- */
- public class Heap1OutOfMemoryError {
- public static void main(String[] args) {
- int i = 0;
- try {
- List
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);
- }
- }
查看当前系统中有哪些 java 进程
jps
查看堆内存占用情况
分别在控制台输出了 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.

图形界面的,多功能的监测工具,可以连续监测
运行程序后,在控制台输出如下命令
jconsole





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
常量池也可以称为Class常量池,每个.java文件经过编译后生成.class文件,每个.class文件里面都包含了一个常量池,这个常量池是在Class文件里面定义的,.java文件编译后就不会在变了,也不能修改,所以称之为静态常量池
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息
常量池是 *.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."
":()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
- #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 // "
":()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."
":()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"

运行时常量池中的静态常量是从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 构建
符号引用也是从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
常量池中的字符串仅是符号,第一次用到时才变为对象
利用串池的机制,来避免重复创建字符串对象
字符串变量拼接的原理是 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."
":()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."
":()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
- #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 // "
":()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."
":()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."
":()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"
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
- }
- }
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
list = new ArrayList(); - 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

- /**
- * 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+个

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
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 直接内存不属于JVM管理,是操作系统内存
(1)常见于NIO操作时,用于数据缓冲区(比如ByteBuffer使用的是直接内存)
(2)分配、回收成本较高,但读写性能高
(3)不受 JVM 内存回收管理

Java本身不具备磁盘的读写能力,要想实现磁盘读写,必须调用操作系统提供的函数(即本地方法)在这里CPU的状态改变从用户态(Java)切换到内核态(system)【调用系统提供的函数后】
内存这边也会有一些相关的操作,当切换到内核态以后,他就可以由CPU的函数,去真正读取磁盘文件的内容,在内核状态时,读取内容后,他会在操作系统内存中划出一块缓冲区,其称之为系统缓冲区,磁盘的内容先读入到系统缓冲区中(分次进行读取),系统的缓冲区Java代码是不能够运行的,所以Java在堆内存中分配一块儿Java的缓冲区,即代码中的new byte[大小]。Java的代码要能访问到刚才读取的那个流中的数据,必须再从系统缓冲区的数据间接再读入到Java缓冲区,然后CPU的状态又切换到用户态了,然后再去调用Java的那个输出流的写入操作,就这样反复进行读写读写,把整个文件复制到目标位置。
由于有两块内存,两块缓冲区,即系统内存和Java堆内存都有缓冲区,那读取的时候必然涉及到这数据存两份,第一次先读到系统缓冲区还不行,因为Java代码访问不到他们,所以把系统缓冲区数据再读入到Java缓冲区中,这样就造成了一种不必要的数据的复制,效率因而不是很高
简言之:Java不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制

当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);
- }
- }
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
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的相关方法

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);
- }
- }
- }
分配内存

释放回收内存
Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
ByteBuer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuer 对象,一但ByteBuer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory方法 来释放直接内存
- ByteBuffer.allocateDirect(_1Gb);
-
-
- public abstract class ByteBuffer extends Buffer implements Comparable
{ - 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);
- }
- }