JVM -- JVM内存结构:程序计数器虚拟机栈本地方法栈堆方法区
Posted MinggeQingchun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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
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)
堆内存被划分为
两块
,一块的年轻代
,另一块是老年代
。年轻代又分为
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异常
(一)堆内存溢出 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 Memor
以上是关于JVM -- JVM内存结构:程序计数器虚拟机栈本地方法栈堆方法区的主要内容,如果未能解决你的问题,请参考以下文章