Bootstrap

jvm面试题

讲一下JVM内存结构?

JVM内存结构分为5大区域,程序计数器、虚拟机栈、本地方法栈、堆、方法区。
在这里插入图片描述
程序计数器
线程私有的,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址。程序计数器主要有两个作用:

当前线程所执行的字节码的行号指示器,通过它实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来的时候能够知道它上次执行的位置。
程序计数器是唯一一个不会出现 ​​OutOfMemoryError​​ 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

虚拟机栈
Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束后,都会有一个栈帧被弹出。 局部变量表是用于存放方法参数和方法内的局部变量。 每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,在方法调用过程中,会进行动态链接,将这个符号引用转化为直接引用。

部分符号引用在类加载阶段的时候就转化为直接引用,这种转化就是静态链接
部分符号引用在运行期间转化为直接引用,这种转化就是动态链接
Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。Java 虚拟机栈会出现两种错误:​​StackOverFlowError​​​ 和 ​​OutOfMemoryError​​。

可以通过​​ -Xss ​​参数来指定每个线程的虚拟机栈内存大小:

java -Xss2M
1.
本地方法栈
虚拟机栈为虚拟机执行 ​​Java​​​ 方法服务,而本地方法栈则为虚拟机使用到的 ​​Native​​​ 方法服务。​​Native​​ 方法一般是用其它语言(C、C++等)编写的。 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。


堆用于存放对象实例,是垃圾收集器管理的主要区域,因此也被称作​​GC​​​堆。堆可以细分为:新生代(​​Eden​​​空间、​​From Survivor​​​、​​To Survivor​​空间)和老年代。

通过 ​​-Xms​​​设定程序启动时占用内存大小,通过​​-Xmx​​​设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出​​OutOfMemory​​异常。

java -Xms1M -Xmx2M
1.
方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载。

永久代

方法区是 JVM 的规范,而永久代​​PermGen​​​是方法区的一种实现方式,并且只有 ​​HotSpot​​​ 有永久代。对于其他类型的虚拟机,如​​JRockit​​没有永久代。由于方法区主要存储类的相关信息,所以对于动态生成类的场景比较容易出现永久代的内存溢出。

元空间

JDK 1.8 的时候,​​HotSpot​​的永久代被彻底移除了,使用元空间替代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。两者最大的区别在于:元空间并不在虚拟机中,而是使用直接内存。 为什么要将永久代替换为元空间呢? 永久代内存受限于 JVM 可用内存,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是相比永久代内存溢出的概率更小。

运行时常量池
运行时常量池是方法区的一部分,在类加载之后,会将编译器生成的各种字面量和符号引号放到运行时常量池。在运行期间动态生成的常量,如 String 类的 intern()方法,也会被放入运行时常量池。

直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。 NIO的Buffer提供了DirectBuffer,可以直接访问系统物理内存,避免堆内内存到堆外内存的数据拷贝操作,提高效率。DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制,不受最大堆内存的限制。 直接内存的读写操作比堆内存快,可以提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到直接内存。

Java对象的定位方式

Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种:

如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
直接指针。reference 中存储的直接就是对象的地址。对象包含到对象类型数据的指针,通过这个指针可以访问对象类型数据。使用直接指针访问方式最大的好处就是访问对象速度快,它节省了一次指针定位的时间开销,虚拟机hotspot主要是使用直接指针来访问对象。

说一下堆栈的区别?

堆的物理地址分配是不连续的,性能较慢;栈的物理地址分配是连续的,性能相对较快。
堆存放的是对象的实例和数组;栈存放的是局部变量,操作数栈,返回结果等。
堆是线程共享的;栈是线程私有的。

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

当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出​​StackOverFlowError​​异常。这种情况通常是因为方法递归没终止条件。
新建线程的时候没有足够的内存去创建对应的虚拟机栈,虚拟机会抛出​​OutOfMemoryError​​异常。比如线程启动过多就会出现这种情况。

类文件结构

ClassFile {
    u4             magic; //类文件的标志
    u2             minor_version;//小版本号
    u2             major_version;//大版本号
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//类的访问标记
    u2             this_class;//当前类的索引
    u2             super_class;//父类
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一个类可以实现多个接口
    u2             fields_count;//字段属性
    field_info     fields[fields_count];//一个类会可以有个字段
    u2             methods_count;//方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法
    u2             attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
}

什么是类加载?类加载的过程?

