JVM面试系列-03

2022年7月17日
大约 22 分钟

JVM面试系列-03

1. Java 中 JVM 什么时候会触发 FullGC?

除直接调用System.gc外,触发Full GC执行的情况有如下四种。

1、旧生代空间不足

旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:

java.lang.OutOfMemoryError: Java heap space

为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

2、Permanet Generation空间满

PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:

java.lang.OutOfMemoryError: PermGen space

为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

3、CMS GC时出现promotion failed和concurrent mode failure

对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。

promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。

应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。

4、统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间

这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。

实例:程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。

当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,(实例)第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。

除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过:

-java-Dsun.rmi.dgc.client.gcInterval=3600000

来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

2. Java 中什么是对象结构?

Java对象由三个部分组成:对象头、实例数据、对齐填充。

对象头由两部分组成:

第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。

第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。

如果是数组对象,则对象头中还有一部分用来记录数组长度。

实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的) 对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)。

3. Java 中如何判断对象可以被回收?

判断对象是否存活一般有两种方式:

引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。

4. JVM 中永久代中会发生垃圾回收吗?

垃圾回收不会发生在永久代。

如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。

如果仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。

Java1.8版本已经移除了永久代,新增加了一个叫做元数据区的native内存区。

5. JVM 中调优命令有哪些?

Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo。

1、jps

JVM Process Status Tool显示指定系统内所有的HotSpot虚拟机进程。

2、jstat

JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

3、jmap

JVM Memory Map命令用于生成heap dump文件。

4、jhat

JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。

5、jstack

用于生成java虚拟机当前时刻的线程快照。

6、jinfo

JVM Configuration info这个命令作用是实时查看和调整虚拟机运行参数。

6. Java 中永久代为什么JDK1.8 之后被废弃?

1、现实使用中易出问题。

由于永久代内存经常不够用或者发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen。

字符串存在永久代中,容易出现性能问题和内存溢出。

类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

2、永久代会位GC带来不必要的复杂度,而且回收效率偏低。

3、Oracle可能会将HotSpot和JRockit合二为一。

参照JEP122原文截取:

Motivation

This is part of the JRockit and Hotspot convergence effort. 
JRockit customers do not need to configure the permanent
 generation (since JRockit does not have a permanent generation)
 and are accustomed to not configuring the permanent generation.

即:移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。

7. Java 中什么是 HotSpot?

Java是解释语言,但并不意味着它一定被解释执行。早期的虚拟机确实一条一条指令解释执行,但人们发现这样效率太低,不满足各种要求,因此出现了许多其它虚拟机,如JIT的虚拟机。

HotSpot也是类似一种虚拟机,自从SUN买下后,已经把它放入JRE 1.3以及后续版本中。

采用HotSpot的Java虚拟机,已经很难说Java是被虚拟机解释执行了,原因是HotSpot实际上是把Java的bytecode编译成Native code,然后运行。

实际上在HotSpot虚拟机中,有两个技术是至关重要的,即动态编译和Profiling。HotSpot对bytecode的编译,不是在程序运行前预先编译的,而是在程序运行过程中,动态编译(compile during run-time),英文称Dynamic compilation。其实Just In Time也就是这个意思。

HotSpot是如何动态编译Javad的bytecode呢?它采用的是一种smart的办法。

HotSpot里有一个运行监视器,即Profile Monitor(不知国内如何翻译Profile),专门监视程序运行中,哪一部分运用频度大, 哪些对性能影响至关重要。

当然Profile Monitor有一些算法,这些算法未必十全十美,但大体是能较好获得相关信息的。对于那些对程序运行效率影响交大的代码,称为热点,即hot spot,HotSpot会把这些部门动态地编译成机器码,Native code,

同时也对机器码进行优化(类似C编译器的一些优化),从而而提高运行效率。

而那些较少运行的Code,HotSpot虚拟机就不再浪费时间把它们编译。

总体来看,Java bytecode是以解释方式被load到虚拟机的。但虚拟机的分析器根据一段运行,获知对程序效率影响最大的部分,然后通过动态编译,同时进行优化,编译成机器码,然后为接下来的运行加速。总的来说,HotSpot对bytecode有三层处理:不编译,编译,编译并优化。

至于程序哪部分不编译,哪部分编译,哪部分做何种优化,则由ProfileMonitor决定。

那么为什么Java采用动态编译器而不是象C++这样采用静态编译器呢?

虚拟机提供的跨平台运行条件固然是一方面,动态编译器也在许多方面比静态编译器优越。Profiling就是一个例子。

静态编译器通常很难准确预知程序运行过程中究竟什么部分最需要优化。静态编译器虽然可以把Java全部编译成Native Code,但却做不到动态编译器那样的优化。另一个典型的例子,叫做Method inlining。

