JAVA面试:JVM面试题

讲讲JVM原理,JVM是做什么的

  • JVM(Java Virtual Machine)是Java虚拟机的简称,它是Java语言的核心部分之一,为运行Java应用程序提供了一个抽象化的计算系统。JVM的主要任务和功能包括以下几个方面:

字节码解释执行:

  • JVM负责接收和加载编译后的Java字节码(.class文件),并将其转化为机器可以识别的指令进行执行。这些字节码具有平台无关性,可以在任何安装了JVM的设备上运行。

内存管理与垃圾回收:

  1. JVM实现了自动内存管理机制,它将内存划分为不同的区域,如堆、栈、方法区等,并对每个区域进行动态分配和回收。
  2. 垃圾回收机制(Garbage Collection, GC)是JVM的一项重要特性,用于自动追踪并回收不再使用的对象所占用的内存空间,从而确保程序在运行过程中不会因为内存泄漏而崩溃。

类加载机制:

  • JVM通过类加载器实现类的生命周期管理,从磁盘读取类文件,执行验证、准备、解析和初始化等过程,使得Java类能够在运行时被正确加载和使用。

多线程支持:

  • JVM提供了内置的线程调度和管理机制,能够高效地创建和管理Java线程,以实现并发编程。

安全性和异常处理:

  • JVM提供了严格的类型检查、访问控制和异常处理机制,保障了Java程序的安全性和健壮性。

运行时优化:

  • 现代JVM(如HotSpot VM)还包含了即时编译器(Just-In-Time Compiler, JIT),把热点代码编译成本地机器码,进一步提高Java程序执行效率。
    总之,JVM是一个负责执行Java字节码并为其提供运行环境的虚拟计算机系统,它的存在保证了“一次编写,到处运行”(Write Once, Run Anywhere, WORA)这一Java语言的重要特性得以实现。

类加载机制、有哪些类加载器

  • JVM的类加载机制是一个动态的过程,它负责将编译后的Java字节码(.class文件)加载到内存中,并对类进行验证、准备、解析和初始化等操作,最终形成可运行的Java类型。
    这个机制包括以下步骤:
  1. 加载:通过类的全限定名获取该类对应的二进制字节流。
  2. 验证:确保被加载的类信息符合JVM规范,没有安全方面的问题。
  3. 准备:为类的静态变量分配内存并设置初始值(0或null)。
  4. 解析:把常量池中的符号引用转化为直接引用(如方法区内的指针或偏移量)。
  5. 初始化:如果类还没有被初始化,则会执行类初始化方法 ,即初始化静态变量和执行静态初始化块。

在JVM中,有如下几种标准的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)
    这是所有类加载器的根加载器,由C/C++编写,嵌入在JVM内部实现,主要负责加载Java的核心库(rt.jar、charsets.jar等),通常无法被Java程序直接引用。
  2. 扩展类加载器(Extension or ExtClassLoader)
    是Java语言实现的类加载器,负责加载标准扩展目录<JAVA_HOME>/lib/ext下的类库。
  3. 应用程序类加载器(Application or System ClassLoader)
    也是Java语言实现的类加载器,负责加载用户类路径(classpath)上的指定类库,它是用户自定义类加载器的父加载器。
  4. 自定义类加载器(User-Defined ClassLoader)

jvm双亲委派机制,为什么要用双亲委派机制?

  • JVM的双亲委派机制是一种类加载器间的协作模式,其核心思想是:当一个类加载器需要加载某个类时,并不是自己去尝试加载这个类,而是首先将该请求委派给父类加载器来完成。如果父类加载器无法完成加载(比如找不到指定类),子类加载器才会尝试自己去加载。

采用双亲委派机制的原因主要包括以下几点:

  1. 防止类重复加载:
    如果没有统一的加载规则,不同类加载器可能会加载同一个类的不同版本,导致类型混乱。通过双亲委派,确保所有类加载请求都遵循自上而下的层级结构,避免了同一类被多次加载到内存中。
  2. 保证Java平台的安全性:
    Java的核心类库都是由启动类加载器负责加载的,它在系统层次上有最高的优先级。这样可以确保Java运行环境的稳定和安全,防止恶意代码篡改或替换系统核心类库中的类。
  3. 实现类的统一视图:
    在Java程序的不同部分,对于同名类的请求都将得到相同的类实例,这对于支持跨模块的透明性和可移植性非常重要。
  4. 提供了一种基于类加载器的命名空间隔离策略:
    不同的应用、框架或者模块可以通过定制不同的类加载器来实现类的逻辑隔离,但同时又能共享公共类库,这有助于构建灵活且稳定的组件化架构。
    因此,双亲委派机制是Java设计者为解决类加载的一致性、安全性及隔离性问题而提出的解决方案,是Java虚拟机的重要特性之一。

