JVM常见面试题
- 请你谈谈你对 JVM 的理解?java8虚拟机和之前的变化更新?
- 什么是OOM?,什么是栈溢出StackOverFlowError?怎么分析?
- JVM的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?知道吗?
dump可以是内存溢出时让其自动生成,或者手工直接导。配置jvm参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/biapp/m.hprof
使用 Jprofiler
- 谈谈JVM中,类加载器你的认识?
虚拟机概念
系统虚拟机:VMware
程序虚拟机:java虚拟机
1、JVM的位置
2、类加载器
面试题:
问:谈谈JVM中,类加载器你的认识?
答:在java中每个类都是由某个类加载器的实体来载入的,因此在Class类的实体中,都会有字段记录载入它的类加载器的实体;
java的类加载器分为以下几种:
- Bootstrap ClassLoader:是所有类加载器的最终父加载器。
- ExtClassLoader:
- AppClassLoader
- ClassLoader
2.1 类加载子系统
在Java虚拟机中,负责查找并装载类的部分陈伟类装载子系统,类装载器子系统用于定位和加载编译后的class文件;
2.2 双亲委派机制
步骤:
1、类加载器收到类加载的请求,它首先不会自己去尝试加载这个类
2、而是将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
3、启动类加载器检查是否能够加载当前这个类,能加载就结束,否则,抛出异常,通知子加载器加载
4、重复步骤3
2.3 类加载过程
类加载会将类信息加入到方法区中(元数据空间)
3、JVM内存结构(运行时数据区)
JVM在Java程序运行时把它所管理的内存划分为几个不同的数据区域:程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、 方法区(Method Area)、堆(Heap)。
3.1 线程私有数据区
(1)程序计数器(PC寄存器)
特点:
- 记录程序执行的位置、行号
- 不存在内存溢出
- 没有GC回收
(2)虚拟机栈
特点:
- 每个方法执行会创建一个栈帧,存储局部变量表、操作栈等信息
- 方法执行入虚拟机栈,方法执行完出虚拟机栈
- 栈深度大于虚拟机所允许StackOverflowError
- 局部变量所指向的值(常量值、对象值)都存放到堆上
- 没有GC回收
注意:
Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
OutOfMemoryError:若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
(3)本地方法栈
特点:
- 与虚拟机栈基本一致
- 区别在于本地方法栈为Native方法服务
- Sunday HotSpot将虚拟机栈和本地方法栈合并
- 有StackOverflowError和OutOfMemoryError
- 没有GC回收
native:凡是带了native关键字的,说明 Java 的作用范围达不到,回去调用底层 C 语言的库,会进入本地方法栈
3.2 线程共享数据区
内存划分:
JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(YoungGeneration)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
用途:
- 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
- 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
JAVA内存是如何划分的,如图:
(1)方法区(元空间/永久区)
介绍:
它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。
这个区域常驻内存的。用来存放 JDK 自身携带的 Class对象。Interface元数据,存储的是 Java运行时的一些环境或者类信息,这个区域不存在垃圾回收,关闭 VM虚拟机就会释放这个区域的内存。
特点:
- 线程共享
- 存储类信息、常量、运行时常量池、静态变量、类信息(构造方法、接口定义)等;但是实例变量存储在堆内存中,和方法区无关。
常见面试题:
1、java8虚拟机和之前的变化更新?
- 在 JDK1.8版本废弃了永久代,替代的是元空间,元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在 JVM中,而是使用本地内存。
2、为什么移除永久代?
- 为融合 HotSpot JVM与 JRockit VM(新JVM技术)而做出的改变,因为 JRockit没有永久代。有了元空间就不再会出现永久代OOM问题了
3、什么是OOM?
- Out of Memory(内存溢出)
4、堆里面的分区有哪些?Eden、form、to,老年区,说说他们的特点?
- Eden区位于Java堆的年轻代,是新对象分配内存的地方,由于堆是所有线程共享的
- 幸存区与Eden区相同都在Java堆的年轻代。幸存区有两块,一块称为from区,另一块为to区,这两个区是相对的,在发生一次轻 GC后,from区就会和to区互换。在发生轻 GC时,Eden区和幸存 from区会把一些仍然存活的对象复制进幸存 to区,并清除内存。幸存 to区会把一些存活得足够旧的对象移至年老代。
- 年老代里存放的都是存活时间较久的,大小较大的对象,因此年老代使用标记整理算法。当年老代容量满的时候,会触发一次重GC(full GC),回收年老代和年轻代中不再被使用的对象资源。
(2)堆
堆内存分为年轻代、老年代
- 年轻代又分为Eden(伊甸园区)和Survivor(幸存区)。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1。
新生区
类:诞生和成长的地方
伊甸园区
幸存区
分代概念:
新生成的对象首先放到年轻代Eden区,当 Eden空间满了,触发Minor GC,存活下来的对象移动到幸存0区,幸存0区满后触发执行Minor GC,幸存0区存活对象移动到幸存1区,这样保证了一段时间内总有一个幸存区为空。经过多次Minor GC仍然存活的对象移动到老年代。
老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对相应要求高的应用尽量减少发生Major GC,避免响应超时。
- Minor GC:清理年轻代
- Major GC:清理老年代
- Full GC:清理整个堆空间,包括年轻代和永久代
所有GC都会停止所有应用进程
为什么要分代?
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
堆内存调优
在JDK1.7中
在JDK1.8中,元空间取代永久代。元空间和永久代的最大的区别是永久代使用的是JVM的堆内存,元空间不在虚拟机中,而是使用本机物理内存。默认清空下,元空间只受本地内存限制,类的元数据放入本地内存,字符串常量池和类型静态变量放入java堆,类的元数据的加载量不再受MaxPermSize控制,而是由系统实际的可用空间来控制。
-Xms:初始分配大小,默认为物理内存的1/64
-Xmx:最大分配内存,默认为物理内存的1/4
-XX:+PrintGCDetails:输出详细的GC处理日志
4、常见的 JVM调优参数
4.1 JVM三种参数类型
- 标准参数。如-version、-help
- X参数。如-Xms、-Xmx
- XX参数。如-XX:+PrintGC
4.2 XX参数数值类型
如-XX:+PrintGCDetails,其中+和-分别表示开启/关闭某个属性,PrintGCDetails表示打印GC详情
KV设值类型。如-XX:NewSize=256M,设置年轻代空间大小为256M
4.3 常用参数
X参数:
XX参数:
- -XX:NewSize:设置年轻代最小空间大小
- -XX:MaxNewSize:设置年轻代最大空间大小
- -XX:PermSize:设置永久代最小空间大小
- -XX:MaxPermSize:设置永久代最大空间大小
- -XX:NewRatio:设置年轻代和老年代的比值。默认值-XX:NewRatio=2,表示年轻代与老年代比值为1:2,年轻代占整个堆大小的1/3
- -XX:SurvivorRatio:设置年轻代中Eden区Survivor区的容量比值。默认值-XX:SurvivorRatio=8,表示Eden:Survivor0:Survivor1=8:1:1
5、垃圾回收
5.1 什么是垃圾?
通过下面一个简单的代码来理解:
public static void write(){
String str = new String("hello");
byte[] bytes = str.getBytes();
writeToFile(bytes);
}
上面代码通过将字符串对象转化成字节数组,然后写入本地文件。方法一旦开始执行,就将会在分配一定内存给新建的对象,然后将引用告诉了str, bytes 变量。等到方法执行完毕,方法内部局部变量紧接将就会被销毁。但是这样仅仅销毁了局部变量,却没有带走内存上这些实际的对象。这类不再起作用,没有被引用的对象,将其归类为垃圾。
5.2 垃圾在哪里回收?
- 如图所示,我们将内存划分为线程私有与线程共享的区域。方法区与堆都是线程共享的区域,这两部分占用 JVM 大部分内存,剩下三个小弟将会跟线程绑定,随着线程消亡,自动将会被 JVM 回收。
JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收的都是新生区
- 新生区
- 幸存区
- 老年区
GC两种类:轻GC(普通垃圾回收)、重GC(全局垃圾回收)
5.3 怎么回收?(GC常用算法)
常见面试题:
1、GC的算法有哪些?标记清除法、标记压缩法、复制算法、引用计数器,怎么用的?
2、轻GC和重GC分别在什么时候发生?
新生成的对象首先放到年轻代Eden区,当 Eden空间满了,触发轻 GC,存活下来的对象移动到幸存0区,幸存0区满后触发执行轻 GC,幸存0区存活对象移动到幸存1区,这样保证了一段时间内总有一个幸存区为空。经过多次轻 GC仍然存活的对象移动到老年代。
老年代存储长期存活的对象,占满时会触发重 GC,GC期间会停止所有线程等待GC完成,所以对相应要求高的应用尽量减少发生Major GC,避免响应超时。
(1)引用计数法:
- 引用计数法通过在对象头分配一个字段,用来存储该对象引用计数。一旦该对象被其他对象引用,计数加 1。如果这个引用失效,计数减 1。当引用计数值为 0 时,代表这个对象已不再被引用,可以被回收。
如上图所示,当 str 引用堆中对象时,计数值增加为 1。当 str 变为 null 时,既不再引用该对象,计数值减 1。此时该对象就可以被 GC 回收。
缺点:
引用计数法只需要判断计数值,所以实现比较简单,这个过程也比较高效。但是存在一个很严重的问题,无法解决对象循环引用问题。
(2)复制算法:
- 将内存按容量划分为两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。缺点需要两倍的内存空间。
将伊甸园区和幸存区 from 复制到幸存区 to
当经历 15 次 GC 还没死,就进入老年区
注意:
- 将幸存区from 复制到幸存区to
- 每次垃圾回收之后伊甸区就为空了
- 当一个对象经历了 15 次 GC,都还没死,可以通过参数调优进入老年区
优点:
- 没有内存的碎片
缺点:
- 浪费了内存空间
复制算法最佳使用场景:
- 对象存活度较低的时候(新生区)
(3)标记清除算法
- GC分为两个阶段,标记和清除。首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。同时会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC。
第一次扫描:对这些对象进行标记
第二次扫描:对没有标记的对象,进行清除
优点:
- 不需要额外的空间
缺点:
- 两次扫描,严重浪费时间,会产生内存碎片
(4)标记压缩算法
- 也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。此方法避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题。
在标记清除算法的基础上再一次扫描:
(5)总结
内存效率:复制算法 > 标记清除算法 > 标记压缩算法
内存整齐度:复制算法 = 标记压缩算法 = 标记清除算法
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
年轻代:
- 存活率低
- 复制算法
老年代:
- 区域大、存活率高
- 标记清除 + 标记压缩混合实现
6、Java内存模型(JMM)
6.1 什么是JMM?
- Java内存模型 (Java Memory Model,JMM)是屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java程序在各种平台下都能达到一致的内存访问效果。
面试题:
问:描述一下java内存模型?
答:java内存模型规定了变量的访问规则,保证操作的原子性、可见行、有序性。
6.2 它是干嘛的
缓存一致性问题:
由于主存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲,CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再将运算结果同步到主存中。
使用高速缓存解决了 CPU 和主存速率不匹配的问题,但同时又引入另外一个新问题:缓存一致性问题。
因此需要每个 CPU 访问缓存时遵循一定的协议,在读写数据时根据协议进行操作,共同来维护缓存的一致性。这类协议有 MSI、MESI、MOSI、和 Dragon Protocol 等。
作用:缓存一致性协议,用于定义数据读写的规则
线程、工作内存、主内存之间的交互关系:
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存
内存间交互操作:
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成。Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load动作使用。
- load(载入):作用于工作内存的变量,它把 read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write操作使用。
- write(写入):作用于主内存的变量,它把 store操作从工作内存中得到的变量的值放入主内存的变量中。
如果要把一个变量从主内存拷贝到工作内存,那就要按顺序执行 read和 load操作,如果要把变量从工作内存同步回主内存,就要按顺序执行 store和 write操作。注意,Java内存模型只要求上述两个操作必须按顺序执行,但不要求是连续执行。也就是说 read与 load之间、store与 write之间是可插入其他指令的