类的加载指的是将类的​​class​​文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个此类的对象,通过这个对象可以访问到方法区对应的类信息。

  1. 通过类的全限定名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的​​Class​​对象,作为方法区类信息的访问入口验证

验证
确保Class文件的字节流中包含的信息符合虚拟机规范,保证在运行后不会危害虚拟机自身的安全。主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。

准备
为类变量分配内存并设置类变量初始值的阶段。

解析
虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用用于描述目标,直接引用直接指向目标的地址。

初始化
开始执行类中定义的​​Java​​代码,初始化阶段是调用类构造器的过程。

什么是双亲委派模型?

一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

为什么需要双亲委派模型?

双亲委派模型的好处:可以防止内存中出现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个​​java.lang.Object​​​的同名类并放在​​ClassPath​​​中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的​​Object​​类,那么类之间的比较结果及类的唯一性将无法保证。

什么是类加载器,类加载器有哪些?

启动类加载器:用来加载 Java 核心类库,无法被 Java 程序直接引用。
扩展类加载器:它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
系统类加载器:它根据应用的类路径来加载 Java 类。可通过​​ClassLoader.getSystemClassLoader()​​获取它。
自定义类加载器:通过继承​​java.lang.ClassLoader​​类的方式实现。

类的实例化顺序?

父类中的​​static​​​代码块,当前类的​​static​​代码块
父类的普通代码块
父类的构造函数
当前类普通代码块
当前类的构造函数

如何判断一个对象是否存活?

对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不再被任何途径引用的对象)。判断对象是否存活有两种方法:引用计数法和可达性分析。

请判断以下场景对象是否会被回收?

a.b = b, b.a = a 除了这两个引用以外,a和b没有被别地方的引用了。a和b会被回收吗?
如果a和b作为GCRoot则不会,否则会。

可作为GC Roots的对象有哪些?

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

强引用、软引用、弱引用、虚引用是什么,有什么区别?

强引用:在程序中普遍存在的引用赋值,类似​​Object obj = new Object()​​这种引用关系。只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。

弱引用:在进行垃圾回收时,不管当前内存空间足够与否,都会回收只具有弱引用的对象。

虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要是为了能在对象被收集器回收时收到一个系统通知。

Minor GC 和 Full GC的区别?

Minor GC:回收新生代,因为新生代对象存活时间很短,因此 ​​Minor GC​​会频繁执行,执行的速度一般也会比较快。
Full GC:回收老年代和新生代,老年代的对象存活时间长,因此 ​​Full GC​​​ 很少执行,执行速度会比 ​​Minor GC​​ 慢很多。

内存的分配策略?

对象优先在 Eden 分配
大多数情况下,对象在新生代 ​​Eden​​​ 上分配,当 ​​Eden​​​ 空间不够时,触发 ​​Minor GC​​。

大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象有长字符串和大数组。可以设置JVM参数 ​​-XX:PretenureSizeThreshold​​,大于此值的对象直接在老年代分配。

长期存活的对象进入老年代
通过参数 ​​-XX:MaxTenuringThreshold​​​ 可以设置对象进入老年代的年龄阈值。对象在​​Survivor​​​区每经过一次 ​​Minor GC​​,年龄就增加 1 岁,当它的年龄增加到一定程度,就会被晋升到老年代中。

动态对象年龄判定
并非对象的年龄必须达到 ​​MaxTenuringThreshold​​​ 才能晋升老年代,如果在 ​​Survivor​​​ 中相同年龄所有对象大小的总和大于 ​​Survivor​​​ 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需达到 ​​MaxTenuringThreshold​​ 年龄阈值。

空间分配担保
在发生 ​​Minor GC​​​ 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 ​​Minor GC​​​ 是安全的。如果不成立的话虚拟机会查看 ​​HandlePromotionFailure​​​ 的值是否允许担保失败。如果允许,那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 ​​Minor GC​​​;如果小于,或者 ​​HandlePromotionFailure​​​ 的值为不允许担保失败,那么就要进行一次 ​​Full GC​​。

Full GC 的触发条件?

对于 Minor GC,其触发条件比较简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 触发条件相对复杂,有以下情况会发生 full GC:

调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。

垃圾回收算法有哪些?

垃圾回收算法有四种,分别是标记清除法、标记整理法、复制算法、分代收集算法。

标记清除算法
首先利用可达性去遍历内存,把存活对象和垃圾对象进行标记。标记结束后统一将所有标记的对象回收掉。这种垃圾回收算法效率较低,并且会产生大量不连续的空间碎片。