JVM 怎么判断一个对象可以销毁了

  • 在Java中,当一个对象不再被任何引用指向时,JVM会将其标记为可回收的垃圾对象。
  • 垃圾回收器会定期扫描内存中的对象,标记哪些对象是可达的(即有引用指向的对象),哪些对象是不可达的(垃圾对象,即没有任何引用指向的对象)。

主要采用的是可达性分析算法

  1. GC Roots的定义:
    JVM会从一系列被称为“GC Roots”的根对象开始,这些根对象包括但不限于:虚拟机栈中引用的对象(如局部变量表中的引用)、方法区中类静态属性引用的对象、常量池中的引用、本地方法栈中JNI(Java Native Interface)引用的对象等。
  2. 可达性分析过程:
    从GC Roots出发,通过引用链向下搜索,遍历整个对象图并标记所有能直接或间接到达的对象。
    如果某个对象无法从任何一个GC Root通过引用链直接或间接达到,则说明这个对象是不可达的。
  3. 最终判定:
    当一轮可达性分析结束后,所有未被标记为可达的对象被认为是不可达对象,也就是垃圾对象,它们将进入待回收的状态。
  4. 进一步处理:
    被标记为垃圾的对象,并非立即被回收,而是等待垃圾收集器按照特定的垃圾回收策略进行回收,比如新生代采用复制算法、老年代使用标记-整理或者标记-压缩算法等。

垃圾回收器、垃圾回收算法

  • Java垃圾回收器是Java虚拟机(JVM)的重要组成部分,用于自动管理程序运行过程中不再使用的对象所占用的内存空间。以下是对几种常见的Java垃圾回收器及其垃圾回收算法:

垃圾回收算法:

  1. 引用计数法:每个对象都有一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。当计数器为0时,该对象可被回收。但主流的Java垃圾收集器并未采用此方法,因为它无法处理循环引用的问题。
  2. 可达性分析算法(也称为根搜索算法或追踪算法):这是当前大多数Java垃圾回收器采用的方法。从一系列被称为GC Roots的对象开始向下搜索,能够到达的对象被认为是存活的,不能到达的对象则被视为垃圾。GC Roots包括全局性的引用、栈中的局部变量引用等。
  3. 标记-清除(Mark-Sweep)算法:分为两个阶段,首先标记出所有活动对象,然后清除未被标记的对象。缺点是会产生大量不连续的内存碎片。
  4. 复制(Copying)算法:将内存分为两块区域,每次只使用其中一块,当这块内存用完后,就将存活的对象复制到另一块上,并清空已使用过的那块区域。这种方式可以避免内存碎片问题,新生代的“eden”区和“from/to” Survivor区之间就是通过这种算法进行垃圾回收。
  5. 标记-整理(Mark-Compact)算法:结合了标记和整理两个步骤,标记完成后,不是直接清理无用对象,而是将存活对象往一端移动,然后直接清理边界外的所有空间。这样既能回收内存,又能解决内存碎片问题。老年代的垃圾回收通常会采用这种或者类似的整理方式。
  6. 分代收集(Generational Collection)算法:根据对象的生命周期长短将内存划分为不同的区域(如新生代、老年代),不同区域采取不同的收集策略。新生代由于对象死亡率高,一般采用复制算法;而老年代对象存活率较高,更适合使用标记-清除或标记-整理算法。

(JDK 8及之前版本的主要垃圾回收器):

  1. Serial GC
    年轻代:Serial Collector是单线程的垃圾回收器,只使用一个CPU或一条GC线程进行垃圾回收工作,适合于客户端应用或者对响应时间要求不高、内存较小的环境。
    算法:在年轻代使用复制算法(Copying),分为 Eden 区和两个 Survivor 区,每次仅清理 Eden 和其中一个 Survivor 区。
  2. Parallel (Throughput) GC
    年轻代:并行收集器,使用多个线程并行执行垃圾回收任务,旨在最大化吞吐量,适用于多处理器且对停顿时间不敏感的应用。
    算法:同样在年轻代使用复制算法,并发标记-清除或并发标记-压缩算法可能应用于老年代。
  3. CMS (Concurrent Mark Sweep) GC
    老年代:并发标记清除垃圾回收器,主要用于老年代垃圾回收,特点是大部分工作与用户线程并发执行,减少了STW(Stop-The-World)的时间。
    • 算法:
      • 初始标记(Initial Mark):STW阶段,标记从根对象直接可达的对象。
      • 并发标记(Concurrent Mark):遍历整个堆,找出所有可到达的对象,与应用程序线程并发执行。
      • 重新标记(Remark):STW阶段,处理并发标记期间因应用程序继续运行而产生的变动。
      • 并发清除(Concurrent Sweep):清除未标记为存活的对象,与应用程序线程并发执行。
  • 初始标记、并发标记以及重新标记阶段利用了三色标记法来确定哪些对象是可达的(黑色),哪些对象需要被清理(白色)。

