常见面试题
问题剖析:当面试官提出这个问题时,就是想考察你对JVM核心基础的理解。这个问题看似容易回答,但是准备不足则难以给出较为满意的答案。可能很多同学的答案回答仅仅是流水账式的列出相关知识点,而没有系统性地组织语言将各个知识点串起来。下面先给出回答这个问题相关的知识点,然后给出一个回答该问题的示例模板。
上图展示了JVM中内存模型,不难看出线程内驱动程序运行的程序计数器、虚拟机栈和本地方法栈都要占用内存;而堆和方法区则是独立于线程占用内存。从内存区域是否共享这个维度作为划分手段,线程内部占用的内存区域为私有,不同线程之间不可互相访问;而堆、方法区和直接内存则是公共且可以互相访问。
面试问题二:请谈谈你对JVM堆的理解
问题剖析:当面试官提出这个问题时,就是想考察你对JVM中核心部分堆的理解,因为堆上经常发生GC,在线上中出现FullGC是很常见的事情,因此不仅要会Dump堆的实际使用情况,还要堆的内存结构有较深的理解。除此之外,随着Java版本的发展,堆内存的结构也出现了不小的变化,因此回答不应仅仅局限于老版本的堆内存结构,还要与时俱进地分析版本之间的差异性。
在JDK7及其更老的版本中,堆通常被分为新生代、老生代以及永生代。JDK8之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7就已经开始了),取而代之是元空间,元空间使用的是直接内存。
面试问题三: JVM堆内存大小一般如何分配
问题阐述:当面试官提出这个问题时,就是想考察你是否对堆有很深的理解。堆总体大小以及各个区域大小的分配在很大程度上会关联到GC效率。
面试问题四: 请谈谈方法区和永生代的区别
问题阐述:当面试官提出这个问题时,就是想考察你对方法区的认识,因为很多人认为方法区和永生代同一个东西,但是二者在狭义上还是有一些区别,不能简单地混为一谈。
进阶知识点与思考
进阶知识一:区分JVM内存模型和Java内存模型
进阶知识二:区分JVM程序计数器在执行指令上与OS层面的区别
无论是操作系统层面还是JVM层面,机器在执行时都会将代码转化成指令(也可以理解为机器码),这种指令一般不容易理解,但是执行效率极高。在OS中的指令一般带有操作数,而JVM的指令不带有操作数,下面以加法为例简要展示二者的差异(OS指令仅参考一种指令设计模式,且操作码和操作数地址均为举例)。
执行一条加法操作时,对应的命令无论是JVM还是OS至少需要以下内容:
- 操作码
- 操作数1
- 操作数2
进阶知识三:逃逸分析
逃逸分析是一项JVM优化技术,用于虚拟机分析对象的作用域。在面试中与这个知识点关联度最大的问题就是”所有对象都在堆空间上分配内存么?“。首先给出这个问题的答案,是否定的,并非所有对象都在堆空间分配内存。下面首先介绍一下逃逸分析,然后从理论上详细解答这个问题,最后给出逃逸分析中的其他几项技术。
Java运行需要经过两个必要的阶段,首先基于javac命令将.java文件编译为.class;然后JVM将字节码转化为机器可执行的指令,转化时逐条读入逐条翻译。这种转化过程一般执行效率较差,为了优化这个问题提出了即时编译技术(JIT)。引入JIT后,JVM具有一定智能性,当JVM发现某一段方法或代码块执行的非常频繁时,就会认为出现了热点代码。为了避免频繁将这段代码转化为机器指令,JIT会将这部分热点代码翻译后放到缓存里以供调用。JIT在判定逃逸时也有较大作用。逃逸是指在方法A内被创建的对象不仅在本方法内被引用,对于其他方法而言也能获得其引用。如果对象发生了逃逸,会产生如下影响:
- 方法A创建的对象在方法结束时依旧存在引用,因此改对象无法被GC回收
- 方法A尚未结束时,方法B修改了共同引用的对象,导致一致性问题
// 对全局/静态变量赋值导致的逃逸 publicstaticObject obj; publicvoidsetObj() { obj = newObject(); }
// 返回引用导致方法逃逸 publicstaticStringBuilder getString() { StringBuilder stringBuilder = newStringBuilder(); stringBuilder.append("hello"); stringBuilder.append("world"); returnstringBuilder; }
逃逸分析的具体实现基于连通图构造对象间的引用可达性关系,并基于一套数据流分析方法实现。感兴趣的同学可以进一步学习<>
回到”所有对象都在堆空间上分配内存么?“这个问题,逃逸分析基于JIT技术使用栈上分配避免一些对象在堆空间分配内存。我们都知道在堆空间上GC有一定时间开销,如果一个对象的作用域不会逃逸出方法调用,那么久没必要在堆上分配空间并产生潜在的GC时间。而在实际的项目开发中,大部分局部对象不会逃逸出方法调用,因此栈上分配存在的意义极大。然而栈上分配技术只能支持到方法维度,在线程维度上它无能为力。下面我们通过实践来对逃逸分析进行测试。首先在执行代码前先给出开启逃逸分析的方法(JDK8中默认开启):
- -XX:+DoEscapeAnalysis :表示开启逃逸分析
- -XX:-DoEscapeAnalysis :表示关闭逃逸分析
- -XX:+PrintEliminateAllocations :显示标量替换详情
publicclassStackDispatch { privatestaticfinalintnum = 1000000; publicstaticvoidmain(String[] args) { for(inti = 0; i < num; i++) { alloc(); } System.out.println("Please check status in command"); // 避免程序提前结束 try{ Thread.sleep(num); } catch(InterruptedException e1) { e1.printStackTrace(); } } privatestaticvoidasignTeacher() { Teacher teacher = newTeacher(); } } classTeacher { privateintage; privateString name; privateString sex; privateString slogan; Teacher() { age = 0; name = ""; sex = ""; slogan = ""; } }
# 查看所有现存的Java程序id号 jps # 查询堆空间上所有对象实例的数量 jmap -histo id号
不出意外StackDispatch将会有100w个实例,也就是说关闭逃逸分析时,虽然teacher没有逃逸出asignTeacher()方法,但是被全量存储到堆空间。更改启动参数为:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError,再次执行上述操作。结果显示基于栈上分配后,堆中只有112842个对象,数量为100w的11%,效果显著。感兴趣的读者还可以通过调整堆空间大小结合GC过程进行观测,这里不做多余赘述。
逃逸分析还有其他几种技术:
- 同步清除技术
- 标量替换技术
// 正常代码,程序员可见 publicvoidvisit() { Object obj = newObject(); synchronized(obj) { System.out.println("this is visit"); } } // 优化后代码,程序员不可见 publicvoidvisit() { Object obj = newObject(); System.out.println("this is visit"); }
// 正常代码,程序员可见 publicclassStackDispatch { privatestaticfinalintnum = 1000000; publicstaticvoidmain(String[] args) { asignTeacher(); } privatestaticvoidasignTeacher() { Teacher teacher = newTeacher(); System.out.println("age = "+teacher.getAge()); } } classTeacher { privateintage; privateString name; privateString sex; privateString slogan; Teacher() { age = 0; name = ""; sex = ""; slogan = ""; } // 省略getter和setter }
// 优化后代码,程序员不可见 publicclassStackDispatch { privatestaticfinalintnum = 1000000; publicstaticvoidmain(String[] args) { asignTeacher(); } privatestaticvoidasignTeacher() { intage; String name; String sex; String slogan; System.out.println("age = "+ age); } }
不难发现,teacher的作用域没有逃逸出asignTeacher()方法,因此标量替换技术将Teacher类中的字段转移到方法的栈上,而不是在堆中分配。因为不需要创建对象,因此避免了堆内存空间的使用。标量替换可以视作栈上分配的一种特例,实现更简单但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。
逃逸分析技术一定程度上提高了内存使用率,但是这种技术并非是上帝恩赐,它依旧存在一些问题:
- 执行分析的过程本身就很复杂,具有较大的计算成本,分析带来的收益如果无法弥补其开销,那将丧失分析的意义
- 对于对象是否逃逸的结果判定,目前无法实现100%的准确率
- 如果分析结果表明,几乎所有对象都没有逃逸,那么在运行时所作的分析等同于无用功
全部评论
(0) 回帖