复制清除算法
半区复制,用于新生代垃圾回收。将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。 特点:实现简单,运行高效,但可用内存缩小为了原来的一半,浪费空间。

标记整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与​​标记-清除​​算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

分类收集算法
根据各个年代的特点采用最适当的收集算法。 一般将堆分为新生代和老年代。
新生代使用复制算法
老年代使用标记清除算法或者标记整理算法
在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,使用复制算法比较合适,只需要付出少量存活对象的复制成本就可以完成收集。老年代对象存活率高,适合使用标记-清理或者标记-整理算法进行垃圾回收。

有哪些垃圾回收器?

| 收集器 | 串行、并行or并发 |新生代/老年代|算法|目标|适用场景

收集器串行、并行or并发新生代/老年代算法目标适用场景
Serial串行新生代复制算法响应速度优先单CPU环境下的Client模式
ParNew并行新生代复制算法响应速度优先多CPU环境时在Server模式下与CMS配合
Parallel Scavenge并行新生代复制算法吞吐量优先在后台运算而不需要太多交互的任务
Serial Old串行老年代标记-整理响应速度优先单CPU环境下的Client模式、CMS的后备预案
Parallel Old并行老年代标记-整理吞吐量优先在后台运算而不需要太多交互的任务
CMS并发老年代标记-清除响应速度优先集中在互联网站或B/S系统服务端上的Java应用
G1并发both标记+整理+复制算法响应速度优先面向服务端应用,将来替换CMS

Serial
收集器单线程收集器,使用一个垃圾收集线程去进行垃圾回收,在进行垃圾回收的时候必须暂停其他所有的工作线程( ​​Stop The World​​ ),直到它收集结束。 特点:简单高效;内存消耗小;没有线程交互的开销,单线程收集效率高;需暂停所有的工作线程,用户体验不好。

ParNew 收集器
​​Serial​​收集器的多线程版本,除了使用多线程进行垃圾收集外,其他行为、参数与 ​​Serial​​ 收集器基本一致。

Parallel Scavenge 收集器新生代收集器,基于复制清除算法实现的收集器。特点是吞吐量优先,能够并行收集的多线程收集器,允许多个垃圾回收线程同时运行,降低垃圾收集时间,提高吞吐量。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值(​​吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)​​​)。

Serial Old 收集器
​​Serial​​ 收集器的老年代版本,单线程收集器,使用标记整理算法。

Parallel Old 收集器
​​Parallel Scavenge​​ 收集器的老年代版本。多线程垃圾收集,使用标记整理算法。

CMS 收集器
​​Concurrent Mark Sweep​​ ,并发标记清除,追求获取最短停顿时间,实现了让垃圾收集线程与用户线程基本上同时工作。
​CMS​​ 垃圾回收基于标记清除算法实现,整个过程分为四个步骤:
初始标记: 暂停所有用户线程(​​Stop The World​​​),记录直接与 ​​GC Roots​​ 直接相连的对象 。
并发标记:从​​GC Roots​​开始对堆中对象进行可达性分析,找出存活对象,耗时较长,但是不需要停顿用户线程。
重新标记: 在并发标记期间对象的引用关系可能会变化,需要重新进行标记。此阶段也会暂停所有用户线程。
并发清除:清除标记对象,这个阶段也是可以与用户线程同时并发的。
在整个过程中,耗时最长的是并发标记和并发清除阶段,这两个阶段垃圾收集线程都可以与用户线程一起工作,所以从总体上来说,​​CMS​​收集器的内存回收过程是与用户线程一起并发执行的。
优点:并发收集,停顿时间短。
缺点:
标记清除算法导致收集结束有大量空间碎片。
产生浮动垃圾,在并发清理阶段用户线程还在运行,会不断有新的垃圾产生,这一部分垃圾出现在标记过程之后,​​CMS​​无法在当次收集中回收它们,只好等到下一次垃圾回收再处理;

