1.什么是运行时数据区
Java虚拟机在执行Java程序的过程中,会涉及的数据划分到不同的内存区域去管理,而这部分区域就是运行时数据区。
运行时数据区有5个区域。分别是:方法区,虚拟机栈,本地方法栈,堆,程序计数器。
其中:这5个区域可以分成两类:线程私有,线程共享
- 线程私有:虚拟机栈,本地方法栈,程序计数器
- 线程共享:方法区,堆
线程共享和线程私有的区别是:线程私有是跟随线程的启动而存在,线程共享是跟随虚拟机的启动而存在
线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行
在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。
当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收
操作系统负责所有线程的安排调度到任何一个可用的CPU上,一旦本地线程初始化成功,它就会调用Java线程中的run()方法
2、程序计数器(PC寄存器)
2.1、程序计数器概述:
JVM中的程序计数器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。
这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切,并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。是软件层上的概念
2.2、程序计数器作用:
PC寄存器用来存储指向下一条指令的地址。也即将要执行的指令代码。又执行引擎读取下一条指令。
2.3、理解程序计数器:
-
它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
-
在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的。生命周期与线程的生命周期保持一致。
-
任何时间一个线程都只有一个方法在执行。也就是所谓的当前方法。程序计数器会存储当前线程所执行的Java方法的JVM指令地址。或者,如果是在执行native方法,则是未指定值(Undefined)。
-
它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
-
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
-
它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError(内存溢出)情况的区域
2.4、程序计数器举例:
public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
String s = "abc";
System.out.println(i);
System.out.println(k);
}
反编译之后:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: ldc #2
12: astore 4
14: getstatic #3
17: iload_1
18: invokevirtual #4
21: getstatic #3
24: iload_3
25: invokevirtual #4
28: return
3、虚拟机栈
3.1、虚拟机栈概述:
栈是运行时的单位,而堆是存储的单位
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放在哪儿。
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧(Stack Frame),对应着一次次的Java方法调用
虚拟机栈是线程私有的,生命周期和线程一致。
虚拟机栈的作用:主管Java程序的运行,它保存方法的局部变量,部分结果,并参与方法的调用和返回。
虚拟机栈的特点:
栈是一种快速有效的分配存储方式,访问速度仅此于程序计数器。
JVM直接对Java栈的操作只有两个:
每个方法执行,伴随着进栈(入栈,压栈)
执行结束后的出栈工作
对于栈来说不存在垃圾回收问题,但有内存溢出的问题
3.2、虚拟机栈异常:
Java虚拟机规范允许Java栈的大小是动态的或者是固定大小。
-
如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常
-
如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程是没有足够的内存去创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutofMemoryError异常
public static void main(String[] args) {
main(args);
}
出现异常:Exception in thread "main" java.lang.StackOverflowError
设置栈内存大小:使用-Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度:
//下面是在默认Java虚拟机中执行
//我们用count来标识函数调用的深度,函数调用时会将栈帧加到虚拟机栈中
//一直加入栈帧,会产生StackOverflowError
public class StackError {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
我们设置虚拟机栈的大小:
-Xss256k
再次执行之后,我们发现count值变小了,这说明加入虚拟机栈中的栈帧变少了,说明虚拟机栈的大小变小了。
3.3、虚拟机栈原理
栈中存储什么?
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在
在这个线程上正在执行的每个方法都各自对应一个栈帧
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈。遵循"先进后出"原则。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Class)。
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另一种是抛出异常,不管是使用哪种方式,都会导致栈帧被弹出。
//通过debug查看栈帧
public class StackError {
public static void methodsA(){
methodsB();
return;
}
public static void methodsB(){
methodsC();
return;
}
public static void methodsC(){
return;
}
public static void main(String[] args) {
//依次调用方法
methodsA();
}
}
3.4、虚拟机栈内部结构
每个栈帧大小取决于内部结构的大小。
每个栈帧中都存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack) 或者表达式栈
- 动态链接(Dynamic Linkling) 或者指向运行时常量池的方法引用
- 方法返回地址(Return Address) 或者方法正常退出或者异常退出的定义
- 一些附加信息
3.4.1、局部变量表
1、局部变量表的理解:
局部变量表也称为局部变量数组或本地变量表
定义为一个一维数字数组,主要用于存储方法参数和定义在方法体内的局部变量
这个数据类型包括各类基本数据类型,对象引用。以及returnAddress类型(返回值类型)
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全性问题
局部变量表所需的容量大小是在编译期间确定下来的,并保存在方法Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
2、通过字节码指令来理解局部变量表
//可以查看反编译字节码后的局部变量表
//我们可以看到,局部变量表中有四个数据
//args 是方法参数 test num str 是方法体内的局部变量
public class StatckTest {
public static void main(String[] args) {
StatckTest test = new StatckTest();
int num = 1;
String str = "jiang";
}
}
我们借助jclasslib工具来查看字节码:
我们可以看到,局部变量表的最大槽数为4
我们这两个表的信息来对局部变量表进行分析:
第一个表:LineNumberTable
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4iwgmI1a-1620817375036)(C:\Users\JiangZW\AppData\Roaming\Typora\typora-user-images\image-20210506230319259.png)]
第二个表:LocalVariableTable
字节码指令:
java代码:
分析:
这两张表都有起始PC,可以通过这个起始PC将这两张表关联起来。
先看LocalVariableTable这张表:
起始PC和length需要一起看:我们可以看到起始PC和length相加始终等于字节码指令的条数。
这就是这个变量的作用域,而起始PC就是这个变量的作用域的起始地址。这个数字指的是对应的字节码指令。
我们看第一张表:LineNumberTable
我们和第二张表结合看:
起始PC对应的是字节码指令。而行号就是对应的Java源代码的行号
3、关于Slot(槽)的理解:
Slot(变量槽):局部变量表最基本的存储单元。
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
局部变量表中存放编译期可知的各种基本数据类型,引用数据类型,returnAdderss类型的变量。
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型占用两个slot
- byte、short、char、float在存储前被转换为int,boolean也被转换为int 0 表示false 非0表示true
- long 和 double 则占据两个slot
JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量中指定的局部变量值。(如果是64位变量值时,只需要使用前一个索引即可)
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上。
如果当前帧是由构造方法或者实例方法(非静态方法)创建的,那么该对象引用this将会存放在在index为0的slot处,其余的参数按照参数表顺序继续排列。
//这是一个实例方法 需要将this 存放在 index0处,也即是序号为0处
public class StatckTest {
//num需要一个槽 index1
public void test(int num){
//i需要一个槽 index2
int i = 0;
//dou需要两个槽 index3
double dou = 2.99;
//l需要两个槽 index5
long l = 1L;
}
}
关于slot重复利用的问题
我们看下面一段代码:
public void test1(){
int a = 0;
{
int b = 0;
b = a + 1;
}
int c = a + 1;
}
局部变量表的长度是多少?
我们可以看出:代码中定义了3个变量,而且是非静态方法,index0处存放this。需要4个槽??
我们反编译字节码:看到只需要3个槽。
这就涉及到变量作用域的问题:
我们看到变量b的起始PC和长度都为4,相加为8,那么它的作用域就是字节码指令是4-8的这一段。
字节码指令8之后,b被回收,index2就空闲。
此时c的起始PC是字节码指令12,即c的作用域是字节码指令12之后。那么c正好使用index2
3.4.2、操作数栈
-
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以称为表达式栈。
-
操作数栈主要用于:保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。
-
一个新的栈帧创建出来时,这个方法的操作数栈是空的。
-
每个操作数栈都会拥有一个明确的栈深度用于存储数值。其所需的最大深度在编译器就定义好了。保存在方法的Code属性中,为Max_Stack的值
-
32bit的类型占用一个栈单位深度,64bit的类型占用两个单位
-
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据。即入栈和出栈。
//举例 Java源代码
public void test(){
byte i = 15;
int j = 8;
int k = i + j;
}
//字节码指令
Code:
0 bipush 15 将15压栈
2 istore_1 对应LocalVariableTable序号1
3 bipush 8 将8压栈
5 istore_2 对应LocalVariableTable序号2
6 iload_1 序号1出栈
7 iload_2 序号2出栈
8 iadd 指令相加
9 istore_3 对应LocalVariableTable序号3
10 return 返回
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈的栈顶,并更新PC寄存器中的下一条需要执行的字节码指令。
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
- 我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
3.4.3、动态链接
每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接。、
3.4.4、方法返回地址
方法执行时由两种退出情况:
- 正常退出,即正常执行到任何方法的返回字节码指令,如RETURN、IREURN、ARETURN
- 异常退出
无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出栈帧,退出可能由三种方式:
- 返回值压入上层调用的栈帧
- 异常信息抛给能够处理的栈帧
- PC计数器指向方法调用后的下一条指令
4、本地方法接口和本地方法栈
4.1、本地方法接口 (Native Method Interface)
什么是本地方法接口
简单来说:一个Native Method就是一个Java调用非Java代码的接口。
什么是Native方法:该方法的实现由非Java语言实现,比如C、C++。这个特征并非Java所特有的,很多其他的编程语言都有这一机制。
在定义一个native method时,并不提供实现体,因为其实现体是由非Java语言在外面实现的。
native 关键字和其余的关键字一起使用,除了abstract关键字
我们为什么要使用Native Method
1. 有的Java应用需要与Java外面的环境交互,我们可以不去关心Java应用之外的细节
2. Java应用需要与操作系统交互
3. Sun的解释器是用C实现的,这样Java就可以像普通的C一样与外部交互
4.2、本地方法栈(Native Method Stack)
-
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
-
本地方法栈也是线程私有的。
-
允许被实现成固定或者可动态扩展的内存大小,这一点和Java虚拟机栈一样。也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
线程开始调用本地方法时,会进入 个不再受 JVM 约束的世界。本地方法可以通过 JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和 JVM 相同的能力和权限。 当大量本地方法出现时,势必会削弱 JVM 对系统的控制力,因为它的出错信息都比较黑盒。对内存不足的情况,本地方法栈还是会抛出 nativeheapOutOfMemory。
5、堆
5.1、堆概述
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域
- Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间
- 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间,但是在逻辑上它应该被视为连续的
- 所有的线程共享Java堆,在这里还可以划分为线程私有的缓冲区(Thead Local Allocation Buffer)
- 《Java虚拟机规范》中堆Java堆的描述是:几乎所有的对象以及数组都应当在运行时分配在堆上
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
- 堆 是GC执行垃圾回收的重点区域
5.2、堆内存细分
现代垃圾收集器大部分都基于分代收集理论设计,堆空间在逻辑上细分为:
- Java7及之前:新生代 + 老年代 + 永久区
Young Generation Space 新生代
又分为Eden区和Survivor区
Tenure generation space 老年代
Permanent Space 永久区
- Java8及之后: 新生代 + 老年代 + 元空间
Young Generation Space 新生代
又分为Eden区和Survivor区
Tenure generation space 老年代
Meta Space 元空间
但是在物理上:永久代在方法区
我们为虚拟机添加参数:
-Xms10m -Xmx10m -- 表示虚拟机的堆空间
public class Demo1 {
public static void main(String[] args) {
try {
//让线程睡1000秒
//因为只有我们在程序运行中才能看到堆空间
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我们打开一个工具:jvisualvm.exe
安装在jdk/bin 目录下。
我们看到堆空间的大小,我们还可以看到内存的细节:
安装插件:
根据图可以看到:
新生代有3M,老年代有7M。正好是10M。因此我们在虚拟机上设置的堆内存没有包含方法区。
5.3、堆内存设置和OOM(OutOfMemeoryError)
我们可以通过虚拟机参数:-Xms
和-Xmx
来设置堆大小
-
Xms 用于表示堆空间初始大小(新生代 + 老年代),等价于-XX:InitialHeapSize
-
Xmx 用于表示堆区的最大内存(新生代 + 老年代),等价于-XX:MaxHeapSize
-X 表示虚拟机参数
一旦堆区中的内存大小超过 -Xmx 所指定的最大内存时。将会抛出OutOfMemeoryError异常。
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况下,初始内存大小:物理电脑内存大小 / 64
最大内存大小:物理电脑内存大小 / 4
public class Demo1 {
public static void main(String[] args) {
//返回Java虚拟机中堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//返回Java虚拟机试图使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");
System.out.println("-Xmx : " + maxMemory + "M");
System.out.println("系统内存大小:" + initialMemory * 64 / 1024 + "G");
System.out.println("系统内存大小:" + maxMemory * 4 / 1024 + "G");
}
}
我的电脑的内存是8G 系统内存大小接近8G
OOM例子:
我们将堆空间的大小改小一点:
-Xms100m -Xmx100m
public class OOMError {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
while(true){
//为了在jvisualvm.exe上看到内存情况,让它慢一点执行
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//死循环 在堆上创建新对象
list.add(new OOMError());
}
}
}
出现OOM异常:
5.4、新生代和老年代
存储在JVM中Java对象可以被划分为两类
- 一类生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。
- 另一类生命周期非常长,在某些极端的情况下还能于JVM的生命周期保持一致。
Java堆区进一步细分的话,可以划分为新生代和老年代
其中新生代又可以划分为Eden区、Survivor0区和Survivor1区(也可称为from区、to区)
如何设置新生代和老年代占堆空间的大小
默认 :-XX:NewRatio = 2 表示新生代占1,老年代占2 ,新生代占整个堆区的1/3
可以进行修改-XX:NewRatio = 4,新生代占整个堆区的1/5
例子:
配置堆内存大小和新生代老年代占比
-Xms10m -Xmx10m -XX:NewRatio=4
public class Demo1 {
public static void main(String[] args) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
几乎所有的Java对象都是在Eden区被new出来的。
绝大部分的Java对象的销毁都是在新生代进行了
根据IBM公司的专门研究表明,新生代中80%的对象都是"朝生夕死"
可以使用选项"-Xmn"设置新生代最大内存大小
如何设置新生代中Eden和Survivor区的占比
默认:Eden : Survivor0 : Survivor1 = 8 : 1 : 1
我们需要设置 +XX:SurvivorRatio = 8
6、方法区
6.1、方法区概述
方法区存储的是什么?
在一个JVM实例的内部,类型信息被存储在一个称为方法区的内存逻辑中。类型信息是由类加载器在类加载时从类文件中提取出来的。类(静态)变量也存储在方法区中。
方法区在什么位置?
《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是堆的一部分,但一些简单的实现可能不会选择区进行垃圾收集或者进行压缩”,但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是和堆分开。
所以,方法区看作是一块独立于Java堆的内存空间。
方法区的大小和堆空间一样,可以选择固定大小或者可扩展
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。java.lang.OutOfMemoryError Metaspace
(加载大量的第三方jar包,Tomcat部署的工程过多,大量的动态生成反射类)
关闭JVM会释放这个区域
6.2、方法区的演进
在java1.7及以前,习惯上把方法区,称为永久代。jdk8开始,使用元空间取代了永久代。
在本质上,方法区和永久代并不等价。我们可以将元空间和永久代理解为方法区的实现。
永久代和元空间的区别:
永久代占据的是虚拟机的内存
元空间占据是本地内存
永久代和元空间的内部结构也有许多的变化,比如字符串常量池、静态变量等。
具体版本之间的演进:
版本 | 变化 |
---|---|
jdk1.6及之前 | 有永久代,静态变量存放咋永久代上 |
jdk1.7 | 有永久代,字符串常量池、静态变量保存在堆中 |
jdk1.8及之后 | 无永久代,类型信息,域信息存放在元空间中,字符串常量池和静态变量存放在堆上 |
hotspot为什么用元空间替代永久代
避免OOM异常
因为通常使用PermSize和MaxPermSize设置永久代大小就决定了永久代的上限。因此可能会有OOM异常
元空间存在本地内存中,有系统的实际可用空间来控制,也可以通过虚拟机参数来控制-XX:MaxMetaspaceSize
提高GC性能
永久代的对象在Full GC时进行垃圾收集,也就是和老年代同时垃圾收集
替换后,简化了Full GC,可以在GC不进行暂停的情况下并发地释放类数据。
持久代的问题
HotSpot的内部类型也是Java对象,他可能会在Full GC中被移动,同时他对应用不透明,且是非强型的,难以跟踪调试。还需要存储元数据的元数据信息。
合并HotSpot和JRockit
合并HotSpot和JRockit的代码,JRockit从来没有所谓的永久代,也不需要开发运维人员设置永久代的大小,但是运行良好。同时也不用担心运行新能的问题。
6.3、设置方法区的大小
方法区的大小和堆空间一样,可以选择固定大小、可扩展。
演示Java7的方法区大小设置:
通过-XX:PermSize来设置永久代初始化分配空间。默认值是20.75M
通过-XX:MaxPermSize来设置永久代最大的可分配空间。
演示Java8的方法区大小设置:
元空间的大小可以使用参数-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize
指定。
元空间的大小默认值依赖与不同的平台:
在windows下 ,-XX:MetaspaceSize 是21M,-XX:MaxMetaspaceSize 的值是-1,即没有限制
MetaspaceSize设置的是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类。即这些类对应的类加载器不在存活。然后这个高水位线将会重置,新的高水位线的值取决于GC后释放了多少空间。
如果设置的初始的好水位线设置过低,那么这种高水位线的调整会发生很多次。Full GC多次调用。为了避免频繁GC,建议设置一个较高的值。
6.4、OOM
public class Demo1 extends ClassLoader {
public static void main(String[] args) {
//使用-XX:-UseCompressedOops 关闭指针压缩参数后 出现 java.lang.OutOfMemory:metaspace
int j = 0;
Demo1 OOMTest = new Demo1();
try {
for (int i = 0; i < 5000; i++) {
//创建ClassWriter对象,用于生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
//指定版本号,修饰符,类名,包名,父类,接口
classWriter.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class" + i,null,"java/lang/Object",null);
//返回byte[]
byte[] code = classWriter.toByteArray();
//类的加载
OOMTest.defineClass("Class" + i,code,0,code.length);//Class对象
j++;
}
} finally {
System.out.println(j);
}
}
}
虚拟机参数设置为:
-XX:-UseCompressedOops -XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=5M
如何解决这些OOM?
- 通过内存映像分析工具对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
- 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置
- 如果不存在内存泄漏,也就是说内存中的对象都还存活着。那就应当检查虚拟机的参数(-Xms 和 -Xmx),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
6.5、方法区内部结构
类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储一下类型信息:
- 这个类型的完整有效名称(包名.类名)
- 这个类型直接父类的完整有效名
- 这个类型的修饰符(public,abstract,final的某个子集)
- 这个类型实现接口的一个有序列表
域信息
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称、域类型、域修饰符等
方法信息
JVM必须在方法区保存所有方法的信息
方法信息包括:方法名称、方法的返回类型、方法参数的数量和类型、方法修饰符、方法的字节码、异常表(每个异常处理的开始位置、结束位置等)
no-final
静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。类变量被类的所有实例共享,即使没有类实例时你也可以访问它。
运行时常量池
6.6、运行时常量池
常量池和运行时常量池:
什么是常量池:
一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池中,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。
常量池,可以看做是一场表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等类型。
public class Demo2 {
public static void main(String[] args) {
String s = new String("Hello World");
Demo2 demo2 = new Demo2();
System.out.println(s);
System.out.println(demo2);
}
}
常量池和运行时常量池的对比:
运行时常量池(Runtime Constant Pool)是方法区的一部分。在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
常量池(Constant Pool Table)是 Class文件的一部分,
用于存放编译期间生成的各种字面量和符号引用。这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池相对于常量 具备 动态性。