常见 JVM 面试题+“答案”, 听说发完年终奖你就需要它了

2021-02-25 16:47:10 浏览数 (1)

面试题不能坑人,所以本篇文章的内容是经过多次打磨的,现在放送给大家。

有些面试题是开放性的,有些是知识性的,注意区别。面试并没有标准答案,尤其是开放性题目,你需要整理成白话文,来尽量的展示自己。

如果你在答案中描述了一些自己不是很熟悉的内容,可能会受到追问。所以,根据问题,整理一份适合自己的吧,这比拿来主义更让人印象深刻。

1、JVM有哪些内存区域?(JVM的内存布局是什么?)

JVM包含元空间Java虚拟机栈本地方法栈程序计数器等内存区域。其中,堆是占用内存最大的一块。我们平常的-Xmx-Xms等参数,就是针对于堆进行设计的。

  • :JVM堆中的数据,是共享的,是占用内存最大的一块区域
  • 虚拟机栈:Java虚拟机栈,是基于线程的,用来服务字节码指令的运行
  • 程序计数器:当前线程所执行的字节码的行号指示器
  • 元空间:方法区就在这里,非堆本地内存:其他的内存占用空间

2、Java的内存模型是什么?(JMM是什么?)

JVM试图定义一种统一的内存模型,能将各种底层硬件及操作系统的内存访问差异进行封装,使Java程序在不同硬件及操作系统上都能达到相同的并发效果。它分为工作内存和主内存,线程无法对主存储器直接进行操作,一个线程要和另外一个线程通信,只能通过主存进行交换。

JMM可以说是Java并发的基础,它的定义将直接影响多线程实现的机制,如果你想要想深入了解多线程并发中的相关问题现象,对JMM的深入研究是必不可少的。

上面两个问题是经常容易搞混的,但它们的内容却完全不同的。

3、JVM垃圾回收时候如何确定垃圾?什么是GC Roots?

JVM采用的是可达性分析算法。JVM是通过GC Roots来判定对象的存活的。从GC Roots向下追溯、搜索,会产生一个叫做Reference Chain的链条。当一个对象不能和任何一个GC Root产生关系,就判定为垃圾。

GC Roots大体包括:

  • 活动线程相关的各种引用,比如虚拟机栈中栈帧里的引用。
  • 类的静态变量的引用。
  • JNI引用等。

当然也有比较详细的回答,个人认为这些就够了。详细版本如下:

  1. Java线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等。也就是与我们栈帧相关的各种引用。
  2. 所有当前被加载的Java类。
  3. Java类的引用类型静态变量。
  4. 运行时常量池里的引用类型常量(String或Class类型)。
  5. JVM内部数据结构的一些引用,比如sun.jvm.hotspot.memory.Universe类。
  6. 用于同步的监控对象,比如调用了对象的wait()方法。
  7. JNI handles,包括global handles和local handles

4、能够找到 Reference Chain 的对象,就一定会存活么?

这不一定,还要看reference类型。弱引用会在GC时会被回收,软引用会在内存不足的时候被回收。但没有Reference Chain的对象就一定会被回收。

5、强引用、软引用、弱引用、虚引用是什么?

普通的对象引用关系就是强引用。

软引用用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。

弱引用对象相比较软引用,要更加无用一些,它拥有更短的生命周期。当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

虚引用是一种形同虚设的引用,在现实场景中用的不是很多,它主要用来跟踪对象被垃圾回收的活动。

6、你说你做过JVM参数调优和参数配置,请问如何查看JVM系统默认值

使用-XX: PrintFlagsFinal参数可以看到参数的默认值。这个默认值还和垃圾回收器有关,比如UseAdaptiveSizePolicy。

7、你平时工作中用过的JVM常用基本配置参数有哪些?

XmxXmsXmnMetaspaceSize等。

你只需要记忆10个左右即可应付绝大多数面试,建议只记忆G1相关参数。CMS这种既耗时间参数又多又被淘汰的东西,不看也罢。面试时间有限,不会在这上面纠结,除非你表现的太嚣张了。

8、请你谈谈对OOM的认识

