深入理解JVM底层实现
模拟面试题QA
田超凡
20191116
- JVM常用的调优参数
-xmx 设置最大堆内存
-xms 设置最小堆内存
-xmn 设置新生代堆内存,默认大小是堆内存的1/3,新生代和老年代在堆区中的内存分配比例是1:2
-xss 设置线程栈大小
- JVM运行时数据区域有哪几部分组成,各自作用
JVM运行时数据区主要有两部分组成:
(1).线程共享区:所有线程共享的JVM内存资源,所有线程都可以访问
线程共享区中有两个区域:
堆区:存放创建的对象,管理对象的生命周期
- 新生代
存放的是生命周期短的对象,对象在新生代中转瞬即逝
- Eden伊甸园区:新创建的对象默认优先放到Eden区
- Survivor 幸存者区:对象主要活动区域,使用复制算法控制对象年龄
- S0 区(From 区):复制算法作用起始区
- S1 区(To 区):复制算法作用目标区
- 老年代
存放的是新生代复制过来的长期存活的对象
方法区(元空间):存放运行时常量池(全局静态常量)、Class类型的对象、全局静态变量
(2).线程独占区:每个线程单独占有的JVM内存资源
虚拟机栈(Virtual Stack):存放线程运行时的局部变量
本地方法栈(Native Stack):存放的是JVM需要调用的本地native方法
程序计数器(Runner Counter):记录当前线程执行到的行数
线程独占区的栈区主要是由栈桢组成的,每个线程每次方法调用都会在当前线程栈区中创建一个栈桢,每个栈桢主要有4部分组成:
- 局部变量表:存放的是方法中定义的局部变量
- 操作数栈:使用栈结构来存放局部变量(后进先出 Last In First Out)
- 动态链接:存放当前线程对方法区(元空间)中的对象引用,比如方法中引用到了静态变量、静态常量,此时就会把指针引用转换成真实的引用。
- 返回地址:返回当前方法的执行结果
- GC算法有哪些?GC收集器有哪些?
GC垃圾回收算法有:
- .引用计数器算法
堆区中每个对象都使用一个计数器来记录当前对象的年龄,没执行一次复制算法,对象年龄都会+1,当达到默认分代年龄的时候,就会把对象从新生代复制到老年代
- .根搜索算法
把堆区中的对象分为可达对象和不可达对象
首先定义GC Roots根节点,GC Roots根节点会使用引用链连接所有可达对象,根搜索算法就是每次从GC Roots根节点出发,回收不在根节点引用链上的不可达对象。
- .标记清除算法
标记清除算法分为两个阶段:
对象标记:标记所有需要被垃圾回收的不可达对象(大多数说法是标记可达对象,但实际上这个说法不符合CMS垃圾回收器实现机制)
对象清除:清除所有上一步标记的需要被垃圾回收的对象
- .复制算法
- 当新生代Eden区执行YGC后,会把Eden区中存活的对象复制到Survivor S0区
- 当新生代Survivor S0区存满之后,会把Eden区和Survivor S0区的幸存对象复制到Survivor S1区
- 当新生代Survivor S1区存满之后,会把Survivor S1区中幸存的对象再次复制到Survivor S0区,依次类推,把对象在Survivor S0区和Survivor S1区之间来回复制。
- 当新生代中的对象达到分代大小或者分代年龄(默认分代年龄是15,可以通过JVM参数动态调整)之后,会把对象从新生代复制到老年代。
- .标记压缩算法
在标记清除算法的基础上来解决内存碎片问题,主要实现策略是对老年代中的对象活动区域进行动态压缩,包括任意顺序、线形顺序、滑动顺序
- .分代算法
根据对象的生命周期把对象活动区域分为新生代和老年代
新生代存放生命周期短、临时的对象
老年代存放生命周期长、长期存在的对象
分代算法可以有助于GC回收更加高效
GC垃圾回收器都是基于不同的GC垃圾回收算法实现的
新生代垃圾回收器都是基于复制算法实现的
老年代垃圾回收器都是基于标记清除算法和标记压缩算法实现的
- 哪些可以作为 GC Roots 的对象
- .虚拟机栈中的局部变量
- .方法区(元空间)中的全局静态变量
- .方法区(元空间)中的全局静态常量
- .本地方法栈中的native方法
- 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms
新生代常用的垃圾回收器有:
Serial 串行收集器,支持单线程垃圾回收,如果是多核服务器会导致效率降低
ParNew 并行收集器,支持多线程垃圾回收,如果是单核服务器会导致效率降低
ParallelScavenge 吞吐量优先收集器,吞吐量=用户本地代码执行时间/(用户本地代码执行时间+GC垃圾回收时间)
老年代常用的垃圾回收器有:
CMS 并行标记清除(Concurrent Mark Sweep)
支持多线程垃圾回收,主要基于根搜索算法+标记清除算法实现,垃圾回收需要经历四个阶段:
- .初始标记:在该阶段会把老年代所有GC Roots直接连接的对象进行标记
- .并发标记:使用多线程并行的方式标记所有老年代GC Roots引用链上的可达对象
- .并发预处理:对并发标记中标记的新晋升到老年代的对象进行标记
- .重新标记:标记所有新生代引用的老年代的对象,需要重新扫描整个堆区
- .并发清除:使用多线程并行的方式对老年代没有标记的对象进行垃圾回收
- .并发重置:重置清除完垃圾对象之后的老年代内存空间
注意:老年代CMS垃圾回收器只能和新生代Serial、ParNew垃圾回收器结合使用,不能和ParallelScavenge垃圾回收器结合使用。
HotSpot VM 默认使用的垃圾回收器都是并行收集器(新生代ParNew+老年代CMS)
Serial Old 串行垃圾回收,不常用
Parallel Older 吞吐量优先垃圾回收,不常用
- 什么是Full GC?minor GC? major GC? STW?
Full GC指的是对整个堆区中的对象进行垃圾回收,等效于Minor gc+Major gc
Minor GC指的是对新生代中的对象进行垃圾回收,Minor gc会触发STW
Major GC指的是对老年代中的对象进行垃圾回收,Major gc会触发Full gc
STW指的是Stop The World,除当前垃圾回收线程外的其他线程都会暂停。
7.JVM中一次完整的GC流程(从YGC到FGC)是怎样的,重点讲讲对象如何晋升到老年代
(1).新生代中的对象每执行一次复制算法进行YGC的时候,对象的年龄就会+1,当达到默认分代年龄的时候,就会把对象从新生代复制到老年代
(2).新生代中的大对象(对象大小超过分代大小阀值的对象)直接复制到老年代
(3).空间分配担保:当新生代可用内存过小,Eden区无法存放创建的对象的时候,就会基于空间分配担保机制执行YGC,把存活的对象直接复制到老年代,在老年代中单独开辟一块区域来存放新晋升到老年代的对象。
(4).动态对象年龄:当Survivor幸存者区相同年龄的对象数量>Survivor幸存者区所有对象总数的50%时,会把年龄大于该对象年龄的对象直接复制到老年代,不需要达到默认分代年龄
8.如何判断是否有内存泄漏
内存泄漏表示堆区中的对象无法被GC回收,长期存活的对象超出了预期的GC回收阀值,可以通过jstack命令来查看JVM内存占用情况来判断是否出现了内存泄漏问题:
- .jstack pid > *.dump 可以把gc快照存放到dump文件中并使用jdk/bin/jvisualvm工具进行GC可视化分析
- .如果发现老年代对象在执行多次Full GC之后,老年代内存大小反而越来越大,则表示发生了内存泄漏,因为内存泄漏时堆区中的对象是无法被GC回收的,也就导致堆区中的内存大小每次Full GC之后只会变得越来越大。
- .内存泄漏还有一个特征点就是服务不可用,当发生内存泄漏时,所有服务都无法正常访问和使用,必须清理内存空间后重新启动服务
- OOM说一下?怎么排查?哪些会导致OOM? OOM出现在什么时候
OOM指的是OutOfMemory内存溢出异常,内存溢出是因为内存泄漏导致的,因为内存泄漏导致堆区中的对象无法正常被GC回收,达到一定数量之后堆区内存就会完全占满,此时再创建对象的时候就会发生内存溢出。
内存溢出是可以进行GC回收的,在发生内存溢出的时候需要使用Full GC强制垃圾回收整个堆空间中的垃圾对象来释放出更多的堆区内存空间。使用jstack命令可以看到堆区的内存空间大小随着Full GC的多次执行越来越小,则表示内存溢出问题已经被解决并释放出了可用的内存空间来存放新创建的对象。
导致OOM内存溢出的问题有很多,比如死循环装容器数据,后台查询没用分页(如果一次查询出来万条数据,每个数据映射的对象都比较大,那么存到集合容器的时候就会把内存占满,导致内存溢出)
内存溢出是因为内存泄漏导致的,因为内存泄漏导致无法释放对象资源导致堆区空间只会呈现出正增长的趋势,堆区内存空间不能得到及时释放,当堆区装满了之后还往堆区存放对象就会发生内存溢出OOM异常。
10.Java中都有哪些引用类型?
强引用:Object obj=new Object() 对象引用指向的是当前对象同类型本身,即对象类型符号和对象引用类型符号相同,强引用对象是不会被GC回收的
弱引用:每次GC垃圾回收都会回收掉的对象就是弱引用对象
软引用:默认情况下不回收,但是当堆区空间占满的时候会回收的对象就是软引用对象。
虚引用:Object obj=new Object(); obj=null; 传递一个空值给对象
提示JVM需要被GC垃圾回收的对象就是虚引用对象。
11.Java中的值类型有哪些?引用类型有哪些?
Java中的基本数据类型(byte/short/int/long/float/double/char/boolean)都是值类型,其他自定义类型和数组、集合类型都是引用类型。
改变值类型参数的值不会改变本身,改变的只是值的副本。
改变引用类型参数的值会直接改变原引用,因为指向同一个堆区中的内存地址。
转载请注明原作者