我们知道无论是在C还是在Java里,函数调用都是很浪费系统时间的,因为有许多进栈出栈操作。

因此有一种优化办法,就是把原来的函数调用,通过编译器的编译,改成非函数调用,把函数代码直接嵌到调用出,变成顺序执行。

但这一方法在Java/C++这样的面向对象的语言的编译器中,较难很好实现。

那些静态编译器,通常可以把private,static等函数进行Method inlining,但由于这些面向对象的语言支持函数重载,支持动态联编(不知道是不是这样翻译,Overridden, dynamic binding),因此静态编译器并不知道究竟应该把函数的哪个实现给inline了。

HotSpot的动态编译,由于有对函数调用的监视,因此可以准确地知道一些环境下,那些被重载和动态识别的函数可以如何被inline到调用者那里去,因此实际上对于一些Server应用来说,可以大幅度提高效率。

HotSpot实际上有两个版本,一个是Server版,一个是Client版。但它们的结构和本质都是一样的,只是有些地方优化不一样。了解了这些,就知道,有时候Java的程序甚至能比C程序运行还快。

8. JVM 中内存区域分类有哪些?

方法区

1)有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型的卸载

2)方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。

3)该区域是被线程共享的。

4)方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。

该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。

虚拟机栈

1)虚拟机栈也就是我们平常所称的栈内存,它为java方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。

2)虚拟机栈是线程私有的,它的生命周期与线程相同。

3)局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定

4)操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式 5.每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。

本地方法栈本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。

堆java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。

程序计数器内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。

9. JVM 中如何判断一个对象是否存活?

判断一个对象是否存活有两种方法:

1、引用计数法所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。

当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收。引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。

2、可达性算法(引用链法)该算法的思想是:从一个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。在java中可以作为GC Roots的对象有以下几种:

虚拟机栈中引用的对象 方法区类静态属性引用的对象 方法区常量池引用的对象 本地方法栈JNI引用的对象

虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一定会被回收。

当一个对象不可达GC Root时,这个对象并不会立马被回收,而是处于一个死缓的阶段,若要被真正的回收需要经历两次标记如果对象在可达性分析中没有与GC Root的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。

如果该对象有必要执行finalize()方法,那么这个对象将会放在一个称为F-Queue的对队列中,虚拟机会触发一个Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize()执行缓慢或者发生了死锁,那么就会造成F-Queue队列一直等待,造成了内存回收系统的崩溃。GC对处于F-Queue中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。

10. Java 中什么是内存模型?

java内存模型(JMM)是线程间通信的控制机制,JMM定义了主内存和线程之间抽象关系。

线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

11. Java 中 JVM 使用哪些寄存器?

所有进程都使用寄存器,Java虚拟机使用下列寄存器管理系统堆栈:

程序记数寄存器:跟踪程序执行的准确位置

堆栈指针寄存器:指示操作栈项

框架寄存器:指向当前执行的环境

变量寄存器:指向当前执行环境中第一个本地变量

Java开发组决定Java只使用四个寄存器,这是因为如果使用的寄存器数多于处理器端口数,那么处理器的效率将严重地降低。

Java虚拟机中的堆栈用于存放变量,Java程序向Java虚拟机传递字节码,Java虚拟机为每个方法创建堆栈框架,每个框架维护三种信息:

1)局部变量:由变量寄存器指向的32位变量数组。 2)执行环境:由框架寄存器指向和执行的方法。 3)操作堆栈:执行先进先出规则(FIFO),它是32位宽度的,它为操作码维护必要的参数,该堆栈的顶部是由堆栈指针寄存器索引的。

寄存器位于处理器内部,这一点和其他的存储媒介都不一样。不过寄存器个数是有限的。在内存中的寄存器区域是由编译器根据需要来分配的。程序开发人员不能够通过代码来控制这个寄存器的分配。

12. 为什么新生代不使用标记-整理算法?

首先大家应该清楚新生代使用的是复制算法。

复制算法与标记-整理算法都有标记的过程,标记的过程都是采用可达性分析算法。

复制算法是把标记存活的对象复制到另一块内存区域中,而标记-整理算法是将标记的对象整理到一端。此时需要注意的是并没有进行内存空间清理,对于新生代需要清理的对象数量十分巨大,所以在将存活的对象插入到待清理对象之前,需要大量移动操作,时间复杂度很高;而反观复制算法,不需要移动待回收对象的操作,直接将存活对象复制到另一块空闲内存区域中,大大减小了时间复杂度,所以通过分析新生代不使用“标记-整理算法”的原因就显而易见了。

13. 垃圾回收对象时程序的逻辑是否可以继续执行?

不同回收器不同:Serial、ParNew会暂停用户所有线程工作;CMS、G1会在某一阶段暂停用户线程。

