jvm 运行时数据区
总览
java 虚拟机在运行时会将它所治理的内存分成若干个区域,这些区域有各自的用处、创立和销毁工夫,有的是随着虚拟机过程启动而创立,有的是随着用户线程的启动和完结而创立和销毁,依照虚拟机标准,虚拟机内存被划分为以下区域
程序计数器
定义
Program computer Register 程序计数器, 其作用为
- 记住下一条指令地址
- 程序执行过程中,呈现分支、循环、跳转、线程复原须要通过计数器获取下一条指令地址去执行
-
特点
-
线程公有
每个线程都有本人独立的程序计数器,因为多线程是通过线程轮流切换实现,同一时刻永远只有一个线程在 cpu 或内核上执行,一个线程执行到一半轮到下一个线程执行,须要通过计数器保留指令执行的地址,复原后就从计数器保留的地址开始继续执行
- 占用内存很小,不存在内存溢出危险
-
案例
上面通过一个例子看看程序计数器怎么起作用的
public class PCTest {public static void main(String[] args) {for (int i = 0; i < 2; i++) {if(i == 0){System.out.println("hello");
}else {System.out.println("world");
}
}
}
}
应用 javap -v PCTest
进行反编译, 失去以下片段,左侧红框外面是字节码指令地址,而篮框示意这条字节码指令会跳转到指定地址去执行。PC 的作用就是记录这些指令的地址。
虚拟机栈
定义
- 每个线程执行时须要的内存空间被称为栈
- 办法调用时会创立一个栈帧,并将其入栈,办法调用结束后会将其栈帧出栈
- 栈帧外面蕴含了局部变量表、操作数栈、动静链接、返回地址等信息
- 每个栈只有一个流动栈帧即位于栈顶的那个栈帧,对应着正在执行的办法
栈帧
局部变量表
一组变量值的存储空间,能够了解为一个数组,数组中每个地位用于存储一个局部变量,或者办法参数、this 变量(实例办法才有)。在编译为 class 文件时,局部变量表的最大长度曾经确定,存在 code 属性的 max_locals 附加属性中。
变量槽
- 每个变量槽都能寄存一个 boolean、byte、char、short、int、float、reference 或 returnAddress 类型的数据
- 如果是 double 或 long 类型的数据,要用间断的两个变量槽
-
reference 类型有两个作用
- 能够间接或间接查到对象在堆中的地址
- 能够间接或间接查到对象所属数据类型的在办法区中的类型信息
变量槽的调配
- 如果是实例办法,第一个变量槽用 this
- 先将参数散布到变量槽,再为局部变量调配变量槽
-
变量槽复用
- 办法体内定义的变量,其作用域不肯定是整个办法,一个变量作用域范畴以外定义的变量,能够复用其变量槽
操作数栈
- 是一个先入后出的栈
- 最大深度在编译实现后曾经确定了,寄存在 code 属性的 max_stacks 数据项中
- double 和 long 占用两个栈容量,其余数据类型占用一个栈容量
- 能够用来传递参数给调用的办法
- 能够用来进行算术运算
动静链接
- 每个栈帧都蕴含了一个援用,指向以后办法所属类型在运行时常量池中的地址,用来反对办法代码的动静链接。
- 在 class 文件中,一个办法通过符号援用来调用其余办法或拜访字段。动静链接将这些办法的符号援用转换为办法的间接援用,必要时会加载类信息以解析尚未解析的符号援用,并将变量的拜访转换为与这些变量运行时地位相干的存储构造中的适当偏移量。
返回地址
- 办法退出有两种,失常退出和异样退出,失常退出可能有返回值,异样退出肯定不会有返回值,然而无论失常或异样退出,都必须返回到办法最后被调用的地位
- 失常退出是通过栈帧中保留的主调办法的 PC 计数器的值作为返回地址
- 异样退出时返回地址通过异样处理表来确定
-
办法退出的流程可能如下
- 以后栈帧销毁,复原上一层栈帧的操作数栈和局部变量表,将以后栈帧的返回值压入调用者栈帧的操作数栈,调整 PC 计数器的值指向办法调用后一条指令的地位
线程、栈、栈帧的关系
<img src=”https://gitee.com/thomasChant/drawing-bed/raw/master/%E7%BA%BF%E7%A8%8B%E3%80%81%E6%A0%88%E3%80%81%E6%A0%88%E5%B8%A7.jpg” alt=” 线程、栈、栈帧 ” style=”zoom:25%;” />
栈帧演示
应用 idea 编写测试类,并在 methodC()
办法打上断点,而后以 debug 模式启动程序
public class StackFrameTest {public static void main(String[] args) {methodA();
}
private static void methodA() {System.out.println("A");
methodB();}
private static void methodB() {System.out.println("B");
methodC();}
private static void methodC() {System.out.println("C");
}
}
当程序运行到 methodC()
的时候,查看控制台,发现呈现了 4 个栈帧
本地办法栈
- 本地办法栈 (Native Method Stacks) 和虚拟机栈十分相似,不同的是虚拟机栈是为调用 java 办法服务,而本地办法栈是为虚拟机调用本地办法服务
- 本地办法栈在栈深度溢出或栈扩大失败是也别离会抛出
StackOverflowError
和OutOfMemoryError
异样
堆
定义
- 用于存储对象实例的内存区域
特点
- 它是线程共享的,堆中对象须要思考线程平安问题
- 通过主动内存回收机制治理堆内存
- 堆最小和最大内存别离通过 -Xms 和 -Xmx 设置,如
-Xms10m
设置最大堆内存为 10m
堆内存溢出
通过以下代码能够测试堆内存的溢出, 启动程序前须要先设置虚拟机参数-Xmx10m
,将堆内存的最大值设置为 10m
package dataarea;
import java.util.ArrayList;
import java.util.List;
/**
* VM Args -Xmx10m
* 堆内存溢出问题
* @author ct
* @date 2021/10/21
*/
public class HeapOOMTest {
static class OOMObject{byte[] bytes = new byte[1024];
}
public static void main(String[] args) {List<OOMObject> list = new ArrayList<>();
try{while (true){list.add(new OOMObject());
}
}catch(Throwable e){e.printStackTrace();
}finally {System.out.println(list.size());
}
}
}
从运行后果能够看出,堆内存产生了溢出,产生 OOM 异样
8641
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.lang.Throwable.printStackTrace(Throwable.java:649)
at java.lang.Throwable.printStackTrace(Throwable.java:643)
at java.lang.Throwable.printStackTrace(Throwable.java:634)
at dataarea.HeapOOMTest.main(HeapOOMTest.java:24)
办法区
定义
- 办法区是所有线程共享的一个区域
- 次要用于存储类相干的信息,如运行时常量池、字段、办法数据,办法或结构器的代码
- 办法区在虚拟机启动时创立
- 办法区尽管在逻辑上是堆的一部分,然而虚拟机的实现能够抉择不进行垃圾回收或整顿。
- 办法区是标准,永恒代或元空间是其实现,hotspot1.8 以前是应用的永恒代,1.8 当前应用的元空间
- 永恒代在堆内存,元空间在本地内存
hotspot8 和 hotspot8 以前的办法区比照
内存溢出
jdk1.8 测试元空间的内存溢出。通过动静代理技术创立大量的类,并设置元空间大小为 10m:-XX:MaxMetaspaceSize=10m
package com.company;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 制作元空间的内存溢出,通过
* VM args -XX:MaxMetaspaceSize=10m
*
* @author ct
* @date 2021/10/22
*/
public class MethodAreaOOMTest1 extends ClassLoader{public static void main(String[] args) {MethodAreaOOMTest1 test = new MethodAreaOOMTest1();
ClassWriter cw = new ClassWriter(0);
for (int i = 0; i < 10000; i++) {
//jdk 版本、修饰符、类名、包名、父类
cw.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i, null, "java/lang/Object", null);
byte[] code = cw.toByteArray();
test.defineClass("Class"+i,code,0,code.length);
}
}
}
执行后果如下
- {:height 107, :width 554}
运行时常量池
- 常量池是编译后的字节码文件中的字面量、符号援用信息,当一个类被 jvm 加载,常量池外面的数据就会被加载到办法区内存中,成为运行时常量池
- 由符号援用翻译过去的间接援用,也会寄存到运行时常量池中
字符串池
测试题
(如果晓得以下程序输入后果,能够跳过这一讲了)
package stringtable;
/**
* stringtable.StringTableTest
*
* @author ct
* @date 2021/10/23
*/
public class StringTableTest {public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a"+"b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
System.out.println(s3 == s4);//
System.out.println(s3 == s5);//
System.out.println(s3 == s6);//
String x1 = new String("c") + new String("d");
String x3 = x1.intern();
String x2 = "cd";
// 如果调换 22、23 行地位,或者在 jdk1.6 运行呢
System.out.println(x1==x3);//
System.out.println(x2==x3);//
}
}
运行常量池和字符串池的关系
常量池的信息加载到运行时常量池中,字符串池是运行时常量池的一部分
字符串池个性
- 常量池中字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来防止反复创立字符串对象
- 字符串变量拼接是通过
StringBuilder
- 字符串常量拼接原理是通过编译器优化
- 能够应用
intern()
办法,被动将串池中还没有的字符串对象放入串池 - 串池是一个底层应用了
hashTable
,能够应用
变量拼接
如下,s1+s2
会被转化为new StringBuilder().append("a").append("b").toString()
String s1 = "a";
String s2 = "b";
String s3 = s1 + s2;
通过查看字节码能够验证。
常量拼接
对于两个字符串常量间接相加,能够通过 javac 在编译期间接优化为一个字符串常量
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
剖析字节码指令能够看出,s3 间接被编译为了ab
字符串常量提早加载的验证
通过上面代码以及 idea 的 debug 性能,咱们来验证一下字符串常量提早加载的过程
package stringtable;
/**
* stringtable.StringTableTest
*
* @author ct
* @date 2021/10/23
*/
public class StringTableTest2 {public static void main(String[] args) {System.out.println();
System.out.println("0");
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println();
System.out.println("0");
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println();}
}
断点打到第 12、22、32 行,当代码执行到第 11 行的时候,咱们能够看到 debug 工具界面右侧,string 类的实例数量为 949 个
当断点执行到底 22 行,能够看到实例数量变为 959 个
可是到底 32 行的时候,再看实例数量,依然为 959 个
从以上执行后果能够看出,最开始字符串 ”0″,”1″,”2″….”9″ 原本是在字符串池中不存在的,执行的过程中创立字符串对象,退出串池并返回其援用,随后再执行,因为曾经在串池中存在,就不再创立新的对象
intern()办法
intern 的字面意思是扣留、幽禁,听起来还挺合乎这个办法的作用,将一个字符串常量放入字符串池中,在 jdk1.8 和 jdk1.6 中,稍有一点区别:
- 1.8 尝试将字符串放入串池,如果有则不放入,如果没有则放入串池,并返回一个对象援用
- 1.6 尝试将字符串放入串池,如果有则不放入,如果没有则复制一份放入串池,并返回一个对象援用
串池所在位置
-
先说论断
- jdk1.6 存在于办法区的永恒代
- jdk1.8 存在于堆中,因为永恒代的回收效率不高,而字符串属于应用比拟频繁的对象,在 1.8 虚拟机中开始将串池转移到堆中
-
验证
下列代码不停往字符串常量池中写数据,造成内存溢出
package stringtable; import java.util.ArrayList; import java.util.List; /** * 测试 jdk1.8 下因为串池过大导致的堆内存溢出,运行前须要批改堆大小为 -Xmx=10m * VM args: -Xmx10m * @author ct * @date 2021/10/27 */ public class StringTableOOMTest {public static void main(String[] args) {List<String> list = new ArrayList<>(); for (int i = 0; i < 1000000; i++) {list.add(String.valueOf(i).intern()); } } }
执行后发现抛出了异样,然而异样提醒为
GC overhead limit exceeded
,依据 Oracle 官网文档,其意思为:默认状况下,如果 Java 过程破费 98% 以上的工夫执行 GC,并且每次只有不到 2% 的堆被复原,则 JVM 抛出此谬误。Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded at java.lang.Integer.toString(Integer.java:401) at java.lang.String.valueOf(String.java:3099) at stringtable.StringTableOOMTest.main(StringTableOOMTest.java:16)
能够通过
-XX:-UseGCOverheadLimit
参数使虚拟机在内存溢出时间接抛出异样。从打印的信息能够看出,抛出了堆内存溢出异样。
串池的垃圾回收
字符串常量池也是会被进行垃圾回收的,通过 -XX:+PrintStringTableStatistics
参数咱们能够在 jvm 退出的时候打印出串池的详细信息
public class StringTableGCTest {public static void main(String[] args) {for (int i = 0; i < 100; i++) {String.valueOf(i).intern();}
}
}
从打印信息能够看出串池中有 846 个字符串
咱们把 100-199 这 100 个数字的字符串也加到串池,再运行:
public class StringTableGCTest {public static void main(String[] args) {for (int i = 0; i < 200; i++) {String.valueOf(i).intern();}
}
}
能够看到字符串个数减少到 946,此时是没有产生内存溢出的
接着咱们将字符串个数增大到 100000,并且将堆最大内存设置为 -Xmx10m
,同时加上-XX:+PrintGCDetails
参数,用于观看 gc 日志
public class StringTableGCTest {public static void main(String[] args) {for (int i = 0; i < 100000; i++) {String.valueOf(i).intern();}
}
}
能够看到后果串池中的字符串个数仅有 3 万多个
查看日志,能够看出产生了新生代的垃圾回收,这一点从侧面阐明串池是存在于堆中的。
性能调优
设置桶大小
因为 StringTable 是一个 hash 表,bucket 个数越大,越不容易产生 hash 抵触,效率也越高,咱们能够通过 jvm 参数 -XX:StringTableSize
来减少桶的个数,看个例子,首先咱们不批改StringTableSize
(默认为 60013),执行上面代码,往串池外面写入字符串:
package stringtable;
import java.util.Random;
/**
* 通过减少 StringTable 的 Bucket 个数,进步 intern()效率
* VM Args: -XX:StringTableSize=500000 -XX:+PrintStringTableStatistics
* @author ct
* @date 2021/10/23
*/
public class StringTableSizeTest {public static void main(String[] args) {Random random = new Random(1);
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {String.valueOf(random.nextInt()).intern();}
System.out.println("cost"+(System.currentTimeMillis() - start));
}
}
能够看到执行工夫为 800ms
接着将 StringTableSize 改为 500000
能够看到效率有将近一倍的晋升
反复字符串放入串池,能够无效缩小内存占用。假如有大量字符串数据须要放入内存,而这些数据中有反复的局部,就能够思考将其 intern 到串池,缩小对内存的占用。