1.JVM内存结构&运行时数据区
运行时数据区定义:Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
JVM中主要分为:堆、方法区、程序计数器、虚拟机栈、本地方法栈。
线程私有区域(线程独享):程序计数器、虚拟机栈、本地方法栈;
线程共享区域(线程共享):堆、方法区;
除此之外,还有个直接内存(堆外内存),虽然不是运行时数据区的一部分,但是也会频繁使用,可以理解为操作系统未被虚拟化的一部分内存。
1.1 程序计数器
较小内存空间,当前线程执行代码的行号指示器,各线程间独立存储,互不影响。
主要记录线程执行的字节码的地址,如分支、循环、跳转、异常、线程恢复等都依赖计数器。
1.2 虚拟机栈
方法的执行和栈帧结构见第2节。
虚拟机栈 作用:存储当前线程执行方法的数据,指令,返回地址。
虚拟机栈基于线程:哪怕只有一个main方法,在线程生命周期中,参与计算的数据都会频繁出入栈,栈的生命周期与线程一样。
虚拟机栈的大小缺省为 1M,可用参数 –Xss 调整大小,例如-Xss256k。
栈帧4部分:局部变量表、操作数栈、动态链接、方法返回地址。
- 局部变量表:用于存储局部变量(一般是方法中变量)。它32位的长度为一个槽slot,Java中8大基础数据类型,如果是32位,直接占用一个slot,如果64位就用高低位占用两个slot。如果是局部一些对象,如Object对象,只需要存它的引用地址。(基本数据类型、对象引用、returnAddress 类型)
- 编译时已经确定局部变量表、操作数栈大小,可在Class文件中查看;
- 局部变量表已槽(slot)为单位,一个slot32位,64位的数据可按高低位占两个slot(基本类型和引用类型占1个slot,long和double占2个slot);
- 方法执行中,虚拟机用局部变量表来实现参数传递过程。如果是实例方法,局部变量表第0位slot存储值默认为方法所属的实例this,其余参数从1开始依次存储;
- 操作数栈:存放Java操作数,栈结构,用来操作数据,操作的元素可以是Java任意类型,通过依次执行指令来操作。操作数栈本质上是JVM执行引擎的一个工作区,只有方法执行的时候才会进行数据进栈出栈操作,代码不执行时栈为空。
- 编译期确定大小;
- Frame创建时,栈为空,可以存Java各种类型,long和double占2个栈深;
- 操作栈调用其它有返回结果的方法时,会把结果 push 到栈上(通过操作数栈来进行参数传递);
- 动态链接:Java语言多态性(后续结合class与执行引擎理解) // TODO
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接;
- 在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在运行时转化为直接引用,这部分称为动态链接;
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接;
- 返回地址:正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)。
三步曲:恢复上层方法的局部变量表和操作数栈、把返回值(如果有的话)压入调用者栈帧的操作数栈中、调整程序计数器的值以指向方法调用指令后面的一条指令、异常的话:(通过异常处理表<非栈帧中的>来确定)
虚拟机栈内存有限,默认1M,如果不断向其中入栈,会导致虚拟机栈爆掉。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性之中。
因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
1.3 本地方法栈
本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是
用 Java 实现的,而是由 C 语言实现的(比如 Object.hashcode 方法)。
本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。你甚至可以认为虚拟机栈和本地方法栈是同一个区域。虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot 直接把本地方法栈和虚拟机栈合二为一 。
1.4 方法区(元空间)
方法区(Method Area)是可供各条线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。
JDK1.7之前存在于“永久代”中;JDK1.8之后使用元空间实现方法区,并且“字符串常量池”和“静态变量”放入了堆中。
元空间:方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。
在 HotSpot 虚拟机、Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地内存。
元空间大小参数:
jdk1.7 及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
jdk1.8 以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize
jdk1.8 以后大小就只受本机总内存的限制(如果不设置参数的话)
元空间代替永久代好处:
1.融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。
2.永久代内存经常不够用或发生内存溢出,抛出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。
方法区(Method Area)是可供各条线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。
JDK1.7之前存在于“永久代”中;JDK1.8之后使用元空间实现方法区,并且“字符串常量池”和“静态变量”放入了堆中。
1.5 运行时常量池
运行时常量池(Runtime Constant Pool)是类或接口常量池(Constant_Pool)加载后的运行时表现形式。它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。
1.6 堆内存
- 堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆;
- 堆一般设置成可伸缩的;
- 随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection);
- 对象到底在堆上分配还是栈上分配,取决于:对象的类型和在 Java 类中存在的位置;
- Java对象来说:基本数据类型、普通对象。
- 普通对象——都是堆上分配内存,其他地方使用它的引用(比如局部变量表中);
- 基本类型数据(byte,short,int,long,float,double,char,boolean)——如果在方法内声明,则在栈分配,其他情况,堆上分配;
-Xms:堆的最小值;
-Xmx:堆的最大值;
-Xmn:新生代的大小;
-XX:NewSize;新生代最小值;-XX:MaxNewSize:新生代最大值;
![](https://img2020.cnblogs.com/blog/617098/202009/617098-20200930012428917-482072680.png)
1.7 堆外内存(直接内存)
JVM 在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的内存也就是堆外内存。
它不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义的内存区域;如果使用了 NIO,这块区域会被频繁使用;
使用时,分配内存,然后在堆中的directByteBuffer对象直接引用来操作。
这块内存不受 java 堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异常。
小结:
1、直接内存主要是通过 DirectByteBuffer 申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小。
2、其他堆外内存,主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。
堆外内存的泄漏是非常严重的,它的排查难度高、影响大,甚至会造成主机的死亡。
2.方法运行和栈帧
2.1 栈帧
线程运行Java方法依赖虚拟机栈,方法被打包成栈帧结构依次压入虚拟机栈(虚拟机栈中被压入的是打包好的栈帧)。
栈帧:局部变量表、操作数栈、动态链接、方法返回地址。
起一个 main 方法,在 main 方法运行中调用 A 方法,A 方法中调用 B 方法,B 方法中运行 C 方法。
启动程序,线程1启动,就会有一个虚拟机栈,同时将每个方法打包成栈帧,从main方法调用,A-B-C,栈帧依次送入虚拟机栈。
C运行完出栈,依次B-A-main出栈,方法调用完成。
虚拟机栈就是存储线程运行方法中的数据的。
2.2 深入了解方法执行和栈帧
2.2.1 Java代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/** * 编译:javac comjvmStackFrame.java * 反编译:javap -p -v comjvmStackFrame.class */ public class StackFrame { public static void main(String[] args) { add(1, 2); } private static int add(int a, int b) { int c = 0; c = a + b; return c; } }
2.2.2 反编译后字节码(部分)
javap -v -p StackFrame:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
Classfile /E:/Code/flickeringproject/pracpro/target/classes/com/lims/pracpro/jvm/StackFrame.class Last modified 2020-9-30; size 557 bytes MD5 checksum 3c7350367a9369cac085d74406eee28f Compiled from "StackFrame.java" public class com.lims.pracpro.jvm.StackFrame minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#24 // java/lang/Object."<init>":()V #2 = Methodref #3.#25 // com/lims/pracpro/jvm/StackFrame.add:(II)I #3 = Class #26 // com/lims/pracpro/jvm/StackFrame #4 = Class #27 // java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Utf8 LineNumberTable #9 = Utf8 LocalVariableTable #10 = Utf8 this #11 = Utf8 Lcom/lims/pracpro/jvm/StackFrame; #12 = Utf8 main #13 = Utf8 ([Ljava/lang/String;)V #14 = Utf8 args #15 = Utf8 [Ljava/lang/String; #16 = Utf8 add #17 = Utf8 (II)I #18 = Utf8 a #19 = Utf8 I #20 = Utf8 b #21 = Utf8 c #22 = Utf8 SourceFile #23 = Utf8 StackFrame.java #24 = NameAndType #5:#6 // "<init>":()V #25 = NameAndType #16:#17 // add:(II)I #26 = Utf8 com/lims/pracpro/jvm/StackFrame #27 = Utf8 java/lang/Object { public com.lims.pracpro.jvm.StackFrame(); 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 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/lims/pracpro/jvm/StackFrame; 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: iconst_1 1: iconst_2 2: invokestatic #2 // Method add:(II)I 5: pop 6: return LineNumberTable: line 9: 0 line 10: 6 LocalVariableTable: Start Length Slot Name Signature 0 7 0 args [Ljava/lang/String; private static int add(int, int); descriptor: (II)I flags: ACC_PRIVATE, ACC_STATIC Code: stack=2, locals=3, args_size=2 0: iconst_0 1: istore_2 2: iload_0 3: iload_1 4: iadd 5: istore_2 6: iload_2 7: ireturn LineNumberTable: line 13: 0 line 14: 2 line 15: 6 LocalVariableTable: Start Length Slot Name Signature 0 8 0 a I 0 8 1 b I 2 6 2 c I } SourceFile: "StackFrame.java"
只看方法:
public class com/lims/pracpro/jvm/StackFrame { // compiled from: StackFrame.java
// 省略了 <init>()V方法 main方法
...... // access flags 0xA private static add(II)I L0 LINENUMBER 13 L0 ICONST_0 ISTORE 2 L1 LINENUMBER 14 L1 ILOAD 0 ILOAD 1 IADD ISTORE 2 L2 LINENUMBER 15 L2 ILOAD 2 IRETURN L3 LOCALVARIABLE a I L0 L3 0 LOCALVARIABLE b I L0 L3 1 LOCALVARIABLE c I L1 L3 2 MAXSTACK = 2 MAXLOCALS = 3 }
2.2.3 add方法字节码解释
# 方法描述 # 括号内为入数类型,这里为两个 int 型入参 # 括号外为返回类型,这里为返回 int 型 descriptor: (II)I # 方法类型,这里为私有的静态方法 flags: ACC_PRIVATE, ACC_STATIC # 操作数栈为 2 # 本地变量容量为 3 # 入参个数为 2 stack=2, locals=3, args_size=2