OOM是非常严重的问题,除了程序计数器,其他内存区域都有溢出的风险。和我们平常工作最密切的,就是堆溢出。另外,元空间在方法区内容非常多的情况下也会溢出。还有就是栈溢出,这个通常影响比较小。堆外也有溢出的可能,这个就比较难排查一些。

9、你都有哪些手段用来排查内存溢出?

(这个话题很大,可以从实践环节中随便摘一个进行总结,下面举例一个最普通的)

你可以来一个中规中矩的回答:

内存溢出包含很多种情况,我在平常工作中遇到最多的就是堆溢出。有一次线上遇到故障,重新启动后,使用jstat命令,发现Old区在一直增长。我使用jmap命令,导出了一份线上堆栈,然后使用MAT进行分析。通过对GC Roots的分析,我发现了一个非常大的HashMap对象,这个原本是有位同学做缓存用的,但是一个无界缓存,造成了堆内存占用一直上升。后来,将这个缓存改成 guava的Cache,并设置了弱引用,故障就消失了。

这个回答不是十分出彩,但着实是常见问题,让人挑不出毛病。

10、GC垃圾回收算法与垃圾收集器的关系?

常用的垃圾回收算法有标记清除、标记整理、复制算法等。引用计数器也算是一种,但是没有垃圾回收器使用这种算法,因为有循环依赖的问题。

很多垃圾回收器都是分代回收的。对于年轻代,主要有Serial、ParNew等垃圾回收器,回收过程主要使用复制算法。

老年代的回收算法有Serial、CMS等,主要使用标记清除、标记整理算法等。

我们线上用的较多的是G1,也有年轻代和老年代的概念,不过它是一个整堆回收器,它的回收对象是小堆区 。

在目前G1大行其道的今天,实在没必要再纠结CMS这么难用的东西了。

11、生产上如何配置垃圾收集器的?

首先是内存大小问题,基本上每一个内存区域我都会设置一个上限,来避免溢出问题,比如元空间。通常,堆空间我会设置成操作系统的2/3(这是想给其他进程和操作系统预留一些时间),超过8GB的堆优先选用G1。

接下来,我会对JVM进行初步优化。比如根据老年代的对象提升速度,来调整年轻代和老年代之间的比例。

再接下来,就是专项优化,主要判断的依据就是系统容量、访问延迟、吞吐量等。我们的服务是高并发的,所以对STW的时间非常敏感。

我会通过记录详细的GC日志,来找到这个瓶颈点,借用gceasy(重点)这样的日志分析工具,很容易定位到问题。之所以选择采用工具,是因为gc日志看起来实在是太麻烦了,gceasy号称是AI学习分析问题,可视化做的较好。

12、怎么查看服务器默认的垃圾回收器是哪一个?

这通常会使用另外一个参数:-XX: PrintCommandLineFlags可以打印所有的参数,包括使用的垃圾回收器。

13、假如生产环境CPU占用过高,请谈谈你的分析思路和定位。

这个可真是太太太常见了,不过已经烂大街了。如果你还是一个有经验的开发者,不知道的话,需要反省一下了。

首先,使用top -H命令获取占用CPU最高的线程,并将它转化为16进制。

然后,使用jstack命令获取应用的栈信息,搜索这个16进制。这样能够方便的找到引起CPU占用过高的具体原因。

如果有条件的话,直接使用arthas就行操作就好了,不用再做这些费事费力的操作。

14、对于JDK自带的监控和性能分析工具用过哪些?

jps:用来显示Java进程;jstat:用来查看GC;jmap:用来dump堆;jstack:用来dump栈;jhsdb:用来查看执行中的内存信息;

都是非常常用的工具,要熟练掌握。因为线上环境通常都有很多限制,用不了图形化工具。当出现这些情况,上面的命令就是救命的。

15、栈帧都有哪些数据?

JVM的运行是基于栈的,和C语言的栈类似,它的大多数数据都是在堆里面的,只有少部分运行时的数据存在于栈上。

在JVM中,每个线程栈里面的元素,就叫栈帧

栈帧包含:局部变量表、操作数栈、动态连接、返回地址等。

16、JIT是什么?

为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。完成这个任务的编译器,就称为即时编译器(Just In Time Compiler),简称 JIT 编译器。

