• 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情况的区域,因为程序计数器是由虚拟机内部维护的,不需要开发者进行操作 

    1. 0: getstatic #20 // PrintStream out = System.out;
    2. 3: astore_1 // --
    3. 4: aload_1 // out.println(1);
    4. 5: iconst_1 // --
    5. 6: invokevirtual #26 // --
    6. 9: aload_1 // out.println(2);
    7. 10: iconst_2 // --
    8. 11: invokevirtual #26 // --
    9. 14: aload_1 // out.println(3);
    10. 15: iconst_3 // --
    11. 16: invokevirtual #26 // --
    12. 19: aload_1 // out.println(4);
    13. 20: iconst_4 // --
    14. 21: invokevirtual #26 // --
    15. 24: aload_1 // out.println(5);
    16. 25: iconst_5 // --
    17. 26: invokevirtual #26 // --
    18. 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、递归调用方法自身

    运行如下代码

    1. /**
    2. * 栈内存溢出 java.lang.StackOverflowError
    3. * -Xss256k
    4. */
    5. public class Stack2StackOverflowError {
    6. private static int count;
    7. public static void main(String[] args) {
    8. try {
    9. method1();
    10. } catch (Throwable e) {
    11. e.printStackTrace();
    12. System.out.println(count);
    13. }
    14. }
    15. private static void method1() {
    16. count++;
    17. method1();
    18. }
    19. }

    报错如下:

    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虚拟机

    1. /**
    2. * cpu 占用过高
    3. */
    4. public class Stack3CPUFull {
    5. public static void main(String[] args) {
    6. new Thread(null, () -> {
    7. System.out.println("1...");
    8. while(true) {
    9. }
    10. }, "thread1").start();
    11. new Thread(null, () -> {
    12. System.out.println("2...");
    13. try {
    14. Thread.sleep(1000000L);
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. }
    18. }, "thread2").start();
    19. new Thread(null, () -> {
    20. System.out.println("3...");
    21. try {
    22. Thread.sleep(1000000L);
    23. } catch (InterruptedException e) {
    24. e.printStackTrace();
    25. }
    26. }, "thread3").start();
    27. }
    28. }
    1. # 后台运行java程序
    2. nohup java Stack3CPUFull &
    3. # top命令查看CPU使用情况;定位哪个进程对cpu的占用过高
    4. top
    5. # ps命令进一步定位是哪个线程引起的cpu占用过高
    6. ps H -eo pid,tid,%cpu | grep 进程id
    7. # 根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
    8. jstack 进程id

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

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

    1. /**
    2. * 线程死锁
    3. */
    4. class A{};
    5. class B{};
    6. public class Stack4ThreadDeadLock {
    7. static A a = new A();
    8. static B b = new B();
    9. public static void main(String[] args) throws InterruptedException {
    10. new Thread(()->{
    11. synchronized (a) {
    12. try {
    13. Thread.sleep(2000);
    14. } catch (InterruptedException e) {
    15. e.printStackTrace();
    16. }
    17. synchronized (b) {
    18. System.out.println("我获得了 a 和 b");
    19. }
    20. }
    21. }).start();
    22. Thread.sleep(1000);
    23. new Thread(()->{
    24. synchronized (b) {
    25. synchronized (a) {
    26. System.out.println("我获得了 a 和 b");
    27. }
    28. }
    29. }).start();
    30. }
    31. }

    三、本地方法栈 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

    1. /**
    2. * 堆内存溢出 java.lang.OutOfMemoryError: Java heap space
    3. * -Xmx8m
    4. */
    5. public class Heap1OutOfMemoryError {
    6. public static void main(String[] args) {
    7. int i = 0;
    8. try {
    9. List list = new ArrayList<>();
    10. String a = "hello";
    11. while (true) {
    12. list.add(a); // hello, hellohello, hellohellohellohello ...
    13. a = a + a; // hellohellohellohello
    14. i++;
    15. }
    16. } catch (Throwable e) {
    17. e.printStackTrace();
    18. System.out.println(i);
    19. }
    20. }
    21. }

    java.lang.OutOfMemoryError: Java heap space 

    (二)堆内存诊断

    1. /**
    2. * 堆内存
    3. */
    4. public class Heap2Memory {
    5. public static void main(String[] args) throws InterruptedException {
    6. System.out.println("1...");
    7. //在此休眠30s 是为了 输出 jmap - heap 进程id 命令
    8. Thread.sleep(30000);
    9. byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
    10. System.out.println("2...");
    11. Thread.sleep(30000);
    12. array = null;
    13. System.gc();
    14. System.out.println("3...");
    15. Thread.sleep(1000000L);
    16. }
    17. }

    1、jps 工具

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

    jps

    2、jmap 工具

    查看堆内存占用情况

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

    jmap - heap 进程id

    可能会遇到报错:

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

    解决方案:

    1、使用时要指定路径 

    1. D:\JDK\jdk1.8.0_291\bin\jmap -heap 21352
    2. C:\Users\zhangm\.jdks\corretto-1.8.0_342\bin\jmap

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

    3次输出如下: 

    1. D:\Java\JavaProject\jvm-demo\myjvm>jps
    2. 23092 RemoteMavenServer36
    3. 9460 Jps
    4. 20664 Launcher
    5. 23640 Heap2Memory
    6. 6040 Launcher
    7. 13020 RemoteMavenServer36
    8. 15852
    9. D:\Java\JavaProject\jvm-demo\myjvm>C:\Users\zhangm\.jdks\corretto-1.8.0_342\bin\jmap -heap 23640
    10. Attaching to process ID 23640, please wait...
    11. Debugger attached successfully.
    12. Server compiler detected.
    13. JVM version is 25.342-b07
    14. using thread-local object allocation.
    15. Parallel GC with 8 thread(s)
    16. Heap Configuration:
    17. MinHeapFreeRatio = 0
    18. MaxHeapFreeRatio = 100
    19. MaxHeapSize = 6377439232 (6082.0MB)
    20. NewSize = 133169152 (127.0MB)
    21. MaxNewSize = 2125463552 (2027.0MB)
    22. OldSize = 267386880 (255.0MB)
    23. NewRatio = 2
    24. SurvivorRatio = 8
    25. MetaspaceSize = 21807104 (20.796875MB)
    26. CompressedClassSpaceSize = 1073741824 (1024.0MB)
    27. MaxMetaspaceSize = 17592186044415 MB
    28. G1HeapRegionSize = 0 (0.0MB)
    29. Heap Usage:
    30. PS Young Generation
    31. Eden Space:
    32. capacity = 100663296 (96.0MB)
    33. used = 6074064 (5.7926788330078125MB)
    34. free = 94589232 (90.20732116699219MB)
    35. 6.034040451049805% used
    36. From Space:
    37. capacity = 16252928 (15.5MB)
    38. used = 0 (0.0MB)
    39. free = 16252928 (15.5MB)
    40. 0.0% used
    41. To Space:
    42. capacity = 16252928 (15.5MB)
    43. used = 0 (0.0MB)
    44. free = 16252928 (15.5MB)
    45. 0.0% used
    46. PS Old Generation
    47. capacity = 267386880 (255.0MB)
    48. used = 0 (0.0MB)
    49. free = 267386880 (255.0MB)
    50. 0.0% used
    51. 1706 interned Strings occupying 175328 bytes.
    52. D:\Java\JavaProject\jvm-demo\myjvm>C:\Users\zhangm\.jdks\corretto-1.8.0_342\bin\jmap -heap 23640
    53. Attaching to process ID 23640, please wait...
    54. Debugger attached successfully.
    55. Server compiler detected.
    56. JVM version is 25.342-b07
    57. using thread-local object allocation.
    58. Parallel GC with 8 thread(s)
    59. Heap Configuration:
    60. MinHeapFreeRatio = 0
    61. MaxHeapFreeRatio = 100
    62. MaxHeapSize = 6377439232 (6082.0MB)
    63. NewSize = 133169152 (127.0MB)
    64. MaxNewSize = 2125463552 (2027.0MB)
    65. OldSize = 267386880 (255.0MB)
    66. NewRatio = 2
    67. SurvivorRatio = 8
    68. MetaspaceSize = 21807104 (20.796875MB)
    69. CompressedClassSpaceSize = 1073741824 (1024.0MB)
    70. MaxMetaspaceSize = 17592186044415 MB
    71. G1HeapRegionSize = 0 (0.0MB)
    72. Heap Usage:
    73. PS Young Generation
    74. Eden Space:
    75. capacity = 100663296 (96.0MB)
    76. used = 16559840 (15.792694091796875MB)
    77. free = 84103456 (80.20730590820312MB)
    78. 16.45072301228841% used
    79. From Space:
    80. capacity = 16252928 (15.5MB)
    81. used = 0 (0.0MB)
    82. free = 16252928 (15.5MB)
    83. 0.0% used
    84. To Space:
    85. capacity = 16252928 (15.5MB)
    86. used = 0 (0.0MB)
    87. free = 16252928 (15.5MB)
    88. 0.0% used
    89. PS Old Generation
    90. capacity = 267386880 (255.0MB)
    91. used = 0 (0.0MB)
    92. free = 267386880 (255.0MB)
    93. 0.0% used
    94. 1707 interned Strings occupying 175376 bytes.
    95. D:\Java\JavaProject\jvm-demo\myjvm>C:\Users\zhangm\.jdks\corretto-1.8.0_342\bin\jmap -heap 23640
    96. Attaching to process ID 23640, please wait...
    97. Debugger attached successfully.
    98. Server compiler detected.
    99. JVM version is 25.342-b07
    100. using thread-local object allocation.
    101. Parallel GC with 8 thread(s)
    102. Heap Configuration:
    103. MinHeapFreeRatio = 0
    104. MaxHeapFreeRatio = 100
    105. MaxHeapSize = 6377439232 (6082.0MB)
    106. NewSize = 133169152 (127.0MB)
    107. MaxNewSize = 2125463552 (2027.0MB)
    108. OldSize = 267386880 (255.0MB)
    109. NewRatio = 2
    110. SurvivorRatio = 8
    111. MetaspaceSize = 21807104 (20.796875MB)
    112. CompressedClassSpaceSize = 1073741824 (1024.0MB)
    113. MaxMetaspaceSize = 17592186044415 MB
    114. G1HeapRegionSize = 0 (0.0MB)
    115. Heap Usage:
    116. PS Young Generation
    117. Eden Space:
    118. capacity = 100663296 (96.0MB)
    119. used = 4026576 (3.8400421142578125MB)
    120. free = 96636720 (92.15995788574219MB)
    121. 4.000043869018555% used
    122. From Space:
    123. capacity = 16252928 (15.5MB)
    124. used = 0 (0.0MB)
    125. free = 16252928 (15.5MB)
    126. 0.0% used
    127. To Space:
    128. capacity = 16252928 (15.5MB)
    129. used = 0 (0.0MB)
    130. free = 16252928 (15.5MB)
    131. 0.0% used
    132. PS Old Generation
    133. capacity = 267386880 (255.0MB)
    134. used = 830432 (0.791961669921875MB)
    135. free = 266556448 (254.20803833007812MB)
    136. 0.31057320389093135% used
    137. 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
    

    控制台输出如下:

    1. D:\Java\JavaProject\jvm-demo\myjvm>cd out/production/myjvm/com/mycompany
    2. D:\Java\JavaProject\jvm-demo\myjvm\out\production\myjvm\com\mycompany>javap -v HelloWorld
    3. 警告: 二进制文件HelloWorld包含com.mycompany.HelloWorld
    4. Classfile /D:/Java/JavaProject/jvm-demo/myjvm/out/production/myjvm/com/mycompany/HelloWorld.class
    5. Last modified 2022-9-27; size 562 bytes
    6. MD5 checksum 56139c042931911e7cea84a4ece0987c
    7. Compiled from "HelloWorld.java"
    8. public class com.mycompany.HelloWorld
    9. minor version: 0
    10. major version: 52
    11. flags: ACC_PUBLIC, ACC_SUPER
    12. Constant pool:
    13. #1 = Methodref #6.#20 // java/lang/Object."":()V
    14. #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
    15. #3 = String #23 // Hello World!
    16. #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
    17. #5 = Class #26 // com/mycompany/HelloWorld
    18. #6 = Class #27 // java/lang/Object
    19. #7 = Utf8
    20. #8 = Utf8 ()V
    21. #9 = Utf8 Code
    22. #10 = Utf8 LineNumberTable
    23. #11 = Utf8 LocalVariableTable
    24. #12 = Utf8 this
    25. #13 = Utf8 Lcom/mycompany/HelloWorld;
    26. #14 = Utf8 main
    27. #15 = Utf8 ([Ljava/lang/String;)V
    28. #16 = Utf8 args
    29. #17 = Utf8 [Ljava/lang/String;
    30. #18 = Utf8 SourceFile
    31. #19 = Utf8 HelloWorld.java
    32. #20 = NameAndType #7:#8 // "":()V
    33. #21 = Class #28 // java/lang/System
    34. #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
    35. #23 = Utf8 Hello World!
    36. #24 = Class #31 // java/io/PrintStream
    37. #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
    38. #26 = Utf8 com/mycompany/HelloWorld
    39. #27 = Utf8 java/lang/Object
    40. #28 = Utf8 java/lang/System
    41. #29 = Utf8 out
    42. #30 = Utf8 Ljava/io/PrintStream;
    43. #31 = Utf8 java/io/PrintStream
    44. #32 = Utf8 println
    45. #33 = Utf8 (Ljava/lang/String;)V
    46. {
    47. public com.mycompany.HelloWorld();
    48. descriptor: ()V
    49. flags: ACC_PUBLIC
    50. Code:
    51. stack=1, locals=1, args_size=1
    52. 0: aload_0
    53. 1: invokespecial #1 // Method java/lang/Object."":()V
    54. 4: return
    55. LineNumberTable:
    56. line 3: 0
    57. LocalVariableTable:
    58. Start Length Slot Name Signature
    59. 0 5 0 this Lcom/mycompany/HelloWorld;
    60. public static void main(java.lang.String[]);
    61. descriptor: ([Ljava/lang/String;)V
    62. flags: ACC_PUBLIC, ACC_STATIC
    63. Code:
    64. stack=2, locals=1, args_size=1
    65. 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
    66. 3: ldc #3 // String Hello World!
    67. 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    68. 8: return
    69. LineNumberTable:
    70. line 5: 0
    71. line 6: 8
    72. LocalVariableTable:
    73. Start Length Slot Name Signature
    74. 0 9 0 args [Ljava/lang/String;
    75. }
    76. SourceFile: "HelloWorld.java"

    3、静态常量

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

    String常量数字常量

    (1)String常量

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

    1. CONSTANT_String_info {
    2. u1 tag;
    3. u2 string_index;
    4. }

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

    1. CONSTANT_Utf8_info {
    2. u1 tag;
    3. u2 length;
    4. u1 bytes[length];
    5. }

    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

    1. // StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
    2. public class StringTable1 {
    3. public static void main(String[] args) {
    4. // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    5. // ldc #2 会把 a 符号变为 "a" 字符串对象
    6. // ldc #3 会把 b 符号变为 "b" 字符串对象
    7. // ldc #4 会把 ab 符号变为 "ab" 字符串对象
    8. String s1 = "a"; // 懒惰的
    9. String s2 = "b";
    10. String s3 = "ab";
    11. String s4 = s1 + s2;// new StringBuilder().append("a").append("b").toString() ----> new String("ab") 堆内存中新对象
    12. String s5 = "a" + "b";// javac 在编译期间的优化,结果已经在编译期确定为ab(单纯的字符串拼接)
    13. System.out.println(s3 == s4);//false
    14. System.out.println(s3 == s5);//true
    15. }
    16. }
    1. D:\Java\JavaProject\jvm-demo\myjvm>cd out/production/myjvm/com/mycompany/stringtable
    2. D:\Java\JavaProject\jvm-demo\myjvm\out\production\myjvm\com\mycompany\stringtable>javap -v Stringtable1
    3. 警告: 二进制文件Stringtable1包含com.mycompany.stringtable.StringTable1
    4. Classfile /D:/Java/JavaProject/jvm-demo/myjvm/out/production/myjvm/com/mycompany/stringtable/Stringtable1.class
    5. Last modified 2022-9-27; size 1045 bytes
    6. MD5 checksum 92716b83ac90d0a1d2798c17959679f0
    7. Compiled from "StringTable1.java"
    8. public class com.mycompany.stringtable.StringTable1
    9. minor version: 0
    10. major version: 52
    11. flags: ACC_PUBLIC, ACC_SUPER
    12. Constant pool:
    13. #1 = Methodref #12.#36 // java/lang/Object."":()V
    14. #2 = String #37 // a
    15. #3 = String #38 // b
    16. #4 = String #39 // ab
    17. #5 = Class #40 // java/lang/StringBuilder
    18. #6 = Methodref #5.#36 // java/lang/StringBuilder."":()V
    19. #7 = Methodref #5.#41 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    20. #8 = Methodref #5.#42 // java/lang/StringBuilder.toString:()Ljava/lang/String;
    21. #9 = Fieldref #43.#44 // java/lang/System.out:Ljava/io/PrintStream;
    22. #10 = Methodref #45.#46 // java/io/PrintStream.println:(Z)V
    23. #11 = Class #47 // com/mycompany/stringtable/StringTable1
    24. #12 = Class #48 // java/lang/Object
    25. #13 = Utf8
    26. #14 = Utf8 ()V
    27. #15 = Utf8 Code
    28. #16 = Utf8 LineNumberTable
    29. #17 = Utf8 LocalVariableTable
    30. #18 = Utf8 this
    31. #19 = Utf8 Lcom/mycompany/stringtable/StringTable1;
    32. #20 = Utf8 main
    33. #21 = Utf8 ([Ljava/lang/String;)V
    34. #22 = Utf8 args
    35. #23 = Utf8 [Ljava/lang/String;
    36. #24 = Utf8 s1
    37. #25 = Utf8 Ljava/lang/String;
    38. #26 = Utf8 s2
    39. #27 = Utf8 s3
    40. #28 = Utf8 s4
    41. #29 = Utf8 s5
    42. #30 = Utf8 StackMapTable
    43. #31 = Class #23 // "[Ljava/lang/String;"
    44. #32 = Class #49 // java/lang/String
    45. #33 = Class #50 // java/io/PrintStream
    46. #34 = Utf8 SourceFile
    47. #35 = Utf8 StringTable1.java
    48. #36 = NameAndType #13:#14 // "":()V
    49. #37 = Utf8 a
    50. #38 = Utf8 b
    51. #39 = Utf8 ab
    52. #40 = Utf8 java/lang/StringBuilder
    53. #41 = NameAndType #51:#52 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    54. #42 = NameAndType #53:#54 // toString:()Ljava/lang/String;
    55. #43 = Class #55 // java/lang/System
    56. #44 = NameAndType #56:#57 // out:Ljava/io/PrintStream;
    57. #45 = Class #50 // java/io/PrintStream
    58. #46 = NameAndType #58:#59 // println:(Z)V
    59. #47 = Utf8 com/mycompany/stringtable/StringTable1
    60. #48 = Utf8 java/lang/Object
    61. #49 = Utf8 java/lang/String
    62. #50 = Utf8 java/io/PrintStream
    63. #51 = Utf8 append
    64. #52 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
    65. #53 = Utf8 toString
    66. #54 = Utf8 ()Ljava/lang/String;
    67. #55 = Utf8 java/lang/System
    68. #56 = Utf8 out
    69. #57 = Utf8 Ljava/io/PrintStream;
    70. #58 = Utf8 println
    71. #59 = Utf8 (Z)V
    72. {
    73. public com.mycompany.stringtable.StringTable1();
    74. descriptor: ()V
    75. flags: ACC_PUBLIC
    76. Code:
    77. stack=1, locals=1, args_size=1
    78. 0: aload_0
    79. 1: invokespecial #1 // Method java/lang/Object."":()V
    80. 4: return
    81. LineNumberTable:
    82. line 3: 0
    83. LocalVariableTable:
    84. Start Length Slot Name Signature
    85. 0 5 0 this Lcom/mycompany/stringtable/StringTable1;
    86. public static void main(java.lang.String[]);
    87. descriptor: ([Ljava/lang/String;)V
    88. flags: ACC_PUBLIC, ACC_STATIC
    89. Code:
    90. stack=3, locals=6, args_size=1
    91. 0: ldc #2 // String a
    92. 2: astore_1
    93. 3: ldc #3 // String b
    94. 5: astore_2
    95. 6: ldc #4 // String ab
    96. 8: astore_3
    97. 9: new #5 // class java/lang/StringBuilder
    98. 12: dup
    99. 13: invokespecial #6 // Method java/lang/StringBuilder."":()V
    100. 16: aload_1
    101. 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    102. 20: aload_2
    103. 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    104. 24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    105. 27: astore 4
    106. 29: ldc #4 // String ab
    107. 31: astore 5
    108. 33: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
    109. 36: aload_3
    110. 37: aload 4
    111. 39: if_acmpne 46
    112. 42: iconst_1
    113. 43: goto 47
    114. 46: iconst_0
    115. 47: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
    116. 50: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
    117. 53: aload_3
    118. 54: aload 5
    119. 56: if_acmpne 63
    120. 59: iconst_1
    121. 60: goto 64
    122. 63: iconst_0
    123. 64: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
    124. 67: return
    125. LineNumberTable:
    126. line 5: 0
    127. line 6: 3
    128. line 8: 6
    129. line 9: 9
    130. line 10: 29
    131. line 12: 33
    132. line 13: 50
    133. line 14: 67
    134. LocalVariableTable:
    135. Start Length Slot Name Signature
    136. 0 68 0 args [Ljava/lang/String;
    137. 3 65 1 s1 Ljava/lang/String;
    138. 6 62 2 s2 Ljava/lang/String;
    139. 9 59 3 s3 Ljava/lang/String;
    140. 29 39 4 s4 Ljava/lang/String;
    141. 33 35 5 s5 Ljava/lang/String;
    142. StackMapTable: number_of_entries = 4
    143. frame_type = 255 /* full_frame */
    144. offset_delta = 46
    145. locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String
    146. , class java/lang/String ]
    147. stack = [ class java/io/PrintStream ]
    148. frame_type = 255 /* full_frame */
    149. offset_delta = 0
    150. stack = [ class java/io/PrintStream, int ]
    151. frame_type = 79 /* same_locals_1_stack_item */
    152. stack = [ class java/io/PrintStream ]
    153. frame_type = 255 /* full_frame */
    154. 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 ]
    155. stack = [ class java/io/PrintStream, int ]
    156. }
    157. SourceFile: "StringTable1.java"

     intern() 方法

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

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

    JDK1.8

    1. public class StringTable2 {
    2. public static void main(String[] args) {
    3. // ["ab", "a", "b"]
    4. // 对比 s == x false
    5. //String x = "ab";
    6. String s = new String("a") + new String("b");
    7. // 堆 new String("a") new String("b") new String("ab")
    8. //1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
    9. //1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
    10. String s2 = s.intern();
    11. // 对比 s == x true
    12. String x = "ab";
    13. System.out.println( s2 == x);//true
    14. System.out.println( s == x );
    15. }
    16. }
    1. /**
    2. * 字符串相关分析
    3. */
    4. public class StringTable3 {
    5. public static void main(String[] args) {
    6. String s1 = "a";
    7. String s2 = "b";
    8. String s3 = "a" + "b"; // 字符串拼接 ----> "ab";javac 在编译器的优化,结果在编译器已经确定的
    9. String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() ----> new String("ab")
    10. String s5 = "ab";
    11. /*
    12. JDK1.8:intern()方法将这个字符串对象尝试放入StringTable串池中;如果有不会放入,如果没有则放入串池中,会把串池对象返回
    13. JDK1.6:intern()方法将这个字符串对象尝试放入StringTable串池中;如果有不会放入,如果没有则会复制一个对象放入串池中,会把串池对象返回
    14. * */
    15. String s6 = s4.intern();
    16. System.out.println(s3 == s4);//false
    17. System.out.println(s3 == s5);//true
    18. System.out.println(s3 == s6);//true
    19. // 堆中 new String("c") ; new String("d") ;new StringBuilder().append("c").append("d").toString() ----> new String("cd")
    20. String x2 = new String("c") + new String("d");
    21. //对比 x1 == x2 false
    22. // String x1 = "cd";
    23. // x2.intern();
    24. //调换最后两行代码位置(true) 对比 x1 == x2 true
    25. //JDK1.8:intern()方法将这个字符串对象尝试放入StringTable串池中;如果有不会放入,如果没有则放入串池中,会把串池对象返回
    26. x2.intern();
    27. String x1 = "cd";
    28. //JDK1.8 如果调换最后两行代码位置(true)
    29. System.out.println(x1 == x2);
    30. }
    31. }

    JDK1.6

    1. public class StringTable2 {
    2. public static void main(String[] args) {
    3. // 串池中 ["ab", "a", "b"]
    4. //对比 s == x false
    5. //String x = "ab";
    6. String s = new String("a") + new String("b");
    7. // 堆 new String("a") new String("b") new String("ab")
    8. //1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
    9. //1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
    10. String s2 = s.intern();
    11. // s 拷贝一份,放入串池(一个新对象;s指向的"ab"地址和s2指向的"ab"地址不是同一份)
    12. // 串池中 ["a", "b","ab"]
    13. //对比 s == x false
    14. String x = "ab";
    15. System.out.println( s2 == x);//true
    16. System.out.println( s == x );//false
    17. }
    18. }
    1. /**
    2. * 字符串相关分析
    3. */
    4. public class StringTable3 {
    5. public static void main(String[] args) {
    6. String s1 = "a";
    7. String s2 = "b";
    8. String s3 = "a" + "b"; // 字符串拼接 ----> "ab";javac 在编译器的优化,结果在编译器已经确定的
    9. String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() ----> new String("ab")
    10. String s5 = "ab";
    11. /*
    12. JDK1.8:intern()方法将这个字符串对象尝试放入StringTable串池中;如果有不会放入,如果没有则放入串池中,会把串池对象返回
    13. JDK1.6:intern()方法将这个字符串对象尝试放入StringTable串池中;如果有不会放入,如果没有则会复制一个对象放入串池中,会把串池对象返回
    14. * */
    15. String s6 = s4.intern();
    16. System.out.println(s3 == s4);//false
    17. System.out.println(s3 == s5);//true
    18. System.out.println(s3 == s6);//true
    19. // 堆中 new String("c") ; new String("d") ;new StringBuilder().append("c").append("d").toString() ----> new String("cd")
    20. String x2 = new String("c") + new String("d");
    21. // 对比 x1 == x2 false
    22. String x1 = "cd";
    23. x2.intern();
    24. //如果调换最后两行代码位置 对比 x1 == x2 false
    25. // x2.intern();
    26. // String x1 = "cd";
    27. //JDK1.6 如果调换最后两行代码位置(false)
    28. System.out.println(x1 == x2);//false
    29. }
    30. }

    2、StringTable位置

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

    JDK1.8 

    1. /**
    2. * StringTable 位置
    3. * 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
    4. * java.lang.OutOfMemoryError: Java heap space
    5. * 单独设置 -Xmx10m 报错
    6. * Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    7. * java.lang.OutOfMemoryError: GC overhead limit exceeded ,超出了GC开销限制。科普了一下,这个是JDK6新添的错误类型。是发生在GC占用大量时间为释放很小空间的时候发生的,是一种保护机制。一般是因为堆太小,导致异常的原因:没有足够的内存。
    8. * 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常
    9. * 在jdk6下设置 -XX:MaxPermSize=10m
    10. * java.lang.OutOfMemoryError: PermGen space 
    11. */
    12. public class StringTable4Location {
    13. public static void main(String[] args) throws InterruptedException {
    14. List list = new ArrayList();
    15. int i = 0;
    16. try {
    17. for (int j = 0; j < 260000; j++) {
    18. list.add(String.valueOf(j).intern());
    19. i++;
    20. }
    21. } catch (Throwable e) {
    22. e.printStackTrace();
    23. } finally {
    24. System.out.println(i);
    25. }
    26. }
    27. }

    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 垃圾回收

    1. /**
    2. * StringTable 垃圾回收
    3. * 在在JDK1.8下VM设置
    4. * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
    5. * -Xmx10m 设置虚拟机堆内存大小
    6. * -XX:+PrintStringTableStatistics 打印字符串表的统计信息
    7. * -XX:+PrintGCDetails -verbose:gc 打印垃圾回收详细信息参数
    8. * 在JDK1.6下VM设置
    9. * -XX:MaxPermSize=10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
    10. * -XX:MaxPermSize=10m 设置虚拟机堆内存大小
    11. * -XX:+PrintStringTableStatistics 打印字符串表的统计信息
    12. * -XX:+PrintGCDetails -verbose:gc 打印垃圾回收详细信息参数
    13. */
    14. public class StringTable5GC {
    15. // 字符串常量池中默认1688个字符串 
    16. public static void main(String[] args) throws InterruptedException {
    17. int i = 0;
    18. try {
    19. // for (int j = 0; j < 100000; j++) { // j=100, j=10000
    20. // String.valueOf(j).intern();
    21. // i++;
    22. // }
    23. } catch (Throwable e) {
    24. e.printStackTrace();
    25. } finally {
    26. System.out.println(i);
    27. }
    28. }
    29. }

    JDK1.8

    (1)try中什么都不做

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

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

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

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

    JDK1.6

    (1)try中什么都不做

    1. 0
    2. Heap
    3. def new generation total 4928K, used 1243K [0x10030000, 0x10580000, 0x15580000)
    4. eden space 4416K, 28% used [0x10030000, 0x10166d20, 0x10480000)
    5. from space 512K, 0% used [0x10480000, 0x10480000, 0x10500000)
    6. to space 512K, 0% used [0x10500000, 0x10500000, 0x10580000)
    7. tenured generation total 10944K, used 0K [0x15580000, 0x16030000, 0x20030000)
    8. the space 10944K, 0% used [0x15580000, 0x15580000, 0x15580200, 0x16030000)
    9. compacting perm gen total 12288K, used 2537K [0x20030000, 0x20c30000, 0x20c30000)
    10. the space 12288K, 20% used [0x20030000, 0x202aa608, 0x202aa800, 0x20c30000)
    11. No shared spaces configured.
    12. Disconnected from the target VM, address: '127.0.0.1:63518', transport: 'socket'
    13. SymbolTable statistics:
    14. Number of buckets : 20011
    15. Average bucket size : 0
    16. Variance of bucket size : 0
    17. Std. dev. of bucket size: 1
    18. Maximum bucket size : 6
    19. StringTable statistics:
    20. Number of buckets : 1009
    21. Average bucket size : 1
    22. Variance of bucket size : 1
    23. Std. dev. of bucket size: 1
    24. 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毫秒

    1. /**
    2. * StringTableSize串池大小对性能的影响
    3. * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
    4. * -Xms500m 设置堆内存最小值
    5. * -Xmx500m 设置堆内存最大值
    6. * -XX:+PrintStringTableStatistics 字符串常量池统计信息
    7. * -XX:StringTableSize=1009
    8. * 设置桶大小(桶即数组索引下标元素);JDK1.6默认为1009,JDK1.7之后默认为60013,JDK1.8开始1009是可以设置的最小值
    9. * 字符串常量池底层为HashTable(HashTable类实现一个哈希表,该哈希表将键映射到相应值;HashTable底层与HashMap原理相同;JDK1.6 数组+单向链表;JDK1.8 数组 + 单向链表 + 红黑树),合理增大常量池大小会解决Hash冲突问题
    10. */
    11. public class StringTable6Optimize {
    12. public static void main(String[] args) throws IOException {
    13. try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("myjvm/linux.words"), "utf-8"))) {
    14. String line = null;
    15. long start = System.nanoTime();
    16. while (true) {
    17. line = reader.readLine();
    18. if (line == null) {
    19. break;
    20. }
    21. line.intern();
    22. }
    23. System.out.println("花费时间:" + (System.nanoTime() - start) / 1000000 + "毫秒");
    24. }
    25. }
    26. }

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

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

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

    1. public class StringTable7OptimizeIntern {
    2. public static void main(String[] args) throws IOException {
    3. List address = new ArrayList<>();
    4. //System.in.read();
    5. for (int i = 0; i < 10; i++) {
    6. try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("myjvm/linux.words"), "utf-8"))) {
    7. String line = null;
    8. long start = System.nanoTime();
    9. while (true) {
    10. line = reader.readLine();
    11. if(line == null) {
    12. break;
    13. }
    14. address.add(line.intern());
    15. }
    16. System.out.println("花费时间:" + (System.nanoTime() - start) / 1000000 + "毫秒");
    17. }
    18. }
    19. //System.in.read();
    20. }
    21. }

    六、直接内存 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 堆内存,从而提高了效率

    1. /**
    2. * ByteBuffer 作用
    3. */
    4. public class ByteAndDirectBuffer {
    5. static final String FROM = "D:\\文件\\SQL基本介绍.avi";
    6. static final String TO = "D:\\a.mp4";
    7. static final int _1Mb = 1024 * 1024;
    8. public static void main(String[] args) {
    9. io(); // io 用时:71.7764;53.8123;107.987
    10. directBuffer(); // directBuffer 用时:40.2163;30.3857;46.0605
    11. }
    12. private static void directBuffer() {
    13. long start = System.nanoTime();
    14. try (FileChannel from = new FileInputStream(FROM).getChannel();
    15. FileChannel to = new FileOutputStream(TO).getChannel();
    16. ) {
    17. ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
    18. while (true) {
    19. int len = from.read(bb);
    20. if (len == -1) {
    21. break;
    22. }
    23. bb.flip();
    24. to.write(bb);
    25. bb.clear();
    26. }
    27. } catch (IOException e) {
    28. e.printStackTrace();
    29. }
    30. long end = System.nanoTime();
    31. System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
    32. }
    33. private static void io() {
    34. long start = System.nanoTime();
    35. try (FileInputStream from = new FileInputStream(FROM);
    36. FileOutputStream to = new FileOutputStream(TO);
    37. ) {
    38. byte[] buf = new byte[_1Mb];
    39. while (true) {
    40. int len = from.read(buf);
    41. if (len == -1) {
    42. break;
    43. }
    44. to.write(buf, 0, len);
    45. }
    46. } catch (IOException e) {
    47. e.printStackTrace();
    48. }
    49. long end = System.nanoTime();
    50. System.out.println("io 用时:" + (end - start) / 1000_000.0);
    51. }
    52. }

    (二)内存溢出OOM

    java.lang.OutOfMemoryError: Direct buffer memory

    1. /**
    2. * 直接内存溢出
    3. * java.lang.OutOfMemoryError: Direct buffer memory
    4. */
    5. public class Direct2OutOfMemory {
    6. static int _100Mb = 1024 * 1024 * 100;
    7. public static void main(String[] args) {
    8. List list = new ArrayList<>();
    9. int i = 0;
    10. try {
    11. while (true) {
    12. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
    13. list.add(byteBuffer);
    14. i++;
    15. }
    16. } finally {
    17. System.out.println(i);
    18. }
    19. /*
    20. 方法区是jvm规范
    21. jdk6 中对方法区的实现称为永久代
    22. jdk8 对方法区的实现称为元空间
    23. * */
    24. }
    25. }

    (三)分配和回收原理

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

    1. /**
    2. * 禁用显式回收对直接内存的影响
    3. */
    4. public class Direct3GC {
    5. static int _1Gb = 1024 * 1024 * 1024;
    6. /*
    7. * -XX:+DisableExplicitGC 显式的
    8. */
    9. public static void main(String[] args) throws IOException {
    10. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
    11. System.out.println("分配完毕...");
    12. System.in.read();
    13. System.out.println("开始释放...");
    14. byteBuffer = null;
    15. System.gc(); // 显式的垃圾回收,Full GC
    16. System.in.read();
    17. }
    18. }

    开始分配内存 

    释放内存 

    注:

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

    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 方法

    1. /**
    2. * 直接内存分配的底层原理:Unsafe
    3. */
    4. public class Direct4Unsafe {
    5. static int _1Gb = 1024 * 1024 * 1024;
    6. public static void main(String[] args) throws IOException {
    7. Unsafe unsafe = getUnsafe();
    8. // 分配内存
    9. long base = unsafe.allocateMemory(_1Gb);
    10. unsafe.setMemory(base, _1Gb, (byte) 0);
    11. System.out.println("分配完毕...");
    12. System.in.read();
    13. // 释放内存
    14. unsafe.freeMemory(base);
    15. System.out.println("释放...");
    16. System.in.read();
    17. }
    18. public static Unsafe getUnsafe() {
    19. try {
    20. Field f = Unsafe.class.getDeclaredField("theUnsafe");
    21. f.setAccessible(true);
    22. Unsafe unsafe = (Unsafe) f.get(null);
    23. return unsafe;
    24. } catch (NoSuchFieldException | IllegalAccessException e) {
    25. throw new RuntimeException(e);
    26. }
    27. }
    28. }

    分配内存 

    释放回收内存

    2、Unsafe分配回收原理

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

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

    1. ByteBuffer.allocateDirect(_1Gb);
    2. public abstract class ByteBuffer extends Buffer implements Comparable{
    3. public static ByteBuffer allocateDirect(int capacity) {
    4. return new DirectByteBuffer(capacity);
    5. }
    6. }
    7. class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer{
    8. DirectByteBuffer(int cap) { // package-private
    9. ......
    10. try {
    11. base = unsafe.allocateMemory(size);
    12. } catch (OutOfMemoryError x) {
    13. Bits.unreserveMemory(size, cap);
    14. throw x;
    15. }
    16. unsafe.setMemory(base, size, (byte) 0);
    17. ......
    18. cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    19. att = null;
    20. }
    21. private Deallocator(long address, long size, int capacity) {
    22. assert (address != 0);
    23. this.address = address;
    24. this.size = size;
    25. this.capacity = capacity;
    26. }
    27. public void run() {
    28. if (address == 0) {
    29. // Paranoia
    30. return;
    31. }
    32. unsafe.freeMemory(address);
    33. address = 0;
    34. Bits.unreserveMemory(size, capacity);
    35. }
    36. }
  • 相关阅读:
    d与C++的绑定的Calypso工具
    JAVA后端工程师笔试题-避坑公司
    Skywalking的知识
    windows环境下搭建redis5.x集群
    电子信息工程专业课复习知识点总结:(四)信号与系统、数字信号处理
    IDEA gradle项目出现java.lang.OutOfMemoryError: GC overhead limit exceeded 之类的错误
    【Redis】Set集合相关的命令
    自定义redission装配和集成分布式开源限流业务组件ratelimiter-spring-boot-starter的正确姿势
    Prometheus监控MongoDB数据库
    滚动播报、el-scrollbar
  • 原文地址:https://blog.csdn.net/MinggeQingchun/article/details/127066302