三色标记算法

  • 黑色:表示对象已被标记为可达,并且其引用的所有子对象也已经被递归地标记为黑色。这意味着从根对象出发能够直接或间接到达的所有对象都属于黑色。
  • 灰色:表示对象已被标记为可达,但其引用的某些子对象尚未被扫描和标记。当一个黑色对象引用到一个白色对象时,这个白色对象会变为灰色。
  • 白色:初始状态下所有对象均为白色,表示对象可能是不可达的。在标记阶段结束时,仍然白色的对象会被认为是不可达的,可以安全地回收。
  1. G1 (Garbage-First) GC
  • 堆分区:G1将整个堆划分为多个固定大小的区域,每个区域可以存储年轻代或老年代的对象。
  • Garbage-First 策略:
    “Garbage-First”策略体现在G1在选择回收哪些区域时,优先处理垃圾最多的区域。G1通过维护记忆集(Remembered Sets)记录跨Region引用信息,实时跟踪各个Region中的存活对象以及可回收的空间大小,从而确定出收益最大的回收集合(CSet:Collection Set)。
  • 并发标记:G1使用并发标记阶段减少STW(Stop-The-World)时间,该阶段包含初始标记、并发标记和最终标记子阶段。
  • 混合回收:在一次GC过程中同时处理新生代和部分老年代对象,避免了传统的新生代和老年代分离的回收方式。
  • 记忆集:用于跟踪跨Region区域引用以确定哪些区域需要扫描。
  • 压缩功能:虽然不是完整的连续空间整理,但G1通过在每次回收后局部地对存活对象进行重定位,降低了内存碎片化问题。
  • 算法:
    • 标记-压缩:G1同时实现了标记和整理功能,通过记录每个区域的存活数据来决定哪些区域优先进行回收(Garbage-First策略)。
    • 并发标记:包括初始标记、并发标记和最终标记阶段,类似于CMS的标记过程。
      • 初始标记:首先进行一次短暂的STW操作,仅标记出GC Roots能直接关联到的对象,这些对象及其引用的对象立即变为黑色或灰色。
      • 并发标记:在不暂停用户线程的前提下,G1垃圾回收器开始并发标记过程。从已知的黑色对象出发,遍历堆内存并标记可达对象,将灰色对象转换为黑色,同时找出新的灰色对象。
        由于并发执行,可能有新的对象分配和引用关系的变化,因此引入了“SATB”(Snapshot At The Beginning)算法来记录并发阶段中的跨代引用变化,确保不会漏掉可回收的对象。
      • 最终标记:这是一个更短的STW阶段,主要处理并发标记阶段结束后遗留下来的未完成的引用链。该阶段需要再次确认那些在并发阶段中因程序执行而改变的引用关系,以保证正确性。
      • 并发预清理和混合回收:在回收过程中,G1会尝试尽可能减少整体暂停时间,在并发清除的过程中,G1会根据之前标记的结果清理确定为不可达的白色区域,将其回收。
    • 压缩:每次收集之后会对收集到的Region内的存活对象进行重新布局(即压缩),避免了长时间运行后出现大量内存碎片的问题。

G1对比CMS

  • 分区域管理、逻辑分代、物理不分代
  • 区域回收压缩整理,避免内存碎片

