2. 运行时数据区概述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FuIwJoCV-1614932926381)(JVM.assets/image-20210302084025635.png)]
2.1 程序计数器
JVM的PC寄存器是对物理PC寄存器的一种抽象模拟。PC寄存器用来储存指向下一条指令的地址。由执行引擎读取下一条指令。是很小的内存空间,也是运行速度最快的存储区域。
面试问题1:使用PC寄存器存储字节码指令地址有什么作用?
CPU需要不停的切换不同的线程,这样切换回来的时候就知道从哪里继续执行。
面试问题2:为什么使用PC就存起记录当前线程的执行地址
JVM的字节码解析器是需要通过改变PC寄存器的值来明确下一条需要执行什么样的字节码指令。
2.2 虚拟机栈
由于跨平台的设计,java指令都是根据栈来设计的。
栈是运行单位,堆是存储单位。
每个线程创建的时候,都会创建一个虚拟机栈,其内部保存一个个栈帧,虚拟机栈的生命周期和线程一致。栈是一种快速有效的分配存储的方式,访问速度仅次于程序计数器。
作用:
- 主管java程序的运行
- 保存方法的局部变量、部分结果并且参与方法的调用和返回
可能出现的错误
- JVM规定允许java的栈是动态的动态的或者固定不变的
- 采用固定大小的java虚拟机栈:当线程请求栈容量超过java虚拟机栈允许的最大容量,java虚拟机会抛出StackOverflowError错误。
- 采用动态的,当尝试扩展到无法申请足够的内存时,java虚拟机会抛出OOM错误(OutofMemeoryError)。
-Xss
:设置虚拟机栈的大小(在IDEA里面中虚拟机的设置可以调节虚拟机栈的大小)。栈的大小直接决定了方法调用的最大可达深度。
2.2.1 栈帧----java方法
一个栈帧对应着一个java方法。
当前栈帧:只有当前正在执行的栈帧(栈顶栈帧)是有效的。当前栈帧对应的方法是当前方法,定义这个方法的类是当前类。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5f8qvoup-1614932926382)(JVM.assets/image-20210302105820399.png)]
局部变量表
局部变量表:定义为一个数字数组,主要存储方法参数和定义在方法内部的局部变量(数据类型:基本数据类型,对象引用和returnAddress类型);布局变量表是线程私有数据,不存在数据安全问题;局部变量表所需要的容量大小是在编译期确定下来的。
变量槽
是局部变量表的基本存储单元。在局部变量表中32位以内的类型只占一个slot,64位的类型(long double)占用两个slot。JVM通过索引定位的方式来使用局部变量表,索引值的范围是从0开始到局部变量表的最大的变量槽数量。局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在这个方法中可通过关键词this
来访问这个隐含的参数。变量槽是可以复用的。
-
成员变量:在使用前都经历过默认初始化赋值。(类变量:在连接阶段的准备阶段给类变量默认赋值,在初始化阶段给类变量显示赋值及代码块;实力变量:随着对象创建会在堆空间中分配实例变量空间,并进行默认赋值)
-
局部变量:在使用前必须要进行显示赋值,否则编译不通过。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈
操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据即为入栈和出栈。主要存储就散的中间过程,同时作为计算过程中变量的临时存储空间。操作数栈并非使用访问索引的方式来进行数据访问(虽然实现方式是数组),通过入栈和出栈操作来完成的。
栈顶缓存技术:将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读写次数,提高执行引擎的执行效率。
动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是支持当前方法的代码能够实现动态链接。
动态链接的作用是将这些符号引用转化为调用方法的直接引用。
在java源文件编译为字节码文件时,所有变量和方法引用都作为符号引用保存在class的常量池中。
(1)方法的调用
方法的绑定机制:
- 静态链接:当字节码文件被装载进JVM内部时,若被调用的目标方法在编译期可知,且运行期保持不变,这种情况下调用方法的符号引用转化为直接引用的过程是静态链接。早起绑定
- 动态链接:若调用的方法在编译器不能被确定下来,也就是说,只能在程序运行期将调用方法的符号引用转化为直接引用,这种引用转换过程具备动态性,因此被称为动态链接。晚期绑定
java中任何一个普通的方法其实都具备虚函数的特征,如果想让java不具备,可以添加关键字final
。
(2)虚方法与非虚方法
非虚方法:
- 定义:方法在编译期就确定了具体的调用版本,这个版本在运行中是不可变的,被称为非虚方法
- 静态方法、私有方法、final方法、势力狗欧早期和父类方法都是非虚方法。其余的方法都是虚方法。
方法调用字节码指令:
- invokestatic:调用静态方法
- invokespecial:调用构造器、私有方法和父类方法
- invokevirtual:调用所有虚方法(final修饰的也是指令,但是是非虚方法)
- invokeinterface:调用接口方法,运行时在确定实现该接口的对象。
- invokedynamic:动态解析出需要的方法,然后执行。(lambda表达式)
前四个分配逻辑固化在JVM中,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。
动态类型语言(js,python)和静态类型语言(java,c++):区别是对类型的检查是在编译期还是在运行期。
JVM为了提高动态分配的性能,JVM在类的方法区建立一个虚方法表来实现,用索引表代替查找。虚方法表在类加载的链接阶段创建并开始初始化。虚方法表实际存放着各个方法的实际入口地址。
方法返回地址
方法的返回地址:存放调用该方法的PC寄存器的值。
方法的结束:
- 正常执行结束。(调用者的PC寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址)
- 出现未处理的异常,非正常退出。(返回的地址要通过异常表来确定,栈帧一般不会保存这部分信息)
2.2.2 常见的面试题
举例栈溢出的情况?
调整栈大小,就能保证不出现栈溢出吗?
分配的栈内存越大越好吗?
垃圾回收是否涉及到虚拟机栈?
方法中定义的局部变量是否线程安全 ?
2.3 本地方法栈
2.3.1 本地方法接口JNI
标识符 native
可以与所有其他的java标识符练游泳,除了abstract
。
java应用需要与java外面的环境交互,这就是本地方法存在的主要原因。
2.3.2 本地方法栈
java虚拟机栈是管理java方法的调用,本地方法栈用于管理本地方法的调用
错误:
- 固定大小 :栈溢出StackOverflowError
- 动态扩展:内存溢出OOM
本地方法栈登记native方法,在执行引擎执行时加载本地方法库
2.4 堆
对一个进程而言,堆和方法区都是唯一的。在这个进程里面的线程是共享堆和方法区的。对于一个线程而言,程序计数器、虚拟机栈和本地方法栈是不共享的。
堆是java内存管理的核心区域,在JVM启动的时候就被创建,其空间大小也确定了。是JVM最大的一块内存空间,在物理上不连续的内存空间,在逻辑上是连续的。堆内存是可以调节的(-Xmx
(堆空间最大内存)和-Xms
(堆区起始内存))。
- 所有对象实例及数组都应当在运行时分配在堆上。
- 数组和对象可能永远不会存储在栈上,因为栈帧存储引用,这个引用指向对象或者数组在堆中的位置。
- 在方法结束后,堆中对象不会马上被移除,只有在垃圾收集的时候才会被移除
- 堆是GC(垃圾收集器)执行垃圾回收的重点区域。
内存细分
现代垃圾收集器大部分都基于分代收集理论设计
Java7之前:新生区+老年区+永久区
java8之后:新生代 +老年代+元空间
堆空间大小设置
-Xms
:设置堆空间起始大小-X
:jvm运行参数ms
:memory start
-Xmx
:设置堆空间最大内存
一旦堆空间中内存大小超过 -Xmx
所指定的最大内存时,会抛出OOM异常。
通常设置 -Xms
和 -Xmx
两个参数配置相同的值,目的是能够在java垃圾回收机制清理完堆内存后不需要重新分隔计算堆区大小,从而提高性能。
默认情况:起始内存大小:物理内存/64,最大内存大小:物理内存/4
新生代和老年代
存储在JVM中的java对象有两类:
- 生命周期较短的瞬时对象
- 生命周期较长的,极端点可以和JVM的生命周期保持一致
配置新生代与老年代在堆中占比:(一般不修改参数)
- 默认:
-XX:NewRatio=2
表示新生代占1 (1/3),老年代占2。 - 可以修改
-XXwRatio=4
表示新生代占1 (1/5) 老年代占4
-XX: -UseAdaptiveSizePolicy
:关闭自适应内存分配策略
-XX:SurvivorRatio=8
:伊甸园区:幸存者0区:幸存者1区=8:1:1
-
几乎所有的对象都是在伊甸园区被new出来的
-
绝大部分对象都被销毁在新生代(80%)
使用 -Xmn
:设置新生代最大内存大小(一般使用默认值)(与上面冲突了,以这个为准)
对象分配的一般过程
重点:
- 幸运者0区(s0)和幸运者1区(s1):复制之后有交换,谁空谁为to
- 关于垃圾回收:频繁在新生代收集,很少在老年代收集,几乎不在元空间收集。
特殊情况:超大对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ytHUm5Js-1614932926388)(https://i.loli.net/2021/03/05/gW5zuaEoq7yOHRP.png)]
常见的调优工具
- JDK命令行
- Eclipse:Memeory Analyzer Tool
- Jconsole
- VisualVM
- Jprofiler
- Java Flight Recorder
- GCViewer
- GC Easy
Minor GC 、Major GC和Full GC
- 部分收集:不是完整收集整个java堆中垃圾收集:
- 新生代收集:Minor GC(YGC):只是新生代垃圾收集。当新生代空间不足,会触发Minor GC(指的是Eden区空间满了)(一般java对象都具备朝生夕灭的特性,Minor GC非常频繁,一般回收速度比较快)(Minor GC会引发STW,暂停别的用户线程,等垃圾回收结束,用户线程才恢复运行)
- 老年代收集:Major GC:只是老年代垃圾收集。
- 注意:只有CMS收集器有单独收集老年代的行为
- 注意:有时候MajorGC和FullGC混淆使用,需要具体判断是整堆垃圾回收还是老年代垃圾回收
- 触发机制:老年代空间不足,会触发Major GC,其速度比Minor GC慢10倍,而且STW时间更长。如果Major GC后内存还是不够,就抛出OOM了。
- 混合收集:Mixed GC:整个新生代和部分老年代垃圾收集
- 目前只有在G1 GC会有这个行为
- 整堆收集:Full GC:收集整个堆(新生代和老年代)和方法区的垃圾收集。开发过程中尽量避免Full GC,这样暂停时间会短点。
STW
:Stop the World
-XX:+PrintGCDetails
:控制台打印垃圾回收细节
堆空间分代思想
分代的唯一理由:优化GC性能。
内存分配策略
- 优先分配到Eden区
- 大对象直接分配到老年代(尽量避免程序中出现过多的大对象)
- 长期存活的对象分配到老年代
- 动态对象年龄判断:若survivor去中相同年龄的所有对象总和大于survivor区的空间一半,年龄大于或者等于该年龄的对象直接进入老年代,无需等待maxTenuringThreshold中要求的年龄。
- 空间分配担保:
-XX:HandlePromotionFailure
。
为对象分配内存:TLAB(Thread Local Allocation Buffer)
在Eden区域中有一块JVM为每个线程分配了一个私有缓存区域。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提高内存分配的吞吐量:这种内存分配机制为快速分配策略
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oQqEv0rJ-1614932926388)(https://i.loli.net/2021/03/05/D7ovqMaImy4Vc9C.png)]
- JVM将TLAB作为内存分配的首选(尽管不是所有对象实例都可以分配到)
-XX:UseTLAB
设置是否开启TLAB空间。默认是开启的。- TLAB空间占内存Eden空间的1%。可以通过设置
-XX:TLABWasteTargetPercent
设置TLAB空间所占用Eden空间的百分比大小 - 一旦对象在TLAB空间分配内存失败,JVM会尝试通过加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
小结:堆空间的常用参数设置
-XX:PrintFlagsInitial
:查看所有参数 的默认初始值-XX:+PrintFlagsFinal
:查看所有参数的最终值-Xms
:初始堆空间内存,(物理内存/64)-Xmx
:最大堆空间内存,(物理内存/4)-Xmn
:设置新生代空间(初始值即最大值)-XX:NewRatio
:设置新生代与老年代在堆结构的占比-XX:SurvivorRatio
:设置新生代中Eden与S0或S1的空间比例-XX:MaxTenuringThreshold
:设置新生代垃圾的最大年龄-XX:+PrintGCDetails
:输出详细的GC处理日志-XX:HandlePromotionFailure
:是否设置空间分配担保- 打印gc简要信息:
-XX:PrintGC
和-verbose:gc
堆是分配对象存储的唯一选择吗?
在jvm中,对象是在堆中分配内存,但是有一种特殊情况。若经过逃逸分析后发现一个对象没有逃逸出对象,则可能被优化在栈上分配,这样就无需在堆上分配内存,也无需进行垃圾回收,这是最常见的堆外存储技术。
逃逸分析:是一种可以有效减少java程序中同时负载和内存堆分配压力的跨函数全局数据流分析算法。
- 经过逃逸分析,Hotspot编译器能够分析出新的对象引用使用范围而决定这个对象要不要分配在堆中
- 逃逸分析的基本行为:分析对象动态作用域
- Hotspot默认开启逃逸分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Au6dEZSm-1614932926389)(https://i.loli.net/2021/03/05/j7dv4SKWBIyPZtr.png)]
结论:开发能使用局部变量的,就不要使用在方法外定义
编译器代码优化:栈上分配 同步省略 分离对象或者标量替换
基于逃逸分析,编译器代码优化:
-
栈上分配:将堆分配转化为栈分配。
-
同步省略:如果一个对选哪个被发现只能从一个线程被访问到,那么对于这个对象可以不考虑同步操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QprObAuB-1614932926390)(https://i.loli.net/2021/03/05/UJi1sVexmTKRHuN.png)]
-
分离对象或标量替换:如果有些对象不需要作为一个连续的内存存储存在被访问呢,那么对象可以部分或者全部不存储在内存中,而存在CPU寄存器中
常见的逃逸了:成员变量赋值、方法返回值和实例引用传递。
2.5 方法区
2.5.1 栈、堆和方法区的交互关系
-
线程共享角度:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4cYqFWpM-1614932926390)(https://i.loli.net/2021/03/05/s2DFWiZXAQgelMa.png)]
-
交互
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IK30isSH-1614932926391)(https://i.loli.net/2021/03/05/sDvUqSa8tyEXIPc.png)]
2.5.2 方法区(元空间)的理解
方法区: 尽管所有的方法区在逻辑上都是堆的一部分,但是简单的实现可能不会选择去进行垃圾收集或者进行压缩,方法区还有一个别名是非堆区。方法区可以看做一个独立于Java堆的内存空间。
- 线程共享
- 实际的物理内存可以不连续
- 内存大小可以固定或者可扩展
- 方法区的大小决定了系统可以保存多少个类 ,若定义太多类的话,可以会出现OOM:MetaSpace错误
- 关闭JVM会释放方法区的内存
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gi8wuFbR-1614932926391)(https://i.loli.net/2021/03/05/Qvr4FLNkR1iVtcB.png)]
元空间和永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
2.5.3 设置方法区大小
-XX:MetaspaceSize
:初始方法区大小(windows默认情况下21m)-XX:MaxMetaspaceSize
:最大方法区大小(值为-1时,没有限制)
内存泄漏:分配的内存空间没有及时回收。
2.5.4 方法区的内部结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HkzqUa07-1614932926392)(https://i.loli.net/2021/03/05/fuh76yX2dtplcxg.png)]
方法区主要存储内容:用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ve4P5uW7-1614932926392)(https://i.loli.net/2021/03/05/VjXaIFxPNil3ucK.png)]
一般来说类型信息=类型信息+域信息+方法信息
类型(类 接口 枚举 注解)信息
- 类型完整的有效名称(包名.类名)
- 类型直接父类的完整有效名称
- 类型的修饰符(public abstract final等)
- 类型直接接口的一个有序列表
域信息
- JVM必须在方法区中保存类型所有域的相关信息和域的声明顺序
- 域的相关信息:域名城、域类型、域修饰符(public static final volatitle transient。。。)
方法信息
- 方法名称
- 方法返回类型
- 方法修饰符(public static final synchronized native abstract。。。)
- 方法的字节码 操作数栈的深度 局部变量表的大小
- 异常表(abstract native方法除外)
静态变量
被final
修饰的类变量(static)在编译的时候就被分配了
运行时常量池
(1)常量池表
.class文件常量池表有
- 数值量
- 字符串值
- 类引用
- 字段引用
- 方法引用
字面量:表达源代码中一种固定值的表达法(整数、浮点数以及字符串等)
常量池可以看做成一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型和字面量类型。
(2)运行时常量池
- 运行时常量池:方法区的一部分。相对于常量池表具备动态性。通过索引访问
- 常量池表:示class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容会在类加载后放到方法区的运行时常量池中。
方法区的演变过程
在jdk1.8之后:类型信息、字段、方法和常量保存在本地内存的元空间,而字符串常量池和静态变量在堆中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cND6jJ8z-1614932926393)(https://i.loli.net/2021/03/05/JqFubzBeO46SyvQ.png)]
为什么要用元空间替换永久代?
- 为永久代设置空间大小很难确定
- 对永久代进行调优比较困难
为什么字符串常量池(StringTable)要转移到堆中?
- 在永久代中(只有当老年代空间不足或者永久代空间不足会触发full gc)字符串常量池回收效率很低
- 在堆中能够及时回收内存
如何证明静态变量存储在哪里?
-
private static byte[] arr=new byte[1024*1024*100];
-
XX:+PrintGCDetails
:这样打印出的看看老年代中是否有100m内存
静态引用对应的对象实体始终存在于堆空间中。
2.5.5 方法区的垃圾收集
方法区的垃圾回收主要回收两个部分:
- 常量池中废弃的常量
- 不再使用的类型