内存分配策略

对象优先在Eden分配:若Eden无空间,Java虚拟机发起一次Minor GC。

大对象直接进入老年代:大对象指需要大量连续内存空间的对象(如长数组、长字符串)。

长期存活的对象进入老年代:每个对象有一个对象年龄计数器,age=15晋升为老年代。age+1的两个情况:对象在Eden出生并经过一次Minor GC存活且被survivor容纳;在survivor区经历过一次minor GC。

14. 简述 minor gc 和 full gc?

Minor GC:从新生代回收内存,关键是Eden区内存不足,造成不足的原因是Java对象大部分是朝生夕死(java局部对象),而死掉的对象就需要在合适的时机被JVM回收。

Major GC:从老年代回收内存,一般比Minor GC慢10倍以上。

Full GC:对整个堆来说的,出现Full GC通常伴随至少一次Minor GC,但非绝对。Full GC被触发的时候:老年代内存不足;持久代内存不足;统计得到的Minor GC晋升到老年代平均大小大于老年代空间。

15. JVM 如何判断对象是否失效?可达性分析是否可以解决循环引用?

引用计数器算法:给对象添加一个引用计数器,当被引用时给计数器加1,引用失效减1,当为0时对象失效。实现简单,判定效率高,无法解决循环引用问题。

可达性分析算法:将一系列GC Root作为起始点,从这些节点开始向下搜索,所走过路径称为引用链,若一个对象无引用链,则判断是否执行finalize()方法,若finalize()被覆盖并且没被JVM调用过,则执行此方法,执行后若还无引用链,则对象失效。

可以作为GC Root的对象:

1)虚拟机栈中引用的对象。

2)方法区中类静态属性引用的对象。

3)方法区中常量引用的对象。

4)本地方法栈中Native方法引用的对象。

16. Java 内存模型的 happen before 原则?

如果两个操作存在happens-before关系,那么前一个操作的结果就会对后面一个操作可见,是定义的两个操作之间的偏序关系,常见的规则:

程序顺序规则:一个线程中每个操作,happens-before于该线程中的任意后续操作。

监视器锁规则:对一个锁的解锁,happens-before于随后这个锁的加锁。

volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个域的读。

传递性:若A happens-before B,B happens-before C,则A happens-before C。

start()规则:如果线程A执行ThreadB.start(),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。

join()规则:若线程A 执行ThreadB.join()并成功返回,则线程B的任意操作happens-before于线程A从ThreadB.jion()操作返回成功。

17. 虚拟机栈中有哪几部分组成?

局部变量表:存放方法参数和方法内部定义的局部变量,以变量槽Slot为基本单位,一个Slot可以存放32位以内的数据类型,可重用。

操作数栈:先入后出,32位数据类型所占栈容量为1,64为数据类型所占栈容量为2。

动态链接:常量池中符号引用有一部分在每次运行期间转换为直接引用,这部分称为动态链接。(一部分在类加载阶段或第一次使用时转换为直接引用—静态解析)。

方法返回地址:方法执行后退出的两种方式:正常完成出口(执行引擎遇到任意一个返回的字节码指令)和异常完成出口(在方法执行过程中遇到异常且此异常未被处理)。两种方式都需要返回到方法被调用的位置程序才能继续执行(正常退出时调用者的PC计数器的值可以作为返回地址且栈帧中很可能保存这个计数器值;异常退出返回地址要通过异常处理器表来确定,栈帧中一般不会保存)。

18. 对象的内存布局有哪几部分组成?

对象内存布局分为三部分:对象头、实例数据、对齐填充。

对象头包含两部分:

1)存储对象自身运行时数据:哈希码、分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等

2)对象指向它的类元数据指针–类型指针。

实例数据:程序代码中所定义的各种类型的字段内容。

对齐填充:不是必然存在,仅起到占位符作用(对象大小必须是8子节整数倍)

19. 什么是静态分派与动态分派?

静态分派: 发生在编译时期,所有依赖静态类型来定位方法执行版本的分派称为静态分派,典型应用为方法重载。

动态分派: 在运行期根据实际类型确定方法执行版本的分派过程。

典型应用为方法重写,实现是在方法去中建立方法表,若子类中没有重写父类方法,则子类虚方法表中该方法的入口地址与父类指向相同,否则子类方法表中地址会替换为指向子类重写的方法的入口地址。

20. Perm Space 中保存什么数据?会引起 OutOfMemory 吗?

Perm Space中保存的是加载class文件。

会引起OutOfMemory异常,出现异常可以通过设置-XX:PermSize的大小解决问题。

JDK1.8版本后,字符串常量不存放在永久带,而是在堆内存中,JDK8以后没有永久代概念,而是用元空间替代,元空间不存在虚拟机中,二是使用本地内存。