新的垃圾回收器(JDK 9及之后版本新增)

  1. Z Garbage Collector (ZGC)
  • 低延迟:ZGC设计目标是实现极低的停顿时间(不超过10ms),支持大内存分配(高达数TB)。

  • 染色指针:ZGC使用一种称为颜色指针的技术,将对象的状态编码到对象引用本身,从而减少了全局数据结构的需求,并允许并发操作。

  • 读/写屏障:ZGC依赖于高效的读/写屏障来维护并发期间的数据一致性。

  • 并发标记与并发压缩:ZGC在大多数情况下不需要长时间的STW,其大部分工作与应用程序线程并发执行,包括标记和压缩阶段。

  • 元数据压缩:ZGC使用了压缩技术来管理对象头信息和其他元数据,以适应大规模内存环境。

  • Colored Pointers(染色指针): 在传统的64位系统中,由于地址空间较大,通常可以为对象分配足够的空间以存储额外的信息。ZGC利用了这一点,通过扩展对象引用(即指针)来存储额外的状态信息,这种设计被称为“染色指针”。具体来说,ZGC将原本只用来指向对象起始地址的指针划分为几个部分,一部分用于存储对象的实际地址,另一部分则用来存储诸如对象年龄、是否移动过等元数据。这样做的好处在于,在并发标记和压缩过程中,无需更改对象本身的内容就可以记录对象在内存中的状态变化,极大地简化了垃圾回收时对象迁移的过程,并且支持并发执行,减少STW停顿时间。

    • 指针扩展: 在64位系统中,由于地址空间非常大,实际分配给每个对象的内存地址可能只需要部分高位字节即可定位到精确位置。ZGC利用这一特性,将原本的指针分割成两部分:一部分存储对象的实际地址;另一部分用于记录与对象相关的元数据,比如对象是否移动过、对象所处的年龄阶段等。
    • 状态编码: 染色指针的“颜色”并不是真正意义上的颜色,而是通过二进制位来编码不同的状态信息。例如,可以设置特定的比特位来表示一个对象是否已经被垃圾收集器移动到堆内存的其他位置,或者用来跟踪对象的生命周期状态。
    • 并发压缩与重定位: 当进行垃圾回收时,ZGC需要对堆内存进行压缩以消除碎片并重新整理对象布局。传统的做法可能会涉及大量对象的重定位操作,这通常会带来较高的STW停顿时间。而使用染色指针后,ZGC可以在并发阶段直接修改染色指针指向新的对象地址,线程读取对象时通过读屏障检测并返回正确的对象新位置,从而避免了传统垃圾回收过程中因对象重定位导致的大规模数据更新问题。
    • 低延迟与高并发性: 通过染色指针技术,ZGC能够做到大部分垃圾回收工作在并发阶段完成,极大地减少了STW的时间。这是因为即使对象被移动,应用程序线程在访问对象时仍然可以通过读屏障获得最新的地址,而不必等待整个垃圾回收周期结束。
    • NUMA优化与负载均衡: ZGC还结合了NUMA-aware的设计,使得染色指针可以帮助管理跨多个CPU节点的内存分布,进一步提升性能和可扩展性。
  • Load Barrier(读屏障): 在并发垃圾收集期间,读屏障是一种确保内存一致性和正确性的重要机制。当线程从堆内存读取对象引用时,读屏障会介入并检查当前读取的对象指针是否满足特定条件(如:对象是否被移动过)。如果对象已经被移动,那么读屏障负责返回新地址,保证程序能访问到正确的对象位置,从而实现在并发阶段对对象移动的支持。
    在ZGC的具体实现中,Load Barrier在硬件层面得到了充分优化,能够尽可能降低开销。它使得在并发标记和整理阶段,即使对象在堆中发生迁移,应用程序仍然能够透明地访问到正确的位置,而不会引起长时间的STW暂停。

  1. Shenandoah GC
    低延迟:类似ZGC,也追求低延迟,但其独特之处在于它引入了“并发压缩”技术,在移动对象的同时允许应用程序继续运行。
    • 算法:
      • 复制算法:在新生代中广泛使用,将内存划分为两部分或三部分,只清理其中一部分,然后将存活对象复制到另一部分,从而避免碎片化问题。
      • 标记-清除算法:基础的垃圾回收算法,先标记出所有活动对象,然后清除未被标记的对象。会产生内存碎片。
      • 标记-整理算法:在标记之后,不是简单地清除无用对象,而是将所有存活的对象向一端移动,然后清理边界之外的所有空间,用于老年代回收以减少碎片。
      • 分代收集算法:根据对象生命周期的不同,将堆内存划分为新生代和老年代,不同区域采取不同的收集策略。

一个对象什么时候会进入新生代、什么时候会进入老年代

新生代

  • 当一个新的对象实例被创建时,默认情况下,它会被分配到新生代的Eden区。

老年代

  1. 年龄阈值晋升:新生代内部采用复制算法进行垃圾回收,每次Minor GC后,存活下来的对象会被移动到Survivor区(S0或S1),并在后续的Minor GC中继续在两个Survivor区之间移动。当一个对象在Survivor区经过一定次数(默认为15次,可配置)的Minor GC后仍存活,它将被晋升到老年代。
  2. 大对象直接进入老年代:对于较大的对象(大于特定阈值,可通过-XX:PretenureSizeThreshold设置),JVM可以决定直接将其分配到老年代,以避免在新生代之间频繁复制而导致效率降低。
  3. 动态年龄判定:如果Survivor空间不足容纳所有应被保留的对象,年龄满足最小晋升年龄的对象也会被晋升至老年代。
  • 新生代、老年代内存比例是1:2,新生代占用总堆内存的1/3,那么老年代大约占用总堆内存的2/3。新生代中Eden区占总新生代80%,Survivor区S0、S1各占10%
  • 例如 -XX:NewRatio 参数可以调整新生代与老年代之间的比例,而 -XX:SurvivorRatio 参数可以设置Eden区与单个Survivor区之间的比例。

