系列文章目录
- 面试题分享之Java并发篇
- 面试题分享之Java集合篇(二)
前言
学过Java的小伙伴肯定对JVM(Java虚拟机)多多少少了解一点,Java的“一次编译,到处运行”的特点就离不开他。今天我们就通过面试题去进一步的了解它。🌈
一、说一说你对JVM内存模型的理解
JVM内存区粗略的可以划分为堆和栈,按照虚拟机规范,又可分为一下几个区域:
JVM内存分为线程私有区和线程公共区。其中,堆内存和方法区属于线程公共区,虚拟机栈、本地方法区和程序计数器属于线程私有区,每个区的组成如上图所示。
二、你知道Java对象的创建过程吗?
我们大致可以分为六个过程:
-
类加载:首先,JVM会从类路径中加载所需要创建的对象类,如果该类没有被加载,JVM会根据类的全限定名找到对应的字节码文件,并加载到内存里面去。
- 分配内存:一旦类被加载之后,JVM会在堆中分配内存来存储实例数据,在堆中生成的内存地址作为对象的引用。
- 初始化零值:在分配内存后,JVM会对分配的内存进行初始化。例如:对基本数据类型int初始化为零,引用数据类型初始化为null。
- 设置对象头:在对象实例化之前,JVM会设置对象头,用于存储对象元信息,如:哈希码,GC信息等。对象头的大小由JVM的实现决定。
- 执行构造函数:一旦对象的内存空间准备好,设置对象头完毕。JVM会根据构造函数来完成实例化。构造函数会对对象的实例字段进行初始化,可以执行其他必要的操作。
- 返回对象引用:当构造函数执行完毕后,对象的状态就被完全初始化了。JVM会返回对象的引用,并赋值给变量,以便后续使用对象。
🧑💼面试官追问:JVM创建对象时,堆会发生抢占吗?
有可能会。
- 如果线程同时执行new操作创建对象时,理论上是存在可能发生堆的抢占情况的。如:假设线程A分配了一块内存,但是还没有发生指针的移动去实例化对象,这时候线程B也同时分配到这个内存,这个时候线程B的指针跟线程A的指针就会发生冲突。
- 如果堆内存不足也会导致线程发生竞争抢占。JVM在堆不足时,会触发垃圾回收去清理不使用的对象,把内存空间给新的对象分配。
🧑💼面试官继续追问:如何解决堆抢占这个问题呢?
- 采用CAS分配重试的方法来保证更新的原子性。
- 每个线程在Java堆中预先分配一小块内存,也就是本地线程分配缓冲,要分配内存的线程,现在本地缓冲区中分配,只有本地缓冲区用完了,才回去锁定新的后缓存区。
三、如何判断对象仍然存活?(如何判断对象需要被回收?)
- 引用计数法:该方法通过对对象进行引用计数,每次对象时计数加1,当引用计数为0时,表示没有被引用,可以被回收。但是引用技术法无法解决循序引用的问题,导致内存泄漏。
- 可达性分析法:常见的垃圾回收算法中,主要采用的可达性分析算法。该算法从一组成为“根”的特定对象(如全局变量、活动线程等)开始,通过追踪对象之间的引用关系形成引用图,然后检查哪些对象可以从“根”对象访问到。如果一个对象可以从“根”对象访问到,那么它就会被认为是存活的。而对于无法从“根”对象访问到的对象,则被判断为不在存活,垃圾回收器将其进行回收。
🧑💼面试官追问:垃圾算法了解吗?
- 标记-清除算法:该算法通过标记对象的可达性来确定对象存活,清除未标记的对象。这种算法虽然零活,但是可能会产生内存碎片。
- 标记-复制算法:该算法需要额外的内存区,将正在使用的内存区里面存活的对象进行标记,然后复制到新的缓存区里面。缺点:需要额外的内存区来保存复制对象,与其他算法比,有一定的内存浪费。
- 标记-整理算法:该算法首先标记存活的对象,然后将他们紧凑的排列到内存区的一端,清除未标记的对象。缺点:需要进行对象移动的操作,可能增加垃圾回收的时间消耗。
- 分代回收算法:该算法根据对象的生命周期将内存分为不同的区。新创建的对象会分到新生代,经过多次回收仍然存活的对象会晋升到老年代。不同带使用不同的垃圾回收策略,以提高效率。
- 新生代:每次都有大量对象消亡,因为有老年代作为内存担保,通常采用复制算法。
- 老年代:对象存活时间长,可采用标记清除、标记整理算法。
四、能详细说一下CMS收集器的垃圾收集过程吗?
CMS(Concurrent Mark Sweep)收集器:是一种以获取最短回收停顿时间为目标的收集器。它主要针对响应时间敏感的应用程序,并通过执行大部分垃圾收集工作来减少停顿时间。
CMS收集器的垃圾收集过程主要分为五个阶段:
收集流程:
- 初始标记:暂停所有其他线程,记录直接与root连接的对象,速度很快
- 并发标记:同时开启GC和用户线程,使用一个闭包结构去记录可达对象。但是在这个阶段结束后,这个闭包结构并不能保证当前所有对象可达,因为用户线程会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。这个算法作用就是跟踪记录这些发生引用更新的地方。
- 重新标记:作用是修正并发标记时因为用户程序而导致标记变更的那一部分对象的标记记录,这个阶段的停顿时间比初始标记时间长,但是远远比并发标记时间短。
- 并发清除:开启用户线程后,同时GC线程开始对未标记的区域做清扫。
五、G1垃圾收集器了解吗?
G1垃圾收集器的设计目标是在停顿时间可控的情况下,最大化系统吞吐量,它旨在提供更可控、更高效的垃圾回收性能。
- 面向服务器的垃圾收集器,极高概率满足GC停顿时间和吞吐量有要求。
- 并行与并发:可以充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者核心)来缩短stop-the-world停顿时间。部分其他收集器原本需要停顿Java线程执行GC,但是G1仍然可以通过并发的方式让Java程序继续执行。
- 分代收集:不需要和其他收集器配合,自己使用分代收集。
- 混合回收:同时处理新生代和老年代的垃圾回收,相比于传统的分代回收,它可以均衡的处理整个堆内存,避免长时间的Full GC暂停。
- 可预测的停顿:G1的一个大优势,G1除了追求低停顿外,还能建立可预测的时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
六、说一下尽量避免内存泄漏的方法?
- 尽量不要使用
static
成员变量,减少生命周期;- 及时关闭无用的资源
- 不用的对象,可以手动设置为 null
七、谈谈你对类加载机制的了解?
虚拟机把描述类的数据从Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
- 加载:查找并加载类的二进制数据
- 验证:确保被加载的类的正确性
- 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把类中的符号引用转换为直接引用
- 初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化