17、Java的双亲委托机制是什么?

它的意思是,除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加载。

Java默认是这种行为。当然Java中也有很多打破双亲行为的骚操作,比如SPI(JDBC驱动加载),OSGI等。

18、有哪些打破了双亲委托机制的案例?

  1. Tomcat可以加载自己目录下的class文件,并不会传递给父类的加载器。
  2. Java的SPI,发起者是BootstrapClassLoaderBootstrapClassLoader已经是最上层的了。它直接获取了AppClassLoader进行驱动加载,和双亲委派是相反的。。

19、简单描述一下(分代)垃圾回收的过程

分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3

新生代使用的是复制算法,新生代里有 3 个分区:EdenTo SurvivorFrom Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

当年轻代中的Eden区分配满的时候,就会触发年轻代的GC(Minor GC)。具体过程如下:

  1. 在Eden区执行了第一次GC之后,存活的对象会被移动到其中一个Survivor分区(以下简称from)
  2. Eden区再次GC,这时会采用复制算法,将Eden和from区一起清理。存活的对象会被复制到to区。接下来,只需要清空from区就可以了

20、CMS分为哪几个阶段?

CMS已经弃用。生活美好,时间有限,不建议再深入研究了。如果碰到问题,直接祭出回收过程即可。

(1)初始标记 (2)并发标记 (3)并发预清理 (4)并发可取消的预清理 (5)重新标记 (6)并发清理

由于《深入理解java虚拟机》一书的流行,面试时省略3、4步一般也是没问题的。

21、CMS都有哪些问题?

(1)内存碎片问题。Full GC的整理阶段,会造成较长时间的停顿。(2)需要预留空间,用来分配收集阶段产生的“浮动垃圾“。(3)使用更多的CPU资源,在应用运行的同时进行堆扫描。(4)停顿时间是不可预期的。

正因为有这些问题,所以大家才用更加完备的G1。况且,现在都是大内存时代了,G1玩得转,就没必要用CMS。

22、你都用过G1垃圾回收器的哪几个重要参数?

最重要的是MaxGCPauseMillis,可以通过它设定G1的目标停顿时间,它会尽量的去达成这个目标。G1HeapRegionSize可以设置小堆区的大小,一般是2的次幂。

InitiatingHeapOccupancyPercent,启动并发GC时的堆内存占用百分比。G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例,默认是45%。

再多?不是专家,就没必要要求别人也是。

23、GC日志的real、user、sys是什么意思?

real 实际花费的时间,指的是从开始到结束所花费的时间。比如进程在等待I/O完成,这个阻塞时间也会被计算在内。user 指的是进程在用户态(User Mode)所花费的时间,只统计本进程所使用的时间,是指多核。sys 指的是进程在核心态(Kernel Mode)花费的CPU时间量,指的是内核中的系统调用所花费的时间,只统计本进程所使用的时间。

这个是用来看日志用的,如果你不看日志,那不了解也无妨。不过,这三个参数的意义,在你能看到的地方,基本上都是一致的,比如操作系统。

24、什么情况会造成元空间溢出?

元空间(Metaspace)默认是没有上限的,不加限制比较危险。当应用中的Java类过多,比如Spring等一些使用动态代理的框架生成了很多类,如果占用空间超出了我们的设定值,就会发生元空间溢出。

所以,默认风险大,但如果你不给足它空间,它也会溢出。

25、什么时候会造成堆外内存溢出?

使用了Unsafe类申请内存,或者使用了JNI对内存进行操作。这部分内存是不受JVM控制的,不加限制的使用,容易发生内存溢出。

26、SWAP会影响性能么?

当操作系统内存不足的时候,会将部分数据写入到SWAP交换分中,但是SWAP的性能是比较低的。如果应用的访问量较大,需要频繁申请和销毁内存,就容易发生卡顿。一般高并发场景下,会禁用SWAP。

27、有什么堆外内存的排查思路?

进程占用的内存,可以使用top命令,看RES段占用的值。如果这个值大大超出我们设定的最大堆内存,则证明堆外内存占用了很大的区域。

使用gdb可以将物理内存dump下来,通常能看到里面的内容。更加复杂的分析可以使用perf工具,或者谷歌开源的gperftools。那些申请内存最多的native函数,很容易就可以找到。