什么时候会发生young gc,什么时候会发生old gc,什么时候会发生full gc

Minor GC (Young GC)

触发条件:

  • 当新生代(Young Generation)的空间不足时,即Eden区或任一Survivor区没有足够的空间来分配新的对象时,将会触发Minor GC。
  • 过程: Minor GC主要针对新生代区域,它的目的是回收新生代中的不再使用的对象,释放内存空间。存活的对象通过复制算法移动到另一个Survivor区或者晋升到老年代。

Major GC (Old GC)

触发条件:

  • 当老年代(Old Generation)的空间不足,需要对老年代进行垃圾回收以便腾出空间,这时会触发Major GC或Old GC。
  • 过程: Major GC主要针对老年代,清理的是这一部分内存区域中不再使用的对象。虽然Old GC通常不会直接影响新生代,但在某些垃圾收集器(如CMS或G1)中,Old GC操作可能伴随着对整个堆内存更为复杂的清理策略。

Full GC

触发条件:

  • 老年代空间不足,无法完成对象晋升。
  • System.gc()方法被显式调用(但通常不推荐这样做,因为它可能导致不必要的性能开销)。
  • 持久代(PermGen,在Java 8之后被元空间Metaspace取代)空间不足。
  • 垃圾收集器在并发模式失败后,需要进行一次全局的、停止世界(Stop-the-World)的垃圾回收。
  • CMS垃圾收集器在Concurrent Mode Failure之后,为了恢复系统的响应性而不得不执行的全堆范围的垃圾回收。
  • 过程: Full GC是对整个堆(包括新生代和老年代)以及方法区(对于Java 8之前的版本)或元空间(对于Java 8及更高版本)进行全面的垃圾回收。Full GC比Minor GC和Major GC更为耗时,因为它涉及到更多的内存区域,而且通常会导致应用程序长时间的暂停。
  • 综上所述,Minor GC是最常见的,也是频率最高的一种垃圾回收;Major GC的触发相对较少,主要与老年代的容量和对象晋升有关;而Full GC的发生则应当尽量减少,因为它往往会对应用性能造成较大影响。

如何看gc了几次

jstat -gcutil [进程ID] [采样间隔] [采样次数](查询java进程GC相关指标统计汇总)

  • YGC:从应用程序启动到采样时年轻代中gc次数
  • YGCT:Young Generation垃圾收集器总共消耗的时间。
  • FGCT:Full GC(或Major GC)总共消耗的时间。
  • GCT:总的GC消耗时间。

jstat -gc [进程ID] [采样间隔](动态观察gc情况)

  • stat -gc 4655 500 (隔500毫秒打印GC的情况)

查询gc日志

  • 设置JVM启动参数,保存gc日志
java -Xloggc:/path/to/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=<number_of_files> -XX:GCLogFileSize=100M <your_main_class>

-Xloggc:/path/to/gc.log:指定垃圾收集日志文件的位置。
-XX:+UseGCLogFileRotation:启用垃圾收集日志文件轮转功能。
-XX:NumberOfGCLogFiles=<number_of_files>:设置最多可以产生的GC日志文件数量。例如,如果您希望系统自动管理一定数量的日志文件,请将 <number_of_files> 替换为期望的文件个数。
-XX:GCLogFileSize=100M:设置每个日志文件的最大大小为100MB。
这样,当单个GC日志文件达到100MB时,JVM会自动创建新的日志文件继续记录垃圾收集信息。请根据实际需求调整 <number_of_files> 的值以避免日志文件过多或过少。

工具分析

  • 使用JDK自带java VisualVM、Jprofiler、Memory Analyzer Tool进行监控、分析GC日志
  • 阿里arthas 监控
  • https://gceasy.io 通用GC日志分析器

fullgc一次大概要多久? 看过实际情况吗?

  • 时间递增趋势,3s、5s、10s、30s
  • 随fullgc次数越多,时间越久。直至java进程卡死

线上做了哪些JVM调优

项目中遇到过OOM吗? (说出来排查步骤)

JVM常用命令

posted @ 2024-03-11 14:06  爪哇搬砖  阅读(13)  评论(0编辑  收藏  举报