JVM常见面试题
基础题
能不能给我讲一下JVM完整的GC流程
我们先从Minor GC
说起吧,当对象分配到Eden
区发现Eden
区空间满了,此时就会触发Minor GC
,将非存活对象回收,再将存活对象放到From区(S1区)
,再将新创建的对象放到Eden
区。
随着时间推移,Eden
区再次满了,此时再次触发Minor GC
,将非存活对象清理,存活对象放到S2区
。
然后我们再来说说对象升级到老年代的4种情况:
- 经过
Minor GC
后,S区
的toSpace
区无法容纳的存活的对象。 - 大对象直接进入老年代,而这个大对象,大对象可以根据参数
PretenureSizeThreshold
知晓值的大小。 - 长期存活的对象直接进入老年代,
JVM
默认设置为15(即在年轻代经过15次Minor GC)
- 还有一种情况,如果在
From
空间中,相同年龄所有对象的大小总和大于From
和To空间
总和的一半,那么年龄大于等于该年龄的对象就会被移动到老年代,而不用等到15岁(默认)
接下来就是Full GC
了:
因为上述原因需要将对象放到老年代,但是老年代的空间不够存放对象时就会触发Full GC
,如果Full GC
完还是无法容纳新对象就会报OOM
的异常。
为确保Minor GC
后有足够的空间可以容纳依旧存活的对象,JVM
还提出了空间分配担保机制:
在发生Minor GC
前,JVM会检查老年代最大连续可用空间是否大于年轻代所有对象的总空间:
- 若大于年轻代总空间,则说明本次垃圾回收是安全,继续执行
Minor GC
。 - 若小于年轻代的总空间,且这个版本在在
JDK 6 Update24
之前,他们则会查看则看参数HandlerPromotionFailure
的配置值:
1. 若配置为true,则会再次检查每次晋升到老年代的平均大小,若老年代空间大于这个值,则无视风险直接进行`Minor GC`,若小于则说明当前老年代空间确实不太够了,直接进行FULL GC。
2. 若配置为false则直接Full GC。
- 在
JDK 6 Update24
之后,HandlerPromotionFailure
这个参数就被作废了,在进行Minor GC
前的空间分配担保检查的就是老年代的连续空间是否大于新生代的对象的总大小或者每次晋升的平均大小,符合任意条件则直接进行Minor GC
,反之就是FULL GC
。
能不能说一下JVM内存区域的分配
内存区域有堆区、虚拟机栈、本地方法栈、程序计数器、方法区。
其中方法区和堆区为线程共享的。其余都是线程隔离的。
而各个组成部分的作用为:
程序计数器(Program Counter Register)
也被称为 PC 寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器。它指向当前线程要执行的下一条指令的地址。Java 虚拟机栈(Java Virtual Machine Stack)
:也是线程私有的,它的生命周期与线程相同。Java 虚拟机栈描述的是 Java 方法执行的线程内存模型,方法执行时,JVM 会同步创建一个栈帧,用来存储局部变量表、操作数栈、动态连接等。本地方法栈(Native Method Stacks)
:与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。- 对于 Java 应用程序来说,
Java 堆(Java Heap)
是虚拟机所管理的内存中最大的一块。Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 里“几乎”
所有的对象实例都在这里分配内存。
Java 堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC 堆”(Garbage Collected Heap)
。从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以 Java 堆中经常会出现新生代、老年代、Eden空间、From Survivor空间、To Survivor空间等名词,需要注意的是这种划分只是根据垃圾回收机制来进行的划分,不是 Java 虚拟机规范本身制定的。 方法区
:是比较特别的一块区域,和堆类似,它也是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。它特别在 Java 虚拟机规范对它的约束非常宽松,所以方法区的具体实现历经了许多变迁,例如 jdk1.7 之前使用永久代作为方法区的实现。
说一下 JDK1.6、1.7、1.8 内存区域的变化?
- JDK6使用永久代作为方法区。
- JDK7将字符串常量池、静态变量放到堆区,而类常量池、运行时常量池仍然存放在方法区中。
JDK8
则是用元数据区实现作为方法区的实现。去掉了永久代这么个东西,而元数据区存放的仍然是运行时常量池和类常量池。
请你介绍一下JVM方法区
答: 方法区主要是用于存储类信息、静态变量以及常量信息的。是各个线程共享的一个区域。我们都知道JVM中有个区域叫堆区,所以有时候人们也会称方法区为Non-Heap(非堆)
。
在JDK8
之前方法区存放在一个叫永久代的空间里。
在JDK8
之后由于HotSpot 和JRockit 的合并,所以方法区就被作为元数据区了。
那你知道方法区和永久代的关系吗?
答: 其实方法区并不是一个实际的区域,他不过是JVM定义的一个规范而已。在HotSpot 实现方法区的方式就在JVM
内存中划分一个区域作为永久代来存放这些数据。
在JDK8之前我们可以用下面的参数来调整永久代的大小
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
那么你来说说为什么JDK8
之后要把永久代 (PermGen)换成`元数据区(MetaSpace) ?
答: 将数据放在永久代固然没问题,但是随着时间的推移,方法区使用的空间可能会逐渐变大,若我们分配大小不当很可能造成线上OOM
问题,所以设计者们就在方法区移动到本地内存中,通过本地内存来存放数据。并且元数据区默认分配值为unlimited(我们也可以通过-XX:MetaspaceSize
来动态调整),理论上是没有明确大小,是可以动态分配空间的,这样一来由于元数据区就不会受到JVM内存分配的约束了,所以理论上发生OOM的概率会小于永久代。
请你介绍一下运行时常量池
首先我们需要了解一下类常量池
类常量池:主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References)。
答: 我们都知道Class
文件包含字段描述符
、方法描述符
、接口
等描述信息,还有编译器生成的字面量和符号引用,都会被存放到JVM
方法区的运行时常量池中。
- 在JDK7之前,运行时常量池包括字符串常量池都存放在永久代。
- JDK7将字符串常量池移动到了堆区。而其他数据依然保留在方法区中,即可永久代区。
- 在JDK8则将永久代改为元数据区,这就意味着运行时常量池就被存放到永久代的元数据区了。
JVM 常量池中存储的是对象还是引用呢?
答: runtime constant pool
(而不是interned string pool / StringTable之类的其他东西)的话,其中的引用类型常量(例如CONSTANT_String、CONSTANT_Class、CONSTANT_MethodHandle、CONSTANT_MethodType之类)
都存的是引用,实际的对象还是存在Java heap
上的。
说说元空间的直接内存的概念吧
答: 在JDK1.4中 NIO(New Input/Output)
类提供的一个名为MappedByteBuffer
的内存映射文件的方式,直接调用Native
操作本机内存,通过这种方式避免操作数据从JVM堆
到Native
堆的开销,从而提高程序执行效率。这就意味着这种这个操作会受到本机内存大小以及处理器寻址空间的限制。
说一下Java对象的创建过程
答: 整体过程大抵是一下几个步骤:
- 类加载检查: 在JVM收到new命令后,就会先去常量池查看是否有这个类的符号引用,若有则再查看这个类是否被加载、解析、初始化过。若没有则进行类加载。
- 分配内存空间: 在堆区划出一个空间将为对象分配空间。
- 设置零值: 完成对象空间的分配之后,就需要将对象中的字段都赋为初始值
(除了对象头)
。 - 设置对象头: 完成上述步骤,我们就需要为对象头设置
哈希码
、对象的GC分代信息
、元数据
信息、以及是否使用偏向锁等都放到对象头中。 - 执行init方法: new方法最终一步,调用
<init>
完成对象的创建。
能说一下对象内存布局嘛?
宏观来说有这么几个模块,大抵可以分为:
- 对象头
- 实例数据
- 对齐填充
先来说说对象头,它由两个部分组成,第一个部分则是记录自身信息的,包含哈希码、gc分代年龄、锁状态标志、线程持有的锁、偏向锁id、偏向时间戳等,它也叫markword。需要补充的是,如果这个是属于数组类型的话
第二个部分则是类型指针,指向对象的类元数据类型,这个类型指针的存在使得我们可以知晓它是哪个类。
实例数据用来存储对象中各自类型的字段内容,即使是从父类继承来的,它也会记录。
对齐填充不是必须的,仅仅作为占位符使用的。
上文提到的分配内存空间这一步,你知道内存分配的两种算法吗?
答: 一种是指针碰撞
、还有一种是空闲列表
。
指针碰撞使用是堆区空间规整的情况下,例如你使用复制算法
、或者标记-整理
算法时,堆区空间就是规整的。而空闲列表则适用于空间不完整的情况,例如标记-清除
算法。
了解过Java
内存对象多线程并发分配问题吗?
答: 在分配空间时JVM
首先会预先为Eden区
为每个线程分配一个TLAB
空间。每次线程都只能操作自己的TLAB
区以及读取其他线程的TLAB区(但是不能操作),若TLAB
空间满了或者不够分配当前对象时,则基于CAS+失败重试在堆区其他空间尝试分配空间。
知道对象访问定位的两种访问方式吗?
答: 有两种方式,一种是使用句柄
,一种是直接指针
。句柄方式则将对象地址、以及对象对应的类的地址信息存放到一个句柄中,引用直接通过句柄得到对象的实际地址,进而去操作要访问的对象。
直接指针方式则是引用中直接记录对象的地址,我们直接通过引用的地址得到对象的地址进而直接操作对象。而对象类型数据信息则都存储在堆中的对象头里。
所以前者优势是稳定,即引用无需因为对象的移动而改变则动态修改。后者优势则是略去了访问句柄的一步,效率更高一些。
请你说说堆内存分配的基本策略
答: 对象优先会被分配在eden区
,如果是大对象直接分配到老年区(避免分配担保机制的负担)
,当对象存活时间达到-XX:MaxTenuringThreshold
的值时也会到老年区。
什么是内存分配担保机制?
答: 如果我们创建了一个大对象,Eden
区不足以分配该对象,就会将Eden
区的对象移到Survivor
区,然后将大对象放到Eden
区。
内存溢出和内存泄露了解过嘛?
内存泄露指的无用的对象未能实时的清除,导致堆区内存被一些无用的垃圾占用。而导致内存泄漏的大概会有以下几个原因:
- 静态集合类,静态集合声明周期和JVM一致的,所以它不可能释放掉,代码如下所示
public class OOM {
static List list = new ArrayList();
public void oomTests(){
Object obj = new Object();
list.add(obj);
}
}
- 单例模式,单例但模式和静态集合类原因差不多,如果这个单例模式是大对象且未能及时销毁很可能导致内存泄漏问题。
- IO等连接资源未能及时释放
- ThreadLocal变量:ThreadLocal 的弱引用导致内存泄漏也是个老生常谈的话题了,使用完 ThreadLocal 一定要记得使用 remove 方法来进行清除。
- hash值改变:对象 Hash 值改变,使用 HashMap、HashSet 等容器中时候,由于对象修改之后的 Hah 值和存储进容器时的 Hash 值不同,所以无法找到存入的对象,自然也无法单独删除了,这也会造成内存泄漏。说句题外话,这也是为什么 String 类型被设置成了不可变类型。
- 变量作用域过大
public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他代码
//由于作用域原因,method1执行完成之后,object 对象所分配的内存不会马上释放
object = null;
}
}
而内存溢出则是当前堆区空间无法容纳新对象导致OOM问题。代码如下所示
/**
* VM参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
可以说内存泄漏会导致内存溢出。
Minor GC/Young GC、Major GC/Old GC、Mixed GC、Full GC 都是什么意思?
Minor GC/Young GC
指的是年轻代的垃圾收集。Major GC/Old GC
指的是老年代的GC,目前只有CMS收集器会有单独收集老年代的行为。Mixed GC
:混合收集,指的是新生代和老年代的垃圾收集,目前只有G1收集器会有这种行为。Full GC
:收集整个Java堆和方法去的垃圾。
Minor GC什么时候触发?
当年轻代空间不足的时候就会触发Minor GC
什么时候会触发 Full GC
- minor GC前检查老年代发现空间不足:在要进行
Young GC
的时候,发现老年代可用的连续内存空间 < 新生代历次Young GC
后升入老年代的对象总和的平均大小,说明本次Young GC
后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,那就会触发Full GC
。 Minor gc
后老年代空间不足:执行Young GC
之后有一批对象需要放入老年代,此时老年代就是没有足够的内存空间存放这些对象了,此时必须立即触发一次Full GC
- 调用
system.gc()
- 空间分配担保失败:新生代的 To 区放不下从 Eden 和 From 拷贝过来对象,或者新生代对象 GC 年龄到达阈值需要晋升这两种情况,老年代如果放不下的话都会触发 Full GC。
- 老年代空间不足:老年代内存使用率过高,达到一定比例,也会触发 Full GC。
- 方法去内存空间不足:如果方法区由永久代实现,永久代空间不足 Full GC。
对象什么时候会进入老年代
- 长时间存活的对象:在对象的对象头信息中存储着对象的迭代年龄,迭代年龄会在每次 YoungGC 之后对象的移区操作中增加,每一次移区年龄加一.当这个年龄达到 15(默认)之后,这个对象将会被移入老年代。
这个可以通过下面这个参数进行设置
- XX:MaxTenuringThreshold
- 大对象直接进入老年代:有一些占用大量连续内存空间的对象在被加载就会直接进入老年代.这样的大对象一般是一些数组,长字符串之类的对。大对象的阈值可以通过这个参数进行设置
-XX:PretenureSizeThreshold
- 动态对象年龄判断:为了能更好地适应不同程序的内存状况,HotSpot 虚拟机并不是永远要求对象的年龄必须达到
- XX:MaxTenuringThreshold
才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。 - 空间分配担保:假如在 Young GC 之后,新生代仍然有大量对象存活,就需要老年代进行分配担保,把 Survivor 无法容纳的对象直接送入老年代。
能不能简单介绍一下强引用、弱引用、软引用、虚引用?
答: 被强引用(StrongReference)
指向的对象无论如何都不会被垃圾回收器回收,宁可被OOM也不会被回收。软引用(SoftReference)
相较于强引用地位低一些,当内存空间不足的时候,它就会被直接回收,一旦它引用的对象被回收,他就被存放到一个与之关联的引用队列中。弱引用(WeakReference)
地位比强引用更低,只要被垃圾回收器线程扫描到就会被直接回收,一旦被回收该引用也被被存放到与之关联的一个队列中。虚引用(PhantomReference)没
有任何地位,任何时间段都能够被回收。
当然,在平时工作中弱引用和虚引用使用的就不是很多,更多是使用软引用,因为软引用不会在没必要的时候被回收,只有内存不足时才能回收,这样对于JVM
性能开销来说回更节约一些。
如何判断一个常量是废弃常量?
答: 我们以字符串为例,如果字符串常量没有任何引用指向的话,那么在垃圾回收阶段这个常量就会被回收。
如何判断一个类是无用的类?
答: 这里说到的是类吧?判断类是否无用大概是从以下3点判断:
1. 这个类的所有实例都被垃圾回收器回收了,也就是Java堆中没有任何该类的实例。
2. ClassLoader 被回收了。
3. java.lang.Class类没有被被引用,无法通过任何地方完成反射操作了。
如果符合上述三点,就说明这个类可以被回收了,注意仅仅是可以,不代表真的就要被回收了。
HotSpot 为什么要分为新生代和老年代?
答: 主要是为了提高GC效率,对年轻代和老年代采用不同的回收算法,利用好每个内存区域空间。
Class 的作用了解么?
答: class文件即字节码文件,是面向虚拟机的一种文件,它解决传统解释器语言效率低的问题。也正是由于它是面向虚拟机的文件,所以Java代码只需编译一次即可在任何有虚拟机的平台使用。
请你介绍一下类加载过程
答: 加载,连接(验证、准备、解析、初始化)、初始化
大抵分为以下三步:
- 通过全类名获取这个类的二进制字节流。
- 将这个静态字节流转换为方法区运行时数据结构。
- 在
方法区
生成Class
对象,作为访问这些数据的入口。
知道那些类加载器嘛?
答:知道,大概有以下三个:
- BootstrapClassLoader(启动类加载器):负责
%JAVA_HOME%/lib
目录下的 jar 包和类或者或被-Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
- AppClassLoader(应用程序类加载器) : 负责加载当前应用程序classpath下的所有jar包。
双亲委派模型的源码简单分析一下
答: 如下所示,从源码中我们就可以了解双亲委派机制了,可以看到loadClass方法会先去查看方法区中是否有该类的加载信息。若没有则会先让根加载器先尝试加载,若没有则再找扩展类加载器,最后才是应用程序类加载器。
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
}
if (c == null) {
long t1 = System.nanoTime();
//自己尝试加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派模型有什么好处?
答: 有两点好处:
- 避免重复加载相同的类。
- 避免用户编写的类破坏核心API。
如果不想使用双亲委派模型怎么办?
答: 如果想自定义类加载器的话,继承ClassLoader
类就好了。如果想破坏双亲委派机制的话,就重写我们上文所说的loadClass
方法。
静态变量在堆区还是在原空间?它是否会被GC回收
jdk8之后,静态成员变量存储在哪?有说存在元数据区,有说迁移到堆中?希望大佬能给个详细的解答,多谢? - 红尘修行的回答 - 知乎
https://www.zhihu.com/question/324306038/answer/688264413
jdk8之后,静态成员变量存储在哪?有说存在元数据区,有说迁移到堆中?希望大佬能给个详细的解答,多谢? - 红尘修行的回答 - 知乎
https://www.zhihu.com/question/324306038/answer/688264413
java static GC 回收问题:https://blog.csdn.net/kangojian/article/details/5186530
参考文献
JVM内存分配担保机制:https://blog.csdn.net/kavito/article/details/82292035
运行时常量池(JVM06):https://zhuanlan.zhihu.com/p/353663613
剖析面试最常见问题之JVM(下):https://xiaozhuanlan.com/topic/3621504987#section1class
深入理解Java虚拟机(第3版):https://book.douban.com/subject/34907497/
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!