28、HashMap中的key,可以是普通对象么?需要什么注意的地方?

Map的key和value都可以是任何类型。但要注意的是,一定要重写它的equals和hashCode方法,否则容易发生内存泄漏。

29、怎么看死锁的线程?

通过jstack命令,可以获得线程的栈信息。死锁信息会在非常明显的位置(一般是最后)进行提示。

30、如何写一段简单的死锁代码?

这个笔试的话频率也挺高(遇见笔试的公司要三思啊),所以这里直接给出一个答案(有很多版本的)。

代码语言:javascript复制
public class DeadLockDemo {
    public static void main(String[] args) {
        Object object1 = new Object();
        Object object2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (object1) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object2) {
                }
            }
        }, "deadlock-demo-1");

        t1.start();
        Thread t2 = new Thread(() -> {
            synchronized (object2) {
                synchronized (object1) {
                }
            }
        }, "deadlock-demo-2");
        t2.start();
    }
}

31、invokedynamic指令是干什么的?

属于比较高级的题目。没看过虚拟机的一般是不知道的。所以如果你不太熟悉,不要气馁,加油!(小拳拳锤你胸口)。

invokedynamicJava7之后新加入的字节码指令,使用它可以实现一些动态类型语言的功能。我们使用的Lambda表达式,在字节码上就是invokedynamic指令实现的。它的功能有点类似反射,但它是使用方法句柄实现的,执行效率更高。

32、volatile关键字的原理是什么?干什么用的?

使用了volatile关键字的变量,每当变量的值有变动的时候,都会将更改立即同步到主内存中;而如果某个线程想要使用这个变量,就先要从主存中刷新到工作内存,这样就确保了变量的可见性。

一般使用一个volatile修饰的bool变量,来控制线程的运行状态。

代码语言:javascript复制
volatile boolean stop = false;
 
 void stop(){
  this.stop = true;
 }
 void start(){
  new Thread(()->{
   while (!stop){
    //sth
   }
  }).start();
 }

33、什么是方法内联?

为了减少方法调用的开销,可以把一些短小的方法,比如getter/setter,纳入到目标方法的调用范围之内,就少了一次方法调用,速度就能得到提升,这就是方法内联的概念。

34、对象是怎么从年轻代进入老年代的?

这是老掉牙的题目了。在下面四种情况下,对象会从年轻代进入老年代。

  1. 如果对象够老,会通过提升(Promotion)进入老年代,这一般是根据对象的年龄进行判断的。
  2. 动态对象年龄判定。有的垃圾回收算法,比如G1,并不要求age必须达到15才能晋升到老年代,它会使用一些动态的计算方法。
  3. 分配担保。当 Survivor 空间不够的时候,就需要依赖其他内存(指老年代)进行分配担保。这个时候,对象也会直接在老年代上分配。
  4. 超出某个大小的对象将直接在老年代分配。不过这个值默认为0,意思是全部首选Eden区进行分配。

35、safepoint是什么?

STW并不会只发生在内存回收的时候。现在程序员这么卷,碰到几次safepoint的问题几率也是比较大的。

当发生GC时,用户线程必须全部停下来,才可以进行垃圾回收,这个状态我们可以认为JVM是安全的(safe),整个堆的状态是稳定的。

如果在GC前,有线程迟迟进入不了safepoint,那么整个JVM都在等待这个阻塞的线程,造成了整体GC的时间变长。

36、MinorGC,MajorGC、FullGC都什么时候发生?

MinorGC在年轻代空间不足的时候发生,MajorGC指的是老年代的GC,出现MajorGC一般经常伴有MinorGC。

FullGC有三种情况。

  1. 当老年代无法再分配内存的时候
  2. 元空间不足的时候
  3. 显示调用System.gc的时候。另外,像CMS一类的垃圾回收器,在MinorGC出现promotion failure的时候也会发生FullGC

37、类加载有几个过程?

加载、验证、准备、解析、初始化。

38、什么情况下会发生栈溢出?

栈的大小可以通过-Xss参数进行设置,当递归层次太深的时候,就会发生栈溢出。比如循环调用,递归等。

0 人点赞