(图源:超过1W字深度分析JVM常量池(全网最详细最有深度))
运转时数据区
未完待续。。。
概述
图。。。
- 线程私有的
- 虚拟机栈
- 本地办法栈
- 程序计数器
- 线程同享的
- 堆
- 办法区
- 直接内存(非运转时数据区的一部分)
Java虚拟机栈
效果
Java中,除了native办法,其他一切办法的调用都是经过虚拟机栈来完成的。办法调用的数据需求经过栈进行传递,每一次办法调用,都会有一个栈帧被压入栈,每一个办法调用完毕后,都会有一个栈帧被弹出。
每个栈帧都包含:局部变量表、操作数栈、动态链接、办法回来地址。
图。。。
局部变量表
局部变量表是以数组的办法寄存了各种编译器已知的变量,包含办法的this指针、办法的传参、办法中声明的局部变量。(局部变量表只存储根本数据类型和目标引证,不能存储数组和办法等类型)
一个存储单位是一个slot。局部变量表的长度,是编译期就已知的,字节码文件中的locals参数便是局部变量表的长度。局部变量表中有start、length、slot、name、signature这几个参数。
- Name:变量的名称
- Signature:变量的?
- Start:表示从哪个哪行字节码开始收效
- Length:表示收效的字节码行数长度
- Slot:表示这个变量占用局部变量表中的起始方位。long和double占用2个slot,其他变量占用1个slot(引证类型的变量呢?)
总结
- 局部变量表的存储方位是在栈帧上,不需求额定的内存分配,因而具有高效的空间利用率。
- 局部变量表的巨细是由编译期决议的,不能在运转时修改。
- 局部变量表只能存储根本数据类型和目标引证,不能存储数组和办法等类型。
- 局部变量表的读写是由 Java 虚拟机直接管理的,因而拜访局部变量的功率比拜访成员变量要高。
操作数栈
(用于寄存办法履行进程中产生的中间核算成果。别的,核算进程中产生的临时变量也会放在操作数栈中。)
操作数栈的效果便是根据字节码指令,往栈中写入或弹出数据。操作数栈的最大深度也是在编译期就确认的,能够在字节码文件的Code属性中的max_stack参数中看到。
当一个办法开始履行的时分,栈帧被创立出来之后,操作数栈的初始状态的空的。跟着办法的履行,开始不断地有入栈和出栈的操作。
事例分析。。。
动态链接
首要用于:一个办法需求调用其他办法的场景。Class文件的常量池里保存有很多的符号引证,比如办法引证的符号引证。当一个办法要调用其他办法时,需求将常量池中指向办法的符号引导转化为其在内存地址中的直接引证。动态链接的效果便是为了将符号引证转换为调用办法的直接引证,这个进程也被称为 动态链接
Java虚拟机栈的反常
栈空间是有限的,但一般正常调用的状况下不会呈现问题。可是假如函数调用呈现无限循环,就会导致栈中被压入太多栈帧,导致栈空间过深。假如此时栈的内存巨细不允许动态扩展?,当线程请求栈的深度超过当时Java虚拟机栈的最大深度的时分,就会呈现StackOverFlowError
过错。
除了StackOverFlowError
过错之外,栈还或许会呈现OutOfMemoryError
。由于假如栈的内存巨细能够动态扩展?,假如虚拟机在动态扩展时无法申请到满足的内存空间,就会抛出OutOfMemoryError
反常。
事例分析
源码
// 留意该办法不是static的
public class LocalVariTable {
public void main(String[] args) {
double d = 2.0d;
}
}
对字节码文件履行javap -v LocalVariTable.class
,成果如下
D:JavaLearningbasic-notestargetclassesjvm>javap -v LocalVariTable.class
Classfile /D:/JavaLearning/basic-notes/target/classes/jvm/LocalVariTable.class
Last modified 2024-2-24; size 468 bytes
MD5 checksum c32ea39bca66887ce1aec3bf0bb0c85c
Compiled from "LocalVariTable.java"
public class jvm.LocalVariTable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
// 运转时常量池
Constant pool:
#1 = Methodref #5.#22 // java/lang/Object."<init>":()V
#2 = Double 2.0d
#4 = Class #23 // jvm/LocalVariTable
#5 = Class #24 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Ljvm/LocalVariTable;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 d
#18 = Utf8 D
#19 = Utf8 MethodParameters
#20 = Utf8 SourceFile
#21 = Utf8 LocalVariTable.java
#22 = NameAndType #6:#7 // "<init>":()V
#23 = Utf8 jvm/LocalVariTable
#24 = Utf8 java/lang/Object
{
public jvm.LocalVariTable();
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 Ljvm/LocalVariTable;
public void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC
// 办法运转的字节码,前面是代表行号?
Code:
// 局部变量表的长度locals是4
// 操作数栈的最大深度statck是2
stack=2, locals=4, args_size=2
0: ldc2_w #2 // double 2.0d
3: dstore_2
4: return
LineNumberTable:
line 5: 0
line 6: 4
// 局部变量表详情
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljvm/LocalVariTable; // this指针,占首位,效果域0-5行字节码全都有效
0 5 1 args [Ljava/lang/String;
4 1 2 d D
MethodParameters:
Name Flags
args
}
SourceFile: "LocalVariTable.java"
本地办法栈
和虚拟机栈所发挥的效果非常类似,区别是:虚拟机栈为虚拟机履行 Java 办法 (也便是字节码)服务,而本地办法栈则为虚拟机运用到的 Native 办法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地办法被履行的时分,在本地办法栈也会创立一个栈帧,用于寄存该本地办法的局部变量表、操作数栈、动态链接、出口信息。
办法履行完毕后相应的栈帧也会出栈并释放内存空间,也会呈现 StackOverFlowError
和 OutOfMemoryError
两种过错。
程序计数器
程序计数器只占用较小的内存空间,首要有以下2个效果:
- 程序计数器能够看作是当时线程所履行的字节码的行号指示器。字节码解释器作业时,经过改动程序计数器来顺次读取指令,从而完成代码的流程操控,如:次序履行、挑选、循环、反常处理。
- 在多线程的状况下,程序计数器能够起到记录当时线程履行方位的效果,等线程切换回来的时分,能够知道线程运转到哪了。
⚠️ 留意:程序计数器是唯一一个不会呈现OutOfMemoryError
的内存区域,它的生命周期跟着线程的创立而创立,跟着线程的完毕而逝世。
Java堆
概述
Java虚拟机中占用内存最大的一块,堆是一切线程同享的区域,在虚拟机启动的时分创立。这个内存区域的效果便是寄存目标实例。
Java中绝大部分的目标实例都是寄存在堆中,可是有一部分实例是直接在栈上创立的(跟着 JIT 编译器的开展与逃逸分析技能逐渐老练,栈上分配、标量替换优化技能将会导致一些奇妙的变化,一切的目标都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始现已默许敞开逃逸分析,假如某些办法中的目标引证没有被回来或许未被外面运用(也便是未逃逸出去),那么目标能够直接在栈上分配内存。)
Java堆是废物收回器管理的首要区域,因而也被称作GC堆。从废物收回的角度,由于现在收集器选用的都是分代收回,所以堆能够细分为:新生代、老时代。
在JDK7及曾经,堆内存一般被分为3部分:新生代内存、老生代、永久代。 JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 替代,元空间运用的是本地内存。 (我会在办法区这部分内容详细介绍到)。
大部分状况,目标都会首先在 Eden 区域分配,在一次新生代废物收回后,假如目标还存活,则会进入 S0 或许 S1,并且目标的年纪还会加 1(Eden 区->Survivor 区后目标的初始年纪变为 1),当它的年纪增加到必定程度(默许为 15 岁),就会被晋升到老时代中。目标晋升到老时代的年纪阈值,能够经过参数 -XX:MaxTenuringThreshold
来设置。
堆的反常
这儿最简单呈现的便是 OutOfMemoryError
过错,并且呈现这种过错之后的表现形式还会有几种,比如:
-
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
:当 JVM 花太多时间履行废物收回并且只能收回很少的堆空间时,就会产生此过错。 -
java.lang.OutOfMemoryError: Java heap space
:假如在创立新的目标时, 堆内存中的空间不足以寄存新创立的目标, 就会引发此过错。(和装备的最大堆内存有关,且受制于物理内存巨细。最大堆内存可经过-Xmx
参数装备,若没有特别装备,将会运用默许值
办法区
运转时常量池、办法区、字符串常量池这些都是不随虚拟机完成而改动的逻辑概念,是公共且抽象的,Metaspace、Heap 是与详细某种虚拟机完成相关的物理概念,是私有且详细的。
概述
办法区是《Java虚拟机标准》中界说的一个逻辑区域。在JDK1.8前后,别离经过永久代和元空间来完成办法区的概念。
(图源:JavaGuide.cn)
办法区的效果
当虚拟机要运用一个类时,它需求读取并解析 Class 文件获取相关信息,再将信息存入到办法区。办法区会存储已被虚拟机加载的 类信息、字段信息、办法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
为什么要将永久代替换为元空间(了解即可)
详见:JavaGuide.cn
办法区常用参数有哪些
JDK 1.8 之前永久代还没被完全移除的时分一般经过下面这些参数来调理办法区巨细。
-XX:PermSize=N //办法区 (永久代) 初始巨细
-XX:MaxPermSize=N //办法区 (永久代) 最大巨细,超过这个值将会抛出 OutOfMemoryError 反常:java.lang.OutOfMemoryError: PermGen
相对而言,废物收集行为在这个区域是比较少呈现的,但并非数据进入办法区后就“永久存在”了。
JDK 1.8 的时分,办法区(HotSpot 的永久代)被完全移除了(JDK1.7 就现已开始了),取而代之是元空间,元空间运用的是本地内存。下面是一些常用参数:
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小巨细)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大巨细
与永久代很大的不同便是,假如不指定巨细的话,跟着更多类的创立,虚拟时机耗尽一切可用的体系内存。
运转时常量池
有关运转时常量池的字节码解释能够看:JavaGuide-JVM-类文件结构详解-常量池
概述
字节码(.Class)文件中除了有类的版本、字段、办法、接口等描述信息外,还有用于寄存编译期生成的各种字面量(Literal)和符号引证(Symbolic Reference)的 常量池表(Constant Pool Table) 。
常量池表会在类加载后寄存到办法区的运转时常量池中。
运转时常量池的功用类似于传统编程语言的符号表,尽管它包含了比典型符号表更广泛的数据。
既然运转时常量池是办法区的一部分,自然遭到办法区内存的约束,当常量池无法再申请到内存时会抛出 OutOfMemoryError
过错。
字面量和符号引证
-
字面量
能够了解为实际值(包含整数、浮点数和字符串字面量),int a = 8中的8 和 String a = “hello”中的hello都是字面量
-
符号引证
常见的符号引证包含类符号引证、字段符号引证、办法符号引证、接口办法符号。
便是一个字符串,只需我们在代码中引证了一个非字面量的东西,不管它是变量仍是常量,它都只是由一个字符串界说的符号,这个字符串存在常量池里,类加载的时分第一次加载到这个符号时,就会将这个符号引证(字符串)解析成直接引证(指针)
《深入了解 Java 虚拟机》7.34 节第三版对符号引证和直接引证的解释如下:
事例分析
。。。
字符串常量池
详见文章:JVM:字符串常量池
非运转时数据区
直接内存
直接内存是一种特别的内存缓冲区,并不在 Java 堆或办法区中分配的,而是经过 JNI 的办法在本地内存上分配的。
直接内存并不是虚拟机运转时数据区的一部分,也不是虚拟机标准中界说的内存区域,可是这部分内存也被频频地运用。并且也或许导致 OutOfMemoryError
过错呈现。
直接内存的分配不会遭到 Java 堆的约束,可是,既然是内存就会遭到本机总内存巨细以及处理器寻址空间的约束。
堆外内存
堆外内存便是把内存目标分配在堆外的内存,这些内存直承受操作体系管理(而不是虚拟机),这样做的成果便是能够在必定程度上减少废物收回对应用程序造成的影响。