JVM之运行时数据区
JVM之运行时数据区
在哪里
详细图!
Java虚拟机定义了若干种程序运行期间会使用到了运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机的退出而销毁。另外一些则是与线程一一对应的,这些线程对应的数据区会随着线程的开始和结束而创建和销毁
图中灰色为单独线程私有,红色为线程共享的
- 每个线程包括:独立的程序计数器、栈、本地栈
- 线程共享包括:堆、堆外内存(永久代或元空间、代码缓存(JIT编译后的产物))
- 永久代或元空间是方法区的具体实现。
线程
- 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行执行。
- 在 Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。
- 当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程终止后,本地线程也会回收。
- 操作系统负责所有线程的安排调度到任何一个可用的CPU上,一旦本地线程初始化成功,它就会调用Java线程中的
run()
方法.
JVM系统线程
如果使用
jconsole
或者是任何一个调试工具,都能看到有许多线程在运行。这些后台线程不包括public static void main(String[] a)
的main线程以及main线程所创建的线程。
在Hotspot JVM
中后台系统线程主要是一下几个:
- 虚拟机线程:这种线程的操作是需要JVM到达安全点才回出现。这些操作必须在不同的线程中发生的原因是它们都需要JVM到达安全点,这样堆才不会变化。这种线程的执行类型包括“stop-the-world”的垃圾收集,线程栈收集,线程挂起已经偏向锁撤销。
- 周期任务线程:这种线程是时间周期事件的体现(比如中断),它们一般用于周期性操作的调度执行。
- GC线程:这种线程对在JVM中不同种类的垃圾收集行为提供支持。
- 编译线程:这种线程在运行时会将字节码贬义词本地代码。
- 信号调度线程:这种线程接收信号并发送给JVM,在它内部通过适当的方法进行处理。
一、 PC寄存器
JVM的程序计数器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。
这里,并非是广义上所指的物理寄存器,可以理解为PC计数器可能会更加贴切(也称为程序钩子),并且也不容易引起误会,JVM的PC寄存器是对物理PC寄存器的一种软件层面的抽象模拟。
作用
PC寄存器用来存储指向下一条指令的地址也即将要指向的指令代码。由执行引擎读取下一条指令。
介绍
- 它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的内存区域。
- 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定的值(undefned)
- 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 它是唯一一个在Java虚拟机规范中没有规定任何
OutOtMemoryError
情况的区域。
查看字节码文件
javap -v RuntimeTest.class
问题
使用PC寄存器存储字节码指令地址有什么用?
或为什么使用PC寄存器记录当前线程执行的地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来后,就得知道接着从哪里开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
二、 虚拟机栈
内存中的栈与堆
栈是运行时单位,堆是存储单位
栈解决的是程序运行问题,即程序如何执行,或者如何处理数据。堆解决的是数据存储文件,即数据怎么放,往哪放。
Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
栈是线程私有的
生命周期
生命周期和线程一致。
作用
主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用与返回。
栈的特点(优点)
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
- JVM直接对Java栈的操作只有2个:
- 每个方法执行,伴随着入栈(压栈)
- 执行结束后的出栈(弹栈)工作
-对于栈来说不存在垃圾回收问题。
- 设置栈内存大小
默认栈大小为:1024kb
使用参数 -Xss 选项来设置线程的最大栈空间,栈大小直接决定了函数调用的最大可达深度。
栈的存储单位
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
在每个线程上正在执行的每个方法都对应各自的一个栈帧(Stack Frame)
栈帧是内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
- JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循栈的“先进后出/后进先出”的原则
- 在一条活动的线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class).
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
- 如果在该方法中调用了其它方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧。
- 不同线程所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
- 如果当前方法调用了其它方法,方法返回之际,当前栈帧会回传此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
- Java方法有2中返回方式,一种是正常函数返回,使用return指令;另一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
栈帧的内部结构
每个栈帧中存储着:
- 1、局部变量表(Local Variables)
- 2、操作数栈(Operand Stack) (或表达式栈)
- 3、动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)
- 4、方法返回地址(Return Address) (或方法正常退出或者异常退出的定义)
- 一些附加信息
1、局部变量表(Local Variables)
- 局部变量表也被称为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象yin引用(reference)、以及
returnAddress
类型。 - 由于局部变量表是建立在线程栈上,是线程私有数据,因此不存在数据安全问题。
- 局部变量表所需要的容量大小是在编译期间确定下来的,并保存在方法的
Code
属性的maximum local variables
数据项中。在方法运行期间是不会改变局部变量包的大小的。
关于Slot
的理解
- JVM会为局部变量表中的每一个
Slot
都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。 - 当一个示例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会安装顺序被复制到局部变量表中的每一个
Slot
上 - 如果需要访问局部变量表中一个
64bit
的局部变量值时,只需要使用前一个索引即可(比如:访问long或者double类型变量) - 如果当前栈帧是由构造方法或者实例方法创建的,那么该对象引用
this
将会存放在index
为0的Slot
处,其余的参数按照参数表顺序继续排列。
关于Slot
重复利用
在下面的代码段中,我们可以看到一共有如下局部变量
- this
- a
- b
- c
理论上应该是在局部变量表中有4个槽位分布,但是由于Slot
有重复利用的机制,我们发现,变量b
的作用域仅在22行有效,而在下面又定义了一个变量c
,变量c
定义时,变量b
已经失效了,所以变量b
被分配的槽将被变量c
复用。
//Solt 重复利用
18 public void test(){
19 int a = 0;
20 {
21 int b = 0;
22 b = a + 1;
23 }
24
25 int c = a + 1;
26
27 }
说明
- 在栈帧中,与性能调优关系最为密切的部分就是
局部变量表
了。在方法执行时,虚拟机使用局部变量表完成方法的传递。 局部变量表
中的变量也是重要的垃圾回收根节点,只要被局部变量表
中直接或者间接引用的对象都不会被回收!
2、操作数栈(Operand Stack)
每一个独立的栈帧中除了包含局部变量表以外,还包含一个操作数栈,也可以称之为表达式栈
-
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据获取提取数据,即入栈(push)/出栈(pop)
-
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈,
-
比如:执行复制、交换、求和等操作
-
操作数栈,主要用于保存计算过程的中间结果,同时作为技术过程中变量临时的存储空间。
-
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始被创建的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
-
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,所需的最大深度在编译期就定义好了,保存在方法的
Code
属性中,为max_stack
的值。 -
栈中任何一个元素都是可以任意的Java数据类型。
-
32bit的类型栈用一个栈单位深度
-
64bit的类型占用两个栈单位深度
-
操作数栈(数组实现)并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)/出栈(pop)操作来完成一次数据访问。
我们以代码的方式体会一下操作数栈是如何工作的
代码
public void testAddOperation(){
byte i = 15;
int j = 8;
int k = i + j;
}
使用
javap -v
命令解析后的字节码指令
Code:
stack=2, locals=4, args_size=1
0: bipush 15
2: istore_1
3: bipush 8
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
分析
栈顶缓存技术
3、动态链接(Dynamic Linking)或指向运行时常量池的方法引用
-
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如
invokedynamic
指令 -
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference) 保存在class文件的常量池里。比如:描述一个方法调用了另外的其它方法时,就是用过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
方法的调用
在Jvm中,将符号引用转换为调用方法的直接引用,与方法的绑定机制相关。
- 静态链接:
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变。
将调用方法的符号引用转换为直接引用的过程称为静态链接。
- 动态链接:
被调用的目标方法在编译期无法被确定下来,只能够在程序运行期将方法的符号引用转换为直接引用,这种引用转换的过程具备动态性,称为动态链接。
方法的绑定机制分为早期绑定(Early Binding)和晚期绑定(Late Bingind)。绑定是一个字段、方法或类在符号引用被替换为直接引用的过程。
- 早期绑定:
被调用的目标方法在编译期可知,且运行保持不变。
- 晚期绑定:
被调用方法在编译期无法被确定下来,只能够在程序运行期根据实际类型绑定相关的方法。
- 非虚方法:
- 如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的,这样
的方法称为非虚方法 - 静态方法、私有方法、
final
方法、实例构造器、父类方法都是非虚方法。 - 其他方法称为虚方法
- 如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的,这样
子类对象的多态性使用前提 ① 类的继承关系 ② 方法的重写
虚拟机中方法调用指令:
普通调用指令:
1) invokestatic:调用静态方法,解析阶段确定唯一方法版本。
2) invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本。
3) invokevirtual:调用所有虚方法。
4) invokeinterface:调用接口方法。
动态调用指令:
5) invokedynamic:动态解析出需要调用的方法,然后执行
invokeinterface:固化在虚拟机内部,方法的调用执行不可人为干预。
invokedynamic:指令支持用户确定方法版本。
invokestatic:指令和invokespecial指令调用的方法称为非虚方法,其余(final修饰除外)称为虚方法。
三、堆
1、堆的核心概述
- 一个 JVM 实例只存在一个堆内存,堆也是Java内存管理的核心区域。
- Java对区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大的一块内存空间。
- 堆内存的大小是可以调节的
- 《JVM虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上他应该被视为连续的。
- 所有线程共享Java堆,在这里还可以划分线程私有的缓存区(TLAB)。
- 《JVM虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当运行时分配在堆上。
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或数组在堆中的位置。
- 在方法结束后,堆中的对象不会被马上移除,仅仅在垃圾收集的时候才会被移除。
- 堆,是GC 执行垃圾收集的重点区域。
2、内存细分
- JAVA 7及之前堆内存逻辑上划分为三个部分 新生区 + 养老区 + 永久区
- 其中永久区又划分为 Eden区和Survivor区
- Java 8 及之后堆内存逻辑上划分为三个部分 新生区 + 养老区 + 元空间
- 约定 新生区 <=> 新生代 <=> 年轻代 | 养老区 <=>老年区 <=> 老年代 | 永久代 <=> 永久区
3、堆空间大小设置
- -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
- -X 是jvm的运行参数
- ms 初始内存大小
- 支持 k、m、g单位
- -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
- 默认堆空间大小
- 初始内存大小:电脑物理内存大小 / 64
- 最大内存大小: 电脑物理内存大小 / 4
- 在开发中一般建议将初始堆内存大小和最大堆内存大小设置一致。
- 查看设置的参数:
- 方式一: jsp / jstat -gc 进程ID
- 方式二:-XX:+PrintGCDetails
4、年轻代与老年代
-
存储在JVM中的Java对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。
- 另一类对象的生命周期却非常长,在某些极端情况下还能够与JVM生命周期保持一致
-
Java堆区进一步细分的话,可以划分为年轻代与老年代
-
其中年轻代又可以划分为Eden空间、Survivor0空间、Survivor1空间(有时也叫做 from区、to区)。
-
配置新生代与老年代在堆结构中占比(一般不会修改)
-
默认-XX:NewRatio=2 , 表示新生代占1,老年代占2,新生代占整个堆的1/3
-
可以修改 -XX:NewRatio=4,表示新生代占1,老年代占4,新生代占老年代的1/5.
-
如果明确知道有较多生命周期较长的对象,则可以适当的调大老年代大小。
-
-
配置Eden区和Survivor区内存占比
- -XX:SurvivorRatio :设置Eden区和Survivor区内存占比,如-XX:SurvivorRatio=8
- 我们知道在默认情况下Eden区和Survivor占比为8:1:1,但实际为6:2:2,需要通过-XX:SurvivorRatio=8参数才能调整为8:1:1
-
-Xmn设置新生代空间大小(一般不设置)
5、对象分配的一般过程
总结说明
- 针对幸存者s0,s1区的总结: 复制之后有交换,谁空谁是to。
- 关于垃圾回收:频繁收集新生代,很少收集老年代,几乎不动永久代/元空间
6、对象分配的特殊过程
7、常用调优工具
- JDK命令行
- Eclipse: Memory Analyzer Tool
- Jconsole
- VisualVM
- Jprofiler
- Java Flight Recorder
- GCViewer
- GC Easy
8、Minor GC、Major GC和Full GC
- JVM在进行GC时,并非每次都对三个内存(新生代、老年代、方法区)区域一起进行回收的,大部分时候的回收是指新生代。
- 针对HotSpot VM的实现,它里面的GC是按照回收区域划分为两大种类:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
- 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为
- 新生代收集(Minor GC / Young GC):只是新生代(Eden/s0,s1)的垃圾收集。
- 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。
- 目前,只有CMS GC会有单独的收集老年代的行为
- 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收
- 混合收集(Mixed GC):收集整合新生代以及部分老年代的垃圾收集。
- 目前,只有G1 GC会有这种行为
- 整堆收集(Full GC):收集整个Java对和方法区的垃圾收集。
9、年轻代GC(Minor GC) 触发条件
- 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会触发GC(每次Minor GC 会清理年代内存)
- 因为Java对象大多具备朝生夕死的特性,所以Minor GC 非常频繁,一般回收速度也比较块。
- Minor GC会引发STW(stop the world),暂停其它用户线程,等待垃圾回收,用户线程才恢复运行。
10、老年代GC(Major GC/Full GC)触发机制
- 指发生在老年代的GC,对象从老年代消失时,我们说"Major GC" 或"Full GC"发生了。
- 出现Major GC,经常会伴随至少一次Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略中就有直接进行Major GC的策略选择过程)
- 也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC
- Major GC的速度一般比Minor GC慢10倍以上,STW时间更长。
- 如果Major GC后,内存还不足,则报OOM。
11、Full GC触发机制
- 调用
System.gc()
时,系统建议执行Full GC,但是不必然执行 - 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代可用内存。
- 由Eden区,from区向to区复制时,对象大小大于to区可用内存,则把对象转存到老年代,且老年代的可用内存小于该对象大小
说明:full gc是开发或者调优中要尽量避免的,这样暂停时间会短一些
12、内存分配策略
- 优先分配到Eden
- 大对象直接分配到老年代
- 尽量避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 动态对象年龄判断
- 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等待 到达年龄阈值。
- 空间分配担保
- -XX:HandlePromotionFailure
- 表示如果Survivor区中无法容纳的对象,存放到老年区,这称为空间分配担保。
13、TLAB
14、堆空间参数设置
- -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
- -XX:+PrintFlagsFinal : 查看所有参数的最终值(即修改后的值)
- -Xms : 初始堆空间大小(默认物理内存1/64)
- -Xmx : 最大堆空间大小(默认物理内存1/4)
- -Xmn : 设置新生代的大小。
- -XX:NewRatio : 配置新生代和老年代在堆结构的占比
- -XX:SurvuvirRatio :设置新生代中Eden和S0/S1空间的比例
- -XX:MaxTenuringThreshold :设置新生代垃圾的最大年龄
- -XX:PrintGCDetails :输出现象的GC处理日志
- -XX:PrintGC :打印GC简要信息
15、堆是分配对象存储的唯一选择吗?
不是的。
逃逸分析:代码优化
四、方法区
《JVM规范》中明确说:“尽管所有的方法区在逻辑上是属于堆的一部分,但是一些简单的实现可能不会选择去进行垃圾回收或进行压缩”.但对于HotSpotJVM而言,方法区还有一个Non-Heap(非堆),目的就是要和堆分开。所以方法区看作是一个块独立于Java堆的内存空间
1、方法区的基本说明
2、设置方法区大小和OOM
3、方法区内部结构
- 方法区存储什么?
- 它用于存储已被虚拟机加载的类型信息(包括类、接口、枚举、注解等)、常量、静态变量、即时编译器编译后的代码缓存等
- 它用于存储已被虚拟机加载的类型信息(包括类、接口、枚举、注解等)、常量、静态变量、即时编译器编译后的代码缓存等