Java虚拟机(JVM)面试专题 上(初级程序员P6)
面试题:-Xmx1024m -Xms10240m -Xmn10240m -XX:SurvivorRatio=4 其最小内存只是多少?Survivor区总大小分别是多少?
垃圾回收器 - ConcurrentMarkSweep GC(基本废弃)
Java虚拟机(JVM)面试专题 上(初级程序员P6)
一、JVM 内存结构
1. 内存结构与执行流程
Java虚拟机(JVM)的内存结构如下图所示:
public class Demo {
public static void main(String[] args){
Student stu = new Student();
stu.study();
stu.hashCode();
stu = null;
}
}
我们自己编写的一段Java代码,指的就是Java Source(源程序),将其编译后,其执行流程如下所示:
执行 javac 命令编译源代码为字节码
执行 java 命令
-
创建 JVM,调用类加载子系统加载 class(这里就是Demo类),将类的信息存入方法区。
-
创建 main 线程,使用的内存区域是 JVM 虚拟机栈,开始执行 main 方法代码。
-
如果遇到了未见过的类(这里指的是Student类),会继续触发类加载过程,同样会存入方法区。
-
需要创建对象(比如这里的 new Student()),会使用堆内存来存储对象。
-
不再使用的对象,会由垃圾回收器在内存不足时回收其内存。
-
调用方法时,方法内的局部变量、方法参数(这里指的就是stu、args)所使用的是 JVM 虚拟机栈中的栈帧内存。
-
调用方法时,先要到方法区获得到该方法的字节码指令,由解释器将字节码指令解释为机器码执行。
-
调用方法时,会将要执行的指令行号读到程序计数器,这样当发生了线程切换,恢复时就可以从中断的位置继续。
-
对于非 java 实现的方法调用,使用内存称为本地方法栈(比如这里的hashCode(),但是这只是这么设计,Oracle公司的HotSpot虚拟机就没有分这么细,而是将两者合二为一都叫做虚拟机栈)
-
对于热点方法调用,或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存,提高执行性能。
JVM内存结构的线程分类
线程私有
程序计数器、JVM虚拟机栈(虚拟机栈、本地方法栈)
线程共享
堆、方法区
2. 内存溢出
什么是内存溢出?什么时候会发生?
该区域的内存被耗尽了、报错了,就是内存溢出!
除了程序计数器,其他的区域都有可能发生内存溢出。
(1)出现 OutOfMemoryError 的情况
-
堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收。
-
方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类。(少见)
-
虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时。
(2)出现 StackOverflowError 的区域
- JVM 虚拟机栈内部的方法递归调用未正确结束、反序列化 json 时循环引用。(线程内的方法调用过多,把该线程中的 1M内存耗尽导致的)
3. 方法区与永久代、元空间之间的关系
-
方法区:JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
-
永久代(过时了): Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)
-
元空间: Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间
所以总的来说,方法区是JVM规范中的一种定义(即在内存中需要有能实现其功能的区域),而永久代和元空间是方法区的具体实现。
(1)元空间的元数据创建
下图中,“箭头”代表时间线,“Heap”就是堆内存,“Metaspace”就是元空间。
-
当第一次用到某个类是,由类加载器将 class 文件的类元信息读入,并存储于元空间。
-
X,Y 的类元信息是存储于元空间中,无法直接访问。
-
可以用 X.class,Y.class 间接访问类元信息,它们俩属于 java 对象,我们的代码中可以使用。
(2)元空间的元数据删除
-
堆内存中:当一个类加载器对象,这个类加载器对象加载的所有类对象,这些类对象对应的所有实例对象都没人引用时,GC 时就会对它们占用的对内存进行释放。
-
元空间中:内存释放以类加载器为单位,当堆中类加载器内存释放时,对应的元空间中的类元信息也会释放。
二、JVM 内存参数
我们使用idea编写代码的时候,肯定有查看过其配置文件,里面有jvm内存参数的配置。
面试题:-Xmx1024m -Xms10240m -Xmn10240m -XX:SurvivorRatio=4 其最小内存只是多少?Survivor区总大小分别是多少?
最小堆内存和最大堆内存大小都是10GB,新生代内存大小为5GB(总共10GB,新5GB,老5GB)
new 表示的就是新生代,from 和 to 的内存大小总是相等的。
-XX:SurvivorRatio=4 表示 eden占4份内存,from和to各占 1 份,所以Survivor区总大小就是from + to的总大小即 5GB / 6(份) * 2
(1)堆内存,按比例设置
参数含义如下
-
-XX:NewRatio=2:1 表示老年代占两份,新生代占一份
-
-XX:SurvivorRatio=4:1 表示新生代分成六份,伊甸园占四份,from 和 to 各占一份
(2)堆内存,按大小设置
-
-Xms 最小堆内存(包括新生代和老年代)
-
-Xmx 最大对内存(包括新生代和老年代)
-
通常建议将 -Xms 与 -Xmx 设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好
-
-XX:NewSize 与 -XX:MaxNewSize 设置新生代的最小与最大值,但一般不建议设置,由 JVM 自己控制
-
-Xmn 设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等
-
保留是指,一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存。
(3)元空间内存设置
要将元空间细分,可以将其分成两类,例如:class space 和 non-class space
-
class space 存储类的基本信息,最大值受 -XX:CompressedClassSpaceSize 控制
-
non-class space 存储除类的基本信息以外的其它信息(如方法字节码、注解等)
class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制。
注意:
-
这里 -XX:CompressedClassSpaceSize 这段空间还与是否开启了指针压缩有关,默认是开启的。
(4)代码缓存内存设置(codecache)
之前提到的 即时编译器 会使用到这个东西!
-
如果 -XX:ReservedCodeCacheSize < 240m,所有优化机器代码不加区分存在一起
-
如果 -XX:ReservedCodeCacheSize >= 240m,分成三个区域(图中mthod是method少了一个e)
-
non-nmethods:JVM 自己用的代码
-
profiled nmethods:部分优化的机器码
-
non-profiled nmethods:完全优化的机器码
-
(5)线程内存设置
参数就是 -Xss
它的大小与操作系统有关,如果是Linux默认一个线程内存大小是 1MB
三、JVM 垃圾回收
1. 三种垃圾回收算法
(1)标记清除法
解析
-
找到 GC Root 对象,即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象(正在使用)、静态变量引用的对象
-
标记阶段:沿着 GC Root 对象的引用链找,直接或间接引用到的对象加上标记
-
清除阶段:释放未加标记的对象占用的内存
要点
-
标记速度与存活对象线性关系
-
清除速度与内存大小线性关系
-
缺点是会产生内存碎片(如果此时需要申请大片的连续空间,就会出现问题!)
-
目前该方法已经不被使用了!
(2)标记整理法
解释
-
前面的标记阶段、清理阶段与标记清除法类似
-
多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片产生
特点
-
标记速度与存活对象线性关系
-
清除与整理速度与内存大小成线性关系
-
缺点是性能上较慢
(3)标记复制法
解析
-
将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象
-
标记阶段与前面的算法类似
-
在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理
-
复制完成后,交换 from 和 to 的位置即可
特点
-
标记与复制速度与存活对象成线性关系
-
缺点是会占用成倍的空间
三种垃圾回收算法总述
标记清除法已经废弃,标记整理法 和 标记复制法 都还在使用!
标记整理法 主要用于老年代的垃圾回收
标记复制法 主要用于新生代的垃圾回收
新生代存活对象比较少,老年代则反之;所以新生代使用 标记复制法 ,需要复制的对象就少,效率高!
2. GC 与 分代回收算法
(1)GC 垃圾回收
GC是垃圾回收的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃。
GC 目的
实现无用对象内存自动释放,减少内存碎片、加快分配速度
GC 超重点
-
回收区域是堆内存,不包括虚拟机栈
-
判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
-
GC 具体的实现称为 垃圾回收器
-
GC 大都采用了分代回收思想
-
理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收
-
根据这两类对象的特性将回收区域分为新生代和老年代,新生代采用标记复制法、老年代一般采用标记整理法
-
(2)GC规模
根据 GC 的规模可以分成 Minor GC,Mixed GC,Full GC
-
Minor GC 发生在新生代的垃圾回收,暂停时间短
-
Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
-
Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免
(3)分代回收
新生代 = eden + from + to
A. 伊甸园 eden,最初对象都分配到这里,与幸存区 survivor(分成 from 和 to)合称新生代
B. 当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象
C. 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放
D. 将 from 和 to 交换位置
E. 经过一段时间后伊甸园的内存又出现不足
F. 标记伊甸园与 from 的存活对象
G. 将存活对象采用复制算法复制到 to 中
H. 复制完毕后,伊甸园和 from 内存都得到释放
I. 将 from 和 to 交换位置
J. 老年代 old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
3. 三色标记
之前我们提到了,怎么看该对象是否还在被使用,就是将被使用的对象标记起来,不被标记的对象将会被视为垃圾,被回收。所以我们用三种颜色记录对象的标记状态。
-
黑色 – 已标记
-
灰色 – 标记中
-
白色 – 还未标记
(1)起始的三个对象还未处理完成,用灰色表示
(2)该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色
(3)依次类推
(4)沿着引用链都标记了一遍
(5)最后为标记的白色对象,即为垃圾
4. 并发漏标问题
用户编写的业务逻辑代码是通过用户线程来运行的,而JVM中有垃圾回收线程,在早期这两种线程是不允许同时工作的!
但是现在比较先进的垃圾回收器都支持与用户线程并发运行,都支持并发标记,即在标记过程中,用户线程仍然能工作。但这样带来一个新的问题,如果用户线程修改了对象引用,那么就存在漏标问题。
问题描述
(1)所示标记工作尚未完成
(2)用户线程同时在工作,断开了第一层 3、4 两个对象之间的引用,这时对于正在处理 3 号对象的垃圾回收线程来讲,它会将 4 号对象当做是白色垃圾
(3)但如果其他用户线程又建立了 2、4 两个对象的引用,这时因为 2 号对象是黑色已处理对象了,因此垃圾回收线程不会察觉到这个引用关系的变化,从而产生了漏标
(4)如果用户线程让黑色对象引用了一个新增对象,一样会存在漏标问题
解决方案
因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:
(1)Incremental Update 增量更新法,CMS 垃圾回收器采用
- 思路是拦截每次赋值动作,只要赋值发生,被赋值的对象就会被记录下来(对于上面的案例就是黑色),在重新标记阶段再确认一遍
(2)Snapshot At The Beginning,SATB 原始快照法,G1 垃圾回收器采用
- 思路也是拦截每次赋值动作,不过记录的对象不同,也需要在重新标记阶段对这些对象二次处理
- 新加对象会被记录
- 被删除引用关系的对象也被记录(那两个白色的会被记录下来!)
5. 垃圾回收器
垃圾回收器 - Parallel GC
-
eden 内存不足发生 Minor GC(发生在新生代),采用标记复制算法,需要暂停用户线程(时间短)
-
old 内存不足发生 Full GC(新生代和老年代),采用标记整理算法,需要暂停用户线程(时间长)
-
注重吞吐量(牺牲一点响应时间)
垃圾回收器 - ConcurrentMarkSweep GC(基本废弃)
-
它是工作在 old 老年代,支持并发标记的一款回收器,采用并发清除算法
-
并发标记时不需暂停用户线程
-
重新标记时仍需暂停用户线程
-
-
如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
-
注重响应时间
垃圾回收器 - G1 GC(优秀)
从JDK9开始,作为默认的垃圾回收器
-
响应时间与吞吐量兼顾
-
划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
-
分成三个阶段:新生代回收、并发标记、混合收集
-
如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
6. G1 GC详解(重点)
G1 回收阶段 - 新生代回收
(1)初始时,所有区域都处于空闲状态
(2)创建了一些对象,挑出一些空闲区域作为 eden 伊甸园区存储这些对象
(3)当 eden 伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程(由于新生代大小受限,幸存对象较少,所以时间很短!)
注意:
eden区的大小会受到新生代大小的限制!
在G1里面新生代的内存占比是在 5% ~ 60%之间波动的!
当eden区的数量达到阈值,就会触发垃圾回收。
(4)复制完成,将之前的伊甸园内存释放
(5)随着时间流逝,伊甸园的内存又有不足
(6)将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代
(7)释放伊甸园以及之前幸存区的内存
G1 回收阶段 - 并发标记与混合收集
并发标记阶段的发生是有条件的!当老年代的占用内存超过阈值(45%)才会发生!
(1)当老年代占用内存超过阈值后,触发并发标记,这时无需暂停用户线程
(2)并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。
此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)即先回收存活对象少的老年区!
假设图中标红的区域表示存活对象少的老年代
(3)混合收集阶段中,参与复制的有 eden、survivor、old,下图显示了伊甸园和幸存区的存活对象复制
(4)下图显示了老年代(红)和幸存区晋升的存活对象的复制
eden 和 未达到晋升预值的幸存区复制到 “新的幸存区(蓝色的s)”
达到晋升预值的幸存区 和 回收价值较高的老年代 会复制到一个 “新的老年代”
(5)复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集