G1收集器
G1将整个堆分成相同大小的分区(​​Region​​​),有四种不同类型的分区:​​Eden、Survivor、Old和Humongous​​​。分区的大小取值范围为 1M 到 32M,都是2的幂次方。分区大小可以通过​​-XX:G1HeapRegionSize​​​参数指定。​​Humongous​​区域用于存储大对象。G1规定只要大小超过了一个分区容量一半的对象就认为是大对象。
G1 收集器对各个分区回收所获得的空间大小和回收所需时间的经验值进行排序,得到一个优先级列表,每次根据用户设置的最大回收停顿时间,优先回收价值最大的分区。
特点:可以由用户指定期望的垃圾收集停顿时间。 G1 收集器的回收过程分为以下几个步骤:
初始标记。暂停所有其他线程,记录直接与 ​​GC Roots​​ 直接相连的对象,耗时较短 。
并发标记。从​​GC Roots​​开始对堆中对象进行可达性分析,找出要回收的对象,耗时较长,不过可以和用户程序并发执行。
最终标记。需对其他线程做短暂的暂停,用于处理并发标记阶段对象引用出现变动的区域。
筛选回收。对各个分区的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,然后把决定回收的分区的存活对象复制到空的分区中,再清理掉整个旧的分区的全部空间。这里的操作涉及存活对象的移动,会暂停用户线程,由多条收集器线程并行完成。

常用的 JVM 调优的命令都有哪些?

jps:列出本机所有 Java 进程的进程号。
jstack:查看某个 Java 进程内的线程堆栈信息。
jstat:用于查看虚拟机各种运行状态信息(类装载、内存、垃圾收集等运行数据)。
jmap:查看堆内存快照。

常用的 JVM 可视化工具都有哪些?

jconsole
JVisulVM

对象头了解吗?

Java 内存中的对象由以下三部分组成:对象头、实例数据和对齐填充字节。
而对象头由以下三部分组成:mark word、指向类信息的指针和数组长度(数组才有)。
​​mark word​​包含:对象的哈希码、分代年龄和锁标志位。 对象的实例数据就是 Java 对象的属性和值。 对齐填充字节:因为JVM要求对象占的内存大小是 8bit 的倍数,因此后面有几个字节用于把对象的大小补齐至 8bit 的倍数。

如何排查 OOM 的问题?

增加JVM参数 ​​-XX:+HeapDumpOnOutOfMemoryError​​​ 和​​-XX:HeapDumpPath=/tmp/heapdump.hprof​​,当 OOM 发生时自动 dump 堆内存信息到指定目录;
jstat 查看监控 JVM 的内存和 GC 情况,评估问题大概出在什么区域;
使用 MAT 工具载入 dump 文件,分析大对象的占用情况 。

生产环境 CPU 占用过高,你如何解决?

top + H 指令找出占用 CPU 最高的进程的 pid
top -H -p
在该进程中找到,哪些线程占用的 CPU 最高的线程,记录下 tid
jstack -l
threads.txt,导出进程的线程栈信息到文本,导出出现异常的话,加上 -F 参数
将 tid 转换为十六进制,在 threads.txt 中搜索,查到对应的线程代码执行栈,在代码中查找占 CPU 比较高的原因。其中 tid 转十六进制,可以借助 Linux 的 printf “%x” tid 指令

生产环境服务器变慢,如何诊断处理?

使用 top 指令,服务器中 CPU 和 内存的使用情况,-H 可以按 CPU 使用率降序,-M 内存使用率降序。排除其他进程占用过高的硬件资源,对 Java 服务造成影响。
如果发现 CPU 使用过高,可以使用 top 指令查出 JVM 中占用 CPU 过高的线程,通过 jstack 找到对应的线程代码调用,排查出问题代码。
如果发现内存使用率比较高,可以 dump 出 JVM 堆内存,然后借助 MAT 进行分析,查出大对象或者占用最多的对象来自哪里,为什么会长时间占用这么多;如果 dump 出的堆内存文件正常,此时可以考虑堆外内存被大量使用导致出现问题,需要借助操作系统指令 pmap 查出进程的内存分配情况、gdb dump 出具体内存信息、perf 查看本地函数调用等。
如果 CPU 和 内存使用率都很正常,那就需要进一步开启 GC 日志,分析用户线程暂停的时间、各部分内存区域 GC 次数和时间等指标,可以借助 jstat 或可视化工具 GCeasy 等,如果问题出在 GC 上面的话,考虑是否是内存不够、根据垃圾对象的特点进行参数调优、使用更适合的垃圾收集器;分析 jstack 出来的各个线程状态。如果问题实在比较隐蔽,考虑是否可以开启 jmx,使用 visualmv 等可视化工具远程监控与分析。

JIT 是什么?

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

参考:

https://blog.51cto.com/u_13401476/4918258
https://blog.51cto.com/u_15061941/2591599

;