全网最全JVM面试题(25秋招走起)

什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

Java 虚拟机是 Java 程序运行环境的一部分,负责执行 Java 字节码,它是 Java 实现平台无关性的基石。

Java 程序首先被编译成一种中间形式,即字节码(.class 文件),这些字节码不针对任何特定的硬件或操作系统。当运行 Java 程序时,JVM 在实际的硬件平台上解释执行这些字节码,实现了“一次编写,到处运行”(Write Once, Run Anywhere, WORA)的理念。因此,Java 程序无需为不同平台重新编译,只要对应平台上有相应的 JVM 实现,Java 程序就可以在上面运行。

这种设计有如下几个好处:

  1. 跨平台性:只需编写一次代码,就可以在多个平台上运行,无需针对每个操作系统写特定的代码。
  2. 安全性:JVM 提供了一个隔离的运行环境,可以在执行 Java 程序时对其进行检查和限制,减少安全风险。
  3. 移植性:由于 JVM 屏蔽了底层硬件和操作系统的差异,Java 程序更容易从一个平台迁移到另一个平台。

内存管理

说一下 JVM 的主要组成部分及其作用?

JVM 的主要组成部分如下:

  1. 类加载器(Class Loaders)
    • 负责加载 Java 类到 JVM 中。它在运行时将 Java 的 .class 文件加载到内存中,并为它们创建对应的 Class 对象。
    • 类加载器按照父级委派模型工作,首先会请求父类加载器加载类,只有在父类加载器加载失败时,才会尝试自己加载。
  1. 运行时数据区(Runtime Data Areas)
    • 方法区(Method Area):存储每个类的结构,如运行时常量池、字段和方法数据,方法和构造函数的代码,以及类型、方法和构造函数的特殊方法。
    • 堆(Heap):JVM 中内存最大的一块,用于存储所有类实例和数组。垃圾收集器主要的工作区域也在这里。
    • 栈(Stacks):存储局部变量和部分结果,并参与方法调用和返回。每个线程拥有自己的调用栈。
    • 程序计数器(PC Register):当前线程所执行的字节码的行号指示器。
    • 本地方法栈(Native Method Stacks):为 JVM 使用到的 Native 方法服务。
  1. 执行引擎(Execution Engine)
    • 负责执行类文件中的指令。它包括一个虚拟处理器、解释器、及即时编译器(JIT 编译器)。
    • 解释器:快速解释字节码,但执行同样代码的效率较低。
    • 即时编译器(JIT):编译热点代码(频繁执行的代码)为本地机器码,提高执行效率。
  1. 垃圾回收器(Garbage Collector)
    • 负责回收堆内存中不再被使用的对象。垃圾回收器的存在使得 Java 程序员不需要手动管理内存,减少了内存泄漏和指针失误的问题。
  1. 本地接口(Native Interface)
    • 提供了一个接口,用于交互 Java 代码和本地库,允许 Java 程序调用或被其他语言如 C/C++ 写的程序调用。
  1. 本地库(Native Libraries)
    • 由 JVM 使用的标准库,这些库通常用 C/C++ 编写,被本地接口调用。

32 位和 64 位的 JVM,int 类型变量的长度是多数?

在 Java 中,数据类型的大小不受 JVM 是 32 位还是 64 位的影响。这是 Java 语言的一部分规范,确保了 Java 程序的可移植性。因此,无论在 32 位还是 64 位的 JVM 上,int 类型的变量长度都是 32 位(4 字节)。这同样适用于其他基本数据类型,如 byte、short、long、float、double、char 和 boolean,它们的大小在不同的 JVM 架构中都是一致的。

请详细介绍下 JVM 运行时数据区

VM 运行时数据区域是 Java 虚拟机定义的内存区域,用于在 Java 程序运行期间存储数据。这些区域包括堆(Heap)、方法区(Method Area)、虚拟机栈(VM Stacks)、程序计数器(Program Counter Register)和本地方法栈(Native Method Stacks)。

  • 堆(Heap):JVM中最大的一块内存区域,它被所有Java线程共享。在堆中,主要存放了Java应用创建的对象实例和数组。因为这些对象的生命周期不总是确定的,所以堆也是垃圾收集器的主要工作区域,以确保释放那些不再被任何引用的对象所占用的内存。
  • 方法区(Method Area):同样是线程共享的内存区域,它用来存储每个类的结构信息,比如类的名称、直接父类的名称、方法和变量的信息以及编译后的代码等。在一些JVM实现中,方法区可能被称为"永久代"(PermGen),但从Java 8开始,已经被元空间(Metaspace)所取代。
  • **Java栈(Java Stack)**则是线程私有的,它的生命周期与线程相同。每个栈由多个栈帧组成,每个栈帧对应着一次Java方法调用。栈帧中包含了局部变量表、操作数栈、动态链接信息以及方法返回时的操作。
  • **程序计数器(Program Counter Register)**是每个线程私有的内存区域,它包含了当前线程所执行的字节码的行号指示器。如果执行的是Java方法,计数器记录的是字节码指令的地址;如果执行的是Native方法,计数器的值则是undefined。
  • **本地方法栈(Native Method Stack)**与Java栈相似,但它被用来支持Java中的Native方法(即用其他语言如C或C++编写的方法)。当线程调用一个Native方法时,它的调用状态会被压入本地方法栈。

说一下堆栈的区别?

堆(Heap)

  • 堆是由 JVM 运行时管理的内存区域,用于存放 Java 应用程序创建的对象和数组。
  • 堆内存是在所有线程之间共享的,对象无固定的存活周期,存活时间可能从应用程序开始到结束。
  • 内存的分配和回收是动态的,垃圾收集器负责回收那些不再被任何引用的对象,防止内存泄漏。
  • 堆的大小和生命周期是可以调节的,通常有一个固定的开始大小,但它会随着对象的创建和回收动态扩展或收缩,直到达到 JVM 配置的限制。

栈(Stack)

  • 栈是线程私有的内存区域,每个线程都有自己的栈,用于存储局部变量和部分结果,以及用于方法调用和返回。
  • 栈内存中的数据有明确的生命周期,遵循“先进后出”(LIFO)的原则。每个方法调用时会创建一个栈帧,方法结束时,对应的栈帧会被销毁。
  • 对于栈内存的分配和回收,操作更快速而且效率较高,因为它不需要复杂的垃圾收集算法,每个方法结束后,局部变量就会自动释放。
  • 栈的大小通常比堆小,且不可动态扩展,每个线程的栈空间在线程创建时被分配。

总的来说,堆是用于存储对象和数组的内存区域,是线程共享的,其对象的生命周期不固定,由垃圾收集器管理。而栈是线程私有的内存区域,用于基本类型的局部变量和对象引用,其数据随着方法调用而创建,方法结束而销毁。

对象创建的过程了解吗?

详细过程如下:

  1. 类加载检查
    • 当代码试图创建对象时,JVM 首先检查这个类的 Class 对象是否已经被加载、链接和初始化。如果没有,那么JVM会执行类加载过程。
  1. 内存分配
    • 一旦确定类已经被加载,JVM 接下来将在堆上为新对象分配内存。
    • 对象所需的内存大小在类加载完成后即已知,包括对象的所有实例变量和其他开销(如元数据信息、对齐填充等)。
    • 内存分配可以通过「指针碰撞」(如果内存是绝对连续的)或者「空闲列表」(如果内存是非连续的)方式来实现。
    • JVM 实现可能会采用垃圾回收算法中的分代收集算法,在这种情况下,新创建的对象通常会被放在堆的年轻代(Young Generation)。
  1. 初始化零值
    • 内存分配后,JVM 将分配的内存空间初始化为零值。这确保了对象的实例变量不会有未知的值。
  1. 设置对象头
    • JVM 会在对象的内存中设置对象头(包括类的元数据信息、哈希码、垃圾回收信息等)。
    • 对象头标识了对象的运行时类型和锁状态等信息。
  1. 执行 <init> 方法
    • 对象头设置完毕后,JVM 会调用构造函数。构造函数可能会调用父类的构造函数,并执行所有初始化块。
    • 构造函数中的代码将负责设置实例变量的初始值,这些值可能与默认零值不同。
  1. 引用分配
    • 完成对象的初始化后,JVM 将分配的内存地址赋值给引用变量,此时对象才算是完全创建好了。

整个对象创建过程是由 JVM 的类加载器、内存分配子系统、执行引擎等协作完成的。这个过程中,可能会触发垃圾收集活动,特别是在堆内存不足时。同时,虚拟机也可能对对象的内存分配和初始化过程进行优化,如通过逃逸分析判断对象是否可以在栈上分配,或者通过即时编译器优化对象的创建代码。

怎么获取 Java 程序使用的内存?堆使用的百分比?

  1. 使用命令行工具
    • jstat 命令可以用来查看堆内存以及其他各种运行时数据区的使用情况。
    • 使用 jstat -gc <pid> 可以看到年轻代、老年代以及永久代/元空间的当前使用情况。
  1. 使用 JConsole 或 VisualVM
    • 这些是 Java 提供的可视化工具,可以用来监控 Java 应用程序的内存使用、线程使用等运行时数据。
    • 它们可以显示堆内存的使用情况,包括初始大小、已用大小、提交大小和最大大小。
  1. 使用 Runtime 类
    • Runtime 类提供了与 Java 应用程序的运行时环境相关的方法。
    • Runtime.getRuntime().totalMemory() :获取 JVM 的总内存量。
    • Runtime.getRuntime().freeMemory() :获取 JVM 的空闲内存量。
    • Runtime.getRuntime().maxMemory() :获取 JVM 尝试使用的最大内存量。
    • 堆使用百分比可以通过计算 (totalMemory - freeMemory) / maxMemory 得到。
  1. 使用 MemoryMXBean
    • java.lang.management 包提供了管理和监视 Java 虚拟机的类和接口。
    • MemoryMXBean 可以获取 Java 虚拟机的内存状态信息。
    • 通过 ManagementFactory.getMemoryMXBean() 获取 MemoryMXBean 实例。然后可以使用 MemoryMXBean.getHeapMemoryUsage() 获取堆的内存使用情况。

什么是指针碰撞?

指针碰撞是一种简单高效的内存分配策略,常用于管理内存堆中的空闲空间,适合处理连续内存分配。

在使用指针碰撞的内存分配中,维护一个指针(通常称为 “分配指针”),指向堆内存中当前可用于分配的第一个位置。当需要分配内存时,只需要进行以下步骤:

  1. 检查足够的空间:首先检查从分配指针开始的内存块是否足够大以满足内存分配请求。
  2. 分配内存:如果有足够的空间,将内存从分配指针指向的位置开始分配给对象。
  3. 移动指针:分配后,将分配指针“碰撞”向前移动到新分配的内存块之后的位置。

指针碰撞内存分配策略非常快,因为它只是简单地移动一个指针,而不需要遍历和查找足够大小的空闲块。然而,它的一个主要缺点是可能会导致内存碎片,特别是在频繁的对象创建和销毁的情况下。为了解决这个问题,通常会结合使用垃圾收集器,特别是那些采用复制(Copying)或标记-整理(Mark-Compact)算法的收集器,来定期整理内存,减少碎片,从而为指针碰撞策略提供连续的空闲内存区域。

什么是空闲列表?

空闲列表是另一种内存管理方法,用于跟踪堆内存中的空闲空间。与指针碰撞方法不同,空闲列表方法适用于非连续的内存分配。

在使用空闲列表管理内存时,系统会维护一个列表,记录堆内存中所有未被分配的空间。这个列表包含了多个空闲块的大小和位置信息。当应用程序请求分配内存时,内存管理器会遍历空闲列表,寻找一个足够大的空闲块来满足这个请求。

工作流程如下:

  1. 查找合适的空闲块:当请求内存分配时,系统会检查空闲列表,查找一个足够大的空闲块。这个查找过程可能会根据不同的策略进行,如首次适应(First Fit)、最佳适应(Best Fit)或最差适应(Worst Fit)。
  2. 分配内存:一旦找到合适的空闲块,系统会从这个块中分配所需的内存给请求者。如果空闲块的大小刚好等于请求的大小,则整个块被分配。如果空闲块比请求的大小大,它会被分割,一部分分配给请求者,剩余的部分仍然保留在空闲列表中。
  3. 更新空闲列表:分配内存后,空闲列表需要更新以反映内存的新状态。如果一个空闲块被完全使用,它将从空闲列表中移除。如果一个空闲块被分割,列表将更新为只包含剩余部分的信息。

空闲列表方法可以减少内存碎片问题,因为它可以更精细地管理内存分配,但这种精细度带来的代价是更高的管理成本,因为系统必须维护更复杂的数据结构,并且在每次内存分配时进行更多的计算。此外,频繁的分配和回收可能会导致列表变得很长,增加查找合适空闲块的时间。为了优化性能,可以使用合并邻近的空闲块的技术来减少列表的长度和提高内存的利用率。

JVM 里 new 对象时,堆会发生抢占吗?JVM 是怎么设计来保证线程安全的?

会。假设 JVM 虚拟机上,每一次 new 对象时,指针就会向右移动一个对象 size 的距离,一个线程正在给 A 对象分配内存,指针还没有来的及修改,另一个为 B 对象分配内存的线程,又引用了这个指针来分配内存,这就发生了抢占。

有两种可选方案来解决这个问题:

  • 采用 CAS 分配重试的方式来保证更新操作的原子性。这意味着当一个线程在执行一个原子操作分配内存时,没有其他线程可以同时执行另一个会干扰它的操作。
  • 线程本地分配缓冲(TLAB)
    • JVM 中的每个线程可以拥有自己的小块私有的内存缓冲区,称为线程本地分配缓冲(TLAB)。
    • 每个线程在自己的 TLAB 上分配内存,这样它们就不会与其他线程冲突,从而减少了同步的需要。
    • 当一个线程的 TLAB 用尽时,它需要获取新的 TLAB,这个过程可能需要同步,但这种情况发生得比较少。

内存溢出和内存泄漏是什么意思

内存溢出:

  • 内存溢出发生时,表示 Java 虚拟机(JVM)中的堆内存不足,无法再分配新的对象。
  • 这种情况通常发生在应用程序尝试创建对象,但是堆内存已经满了,而且垃圾收集器无法释放任何更多的内存空间。
  • 内存溢出可能是由于内存泄漏,或者是因为 JVM 的堆内存设置不足以应对应用程序的需求。
  • 当内存溢出发生时,JVM 会抛出 OutOfMemoryError 异常。

内存泄漏:

  • 内存泄漏是指已分配的内存资源未能被释放,而应用程序不再使用这些内存资源。
  • 在 Java 中,内存泄漏通常是由于长时间生存的对象持有对不再需要的对象的引用,阻止垃圾收集器回收它们,导致无法回收的对象逐渐积累。
  • 随着时间的推移,内存泄漏会导致可用内存逐渐减少,最终可能导致内存溢出。
  • 内存泄漏的诊断通常需要借助分析工具(如MAT)来检查哪些对象被错误地保留在内存中。

总结来说,内存溢出是指没有足够的内存空间来分配新的对象,通常是一种严重的错误,会导致程序崩溃。而内存泄漏是一个渐进的过程,未使用的对象逐渐积累,消耗掉所有可用内存,可能最终导致内存溢出。

内存泄漏可能由哪些原因导致呢?

内存泄露主要是由于对象不再需要,但是垃圾收集器无法进行回收,因为仍然存在对这些对象的引用。

长生命周期对象持有短生命周期对象的引用

  • 如果一个具有长生命周期的对象引用了应该是短生命周期的对象,那么这些短生命周期的对象不会被回收。

集合类中的对象没有被及时清理

  • 在使用集合类(如 List、Map、Set 等)时,即使对象已经不再需要,如果没有从集合中移除,它们就会一直占用内存。

静态字段

  • 静态字段的生命周期与类的生命周期相同,如果静态字段引用了某个对象,那么这个对象可能在整个应用程序的生命周期内都不会被回收。

内部类和匿名类持有外部类的引用

  • 非静态内部类和匿名类隐式持有对其外部类实例的引用,如果内部类的实例比外部类活得更久,那么外部类实例也不会被回收。

缓存对象

  • 未正确管理的缓存可能导致已缓存的对象长时间占用内存,尤其是在使用强引用缓存时。

不当的资源管理

  • 如果资源(如数据库连接、网络连接等)没有被正确关闭,可能导致内存泄漏。

ThreadLocal变量滥用

  • ThreadLocal 变量存储在线程的内存中,如果线程一直运行(例如,在线程池中),那么这些变量可能不会被清理。

如何判断对象仍然存活?

  1. 引用计数法:这是最简单的一种方法,但在 Java 的主流 JVM 实现中并未使用。每个对象有一个引用计数器,当有一个地方引用它时,计数器值加一;当引用失效时,计数器值减一。计数器为零时,对象可以被回收。这种方法的主要问题是它无法处理循环引用的情况。
  2. 可达性分析(Reachability Analysis):这是 Java 中最常用的方法。在这种方法中,一系列称为“GC Roots”的对象被认为是活跃的。从这些节点开始,JVM 通过引用关系遍历,能够连续到达的对象被认为是活跃的,否则被认为是可回收的。常见的 GC Roots 包括:
    • 活跃线程
    • 静态字段(类的静态属性)
    • 本地方法栈内的局部变量

怎么判断对象是否可以被回收?

判断一个对象是否可以被回收主要依赖于可达性分析(Reachability Analysis)。下面是判断一个对象是否可以被回收的主要步骤和准则:

  1. GC Roots 连接:在可达性分析中,首先确定一组称为 GC Roots 的对象。这些对象是垃圾收集的起点。常见的 GC Roots 包括:
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中 JNI(即通常说的 Native 方法)引用的对象。
  1. 可达性分析:从 GC Roots 开始,JVM 检查所有通过引用链能够到达的对象。如果一个对象可以从 GC Roots 出发通过引用链到达,它被认为是可达的(活跃的),因此不应该被回收。
  2. 不可达对象:如果对象无法从任何 GC Roots 到达,那么它被认为是不可达的,可以被回收。这些对象包括:
    • 完全没有被引用的对象。
    • 仅由不可达的对象引用的对象(即它们不是由任何活跃对象或 GC Roots 直接或间接引用的)。
  1. 特殊情况 - 弱引用、软引用和虚引用:在 Java 中,除了强引用外,还有弱引用(WeakReference)、软引用(SoftReference)和虚引用(PhantomReference)。这些引用类型在垃圾收集时有特殊的处理规则。例如,软引用在内存不足时会被回收,而弱引用则无论内存状况如何总是会被回收。
  2. 回收判定后的处理:一旦对象被判定为不可达,它就可以被垃圾收集器回收。在这个阶段,如果对象类定义了 finalize 方法,且该方法未被调用过,JVM 会将这些对象放入一个队列(Finalizer队列),等待执行其 finalize 方法,这是对象的最后一次机会改变自己的“死亡”命运。如果在 finalize 方法执行后,对象仍然不可达,则会在下一次垃圾回收中被清理。

Java 中可作为 GC Roots 的对象有哪几种?

  1. 本地方法栈中的引用: 这些是从本地方法(即用非 Java 语言编写的方法,如 C 或 C++)引用的对象。
  2. Java 虚拟机栈中的引用: 这包括 Java 方法的局部变量。每个线程的方法调用都有自己的栈帧,其中包含局部变量表,这些局部变量可能引用其他对象。
  3. 方法区中的类静态属性引用的对象: 方法区(Method Area)存储了每个类的结构,如运行时常量池、字段、方法数据等。类的静态变量也存储在这里,它们可能引用其他对象。
  4. 方法区中常量引用的对象: 这指的是方法区中的常量池中的引用,常量池主要存储编译时期生成的各种字面量和符号引用。
  5. 同步锁持有的对象: 在同步代码块或方法中作为锁定对象的引用。

Java 中都有哪些引用类型?

Java 中的引用类型主要分为四种:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。

  1. 强引用(Strong Reference):
    • 最常见的引用类型,当我们通常创建一个对象并赋值给一个引用变量时,这就是强引用。例如:Object obj = new Object();
    • 只要某个对象有强引用,垃圾回收器就不会回收它。即使程序可能面临内存溢出的风险,只要对象还被强引用指向,它就不会被回收。
  1. 软引用(Soft Reference):
    • 软引用是用来描述一些有用但并非必需的对象。在Java中通过SoftReference类实现。
    • 软引用的对象在内存足够的情况下不会被回收,只有在内存不足时,JVM才会回收这些对象,从而有效地避免了内存溢出。
    • 适合用于缓存。
  1. 弱引用(Weak Reference):
    • 比软引用的生存期更短。通过WeakReference类实现。
    • 当垃圾回收器工作时,不管内存是否足够,都会回收只被弱引用指向的对象。
    • 弱引用可以用于实现规范映射(Canonical Maps)等,例如WeakHashMap。
  1. 虚引用(Phantom Reference):
    • 最弱的一种引用类型。
    • 通过PhantomReference类实现,并且必须和ReferenceQueue一起使用。
    • 虚引用的对象在被垃圾回收时,会收到一个系统通知,这通常用于执行一些重要的清理工作。

finalize()方法了解吗?有什么作用?

finalize()主要用于在对象被垃圾回收器销毁之前执行清理操作。它是 Object 类的一个方法,因此每个类都可以覆盖finalize() 方法来实现自定义的清理逻辑。其作用有如下几个:

  • 资源释放:finalize() 方法通常用于释放对象持有的资源,如关闭文件句柄、网络连接或者释放自定义的内存资源等。这在 Java 早期版本中是一种常见的模式。
  • 最终机会清理:在对象被垃圾回收之前,finalize() 提供了一个最后的机会来执行清理操作。这意味着你可以在对象不再被使用时执行一些重要的清理工作。

但是,由于 finalize() 的调用时机完全取决于垃圾回收器,导致它不可预测性和性能问题,所以已经不再被推荐使用。且在 Java 9 及以后的版本中,finalize() 方法被标记为废弃(deprecated)。

Java 堆的内存分区了解吗?

  • 年轻代(Young Generation):
    • 这是新创建的对象首先分配内存的地方。年轻代内存分为几个部分:
      • Eden 空间:大部分新生成的对象首先在 Eden 空间分配。
      • 两个幸存者空间(Survivor Spaces):分为 S0(Survivor 1)和 S1(Survivor 2)。存活下来的对象从 Eden 空间转移到一个幸存者空间,然后在幸存者空间之间来回移动。
    • 年轻代的特点是对象的分配和回收都相对较快。
  • 老年代(Old Generation 或 Tenured Generation):
    • 存放长时间存活的对象。
    • 当对象在年轻代中存活足够长的时间后,它们就会被转移到老年代。
    • 老年代的垃圾回收(GC)频率比年轻代低,但每次回收的时间更长。
  • 永久代/元空间(PermGen/Metaspace,取决于 JVM 版本):
    • 在 JDK 8 之前,JVM 使用的是永久代(PermGen)。永久代主要用于存储类元数据、方法对象等。
    • 由于永久代经常出现内存溢出的问题,JDK 8 引入了元空间(Metaspace)来替代永久代。元空间不在虚拟机内存中而是使用本地内存,因此其大小受本地内存限制。

请详细介绍下新生代

新生代专门用于存放新创建的对象。由于大多数对象生命周期都很短,JVM 通过在新生代中频繁进行垃圾回收来有效管理内存。

新生代的内存结构

新生代主要分为三个部分:

  • Eden 空间:新创建的对象通常首先在 Eden 空间分配内存。当 Eden 空间填满时,触发一次 Minor GC(也叫 Young GC),即新生代的垃圾回收。
  • 两个幸存者空间:这两个空间分别被称为 S0(Survivor 1)和 S1(Survivor 2)。在进行 Minor GC 时,存活的对象会从 Eden 空间移动到一个幸存者空间(如 S0),而从一个幸存者空间(如 S0)存活下来的对象则会移动到另一个幸存者空间(如 S1)。这种来回移动的过程有助于筛选出那些更有可能长期存活的对象。

Eden 和两个幸存者空间的大小比例默认是 8:1:1,可以通过 -XX:SurvivorRatio 来调整。

新生代的垃圾回收(Minor GC)

  • 当 Eden 区域满了,就会触发一次 Minor GC。
  • 在 Minor GC 过程中,存活的对象会从 Eden 移动到一个幸存者空间,同时,从一个幸存者空间存活下来的对象会移动到另一个幸存者空间。
  • 经过多次 GC 后,仍然存活的对象会被认为是长期存活对象,它们将被移动到老年代(Old Generation)。

请详细介绍下老年代

老年代主要用于存储长期存活的对象。

老年代的特点

  • 长期存活对象
    • 当对象在新生代(Young Generation)的多次垃圾回收(GC)后仍然存活,它们通常会被移动到老年代。
    • 这些对象被视为长期存活对象,例如缓存数据或持久化的状态信息。
  • 大容量
    • 老年代的大小通常比新生代大得多,占据了堆内存的大部分。它的容量决定了可以长期存储的对象数量。
  • 垃圾回收频率
    • 老年代的垃圾回收频率较低,但每次回收的耗时通常比新生代长。

老年代的垃圾回收

Major GC / Full GC

  • 当进行老年代的垃圾回收时,通常称为 Major GC 或 Full GC。这种回收可能会涉及整个堆内存的回收,包括新生代和老年代。
  • Major GC 的执行时间通常比新生代的 Minor GC 长得多,可能导致明显的停顿。

知道内存分配策略吗?

  • 对象优先在 Eden 分配
    • 在大多数情况下,对象首先在新生代的 Eden 区域分配。当 Eden 区域填满时,会触发 Minor GC(即新生代垃圾回收)。
  • 大对象直接进入老年代
    • 大对象(如大数组或大对象)可能直接在老年代分配内存,以避免在 Eden 区和两个 Survivor 区之间来回复制,减少垃圾回收时的开销。
  • 长期存活的对象将进入老年代
    • 对象在新生代中每经历一次 Minor GC,其年龄就会增加1。当对象的年龄增加到一定程度(默认为 15,可通过 -XX:MaxTenuringThreshold 设置),它们就会被移动到老年代。
    • 这个机制减少了在新生代和老年代之间不必要的对象复制。
  • 动态对象年龄判定
    • 为了更有效地利用内存,JVM 会动态地调整对象晋升到老年代的年龄阈值。
    • 如果在 Survivor 空间中相同年龄所有对象大小的总和超过 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到 -XX:MaxTenuringThreshold 设置的年龄。
  • 空间分配担保
    • 在发生 Minor GC 之前,JVM会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果这个条件满足,那么 Minor GC 是安全的。如果不满足,VM 会查看 -XX:HandlePromotionFailure 设置,决定是否进行一次 Full GC。

为什么 Java 8 使用元空间替代永久代作为方法区的实现?

有如下几个原因:

  • 内存限制问题
    • 永久代内存限制:在旧版本的 JVM 中,永久代的大小是固定的,且在 JVM 启动时就被确定。这意味着存储在永久代中的类元数据的总量受到限制,容易出现 OutOfMemoryError。
    • 元空间更灵活:元空间使用本地内存,而不是虚拟机内存,因此不受 Java 堆大小的限制。这使得元空间可以动态调整大小,减少了内存溢出的风险。
  • 垃圾回收的简化
    • 永久代的回收复杂性:永久代的垃圾回收相对复杂,特别是类和类加载器的卸载机制。
    • 元空间的垃圾回收:将类元数据移到本地内存中的元空间简化了垃圾回收过程,因为元空间主要涉及类元数据的回收,而这通常是在类加载器的生命周期结束时发生。

对象什么时候会进入老年代?

年龄阈值

  • 每个对象在新生代中有一个年龄计数器。当对象在新生代的 Eden 区或 Survivor 区存活下来后,每经历一次 Minor GC,它的年龄就会增加。
  • 当对象的年龄达到一定阈值(默认值通常为 15,但可以通过 JVM 参数 -XX:MaxTenuringThreshold 调整)时,它会被晋升到老年代。
  • 这个机制基于这样一个假设:如果一个对象已经存活了很多次垃圾回收,那么它可能会继续存活更长的时间。

动态年龄判定

  • JVM 采用动态年龄判定机制来决定对象晋升到老年代的时机。
  • 如果在 Survivor 空间中相同年龄的所有对象的大小总和超过 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到最大年龄阈值。

大对象直接进入老年代

  • 大对象(即所需内存空间较大的对象)可能会直接在老年代分配,以避免在新生代中占用大量空间并在 Minor GC 时导致大量的数据复制。
  • 这个行为可以通过 JVM 参数(如 -XX:PretenureSizeThreshold)控制。当对象的大小超过这个设置值时,会直接在老年代分配。

空间分配担保

  • 在进行 Minor GC 前,JVM 会检查老年代的最大可用连续空间是否大于新生代所有对象总空间。
  • 如果不满足条件,JVM 会调整对象的晋升年龄,让一些对象提前进入老年代,以确保 Minor GC 能顺利进行。

什么是 Stop The World ? 什么是 OopMap ?什么是安全点?

Stop-The-World(STW)

  • 定义:Stop-The-World 指的是在垃圾回收过程中,JVM 会暂停应用程序的所有线程。在这段时间里,除了垃圾回收线程之外,所有线程都会被暂停,不进行任何工作。
  • 目的:STW 事件发生是为了防止应用程序在垃圾回收过程中继续修改对象,这样可以保证垃圾回收的准确性和安全性。
  • 影响:STW 会影响应用程序的响应时间和吞吐量。因此,选择垃圾回收器时,需要考虑其 STW 的时间和频率,以适应不同的应用需求。

OopMap(Ordinary Object Pointer Map)

  • 定义:OopMap 是一种数据结构,它用于记录对象内部哪些部分是指向其他对象的引用(即 Ordinary Object Pointers)。
  • 用途:在垃圾回收过程中,JVM 需要知道堆栈和寄存器中哪些位置存储了指向堆上对象的引用。OopMap 提供了这些信息,使得垃圾回收器可以正确识别这些引用,以更新它们(如果对象被移动了)或者判断对象是否可达。
  • 效率:OopMap 提高了垃圾回收的效率,因为垃圾回收器不需要扫描整个对象来查找引用。

安全点(Safepoint)

  • 定义:安全点是程序执行中的特定位置,在这些位置上,JVM 可以安全地进行垃圾回收。
  • 特征:在安全点,所有引用的位置都是已知的(例如,在方法调用、循环跳转、异常抛出等位置)。这意味着 JVM 可以确保在这些点上的对象引用信息是准确和一致的。
  • STW 和安全点:Stop-The-World 事件通常在安全点发生,因为这是执行垃圾回收操作的安全时机。
  • 线程同步:为了达到安全点,JVM 会通知所有线程,在它们达到最近的安全点时暂停执行。

对象一定分配在堆中吗?有没有了解逃逸分析技术?

在 Java 中,传统上对象是分配在堆(Heap)内存中的。然而,并非所有对象都必须分配在堆上。Java 虚拟机(JVM)的一个优化技术,称为逃逸分析(Escape Analysis),可以改变这种分配方式。

逃逸分析

逃逸分析是一种编译器优化技术,用于分析对象的作用域和生命周期:

  1. 定义:逃逸分析确定对象的使用范围和生命周期是否超出了其定义的作用域。
  2. 不逃逸的对象:如果一个对象在方法内创建,并且其引用不会逃逸到方法之外,那么这个对象被认为是“不逃逸”的。
  3. 栈分配:对于不逃逸的对象,JVM 可以选择在栈上分配这些对象,而不是在堆上。栈上分配的好处是,一旦方法执行完毕,这些对象的内存就可以立即被回收,从而减少垃圾回收的负担。
  4. 同步省略:逃逸分析还可以用于同步省略(也称为锁消除)。如果确定某个对象只能被一个线程访问,那么对这个对象的同步操作可以被省略。
  5. 标量替换:逃逸分析还可以进行标量替换,将一个聚合对象拆分成几个独立的局部变量,进一步优化性能。

逃逸分析通常是默认启用的。但是,你也可以通过 JVM 参数显式地开启或关闭它:

  • 启用逃逸分析:-XX:+DoEscapeAnalysis
  • 禁用逃逸分析:-XX:-DoEscapeAnalysis

垃圾回收

GC是什么?为什么要GC

GC,即Garbage Collection,垃圾回收,用于自动管理内存。它的主要目的是识别和回收程序不再使用的内存,即“垃圾”,以避免内存泄漏和优化内存使用效率。

为什么需要垃圾回收

  1. 自动内存管理:在 Java 这样的高级语言中,程序员不需要显式地分配和释放内存,这是由 JVM 的垃圾回收器自动完成的。这大大简化了编程,减少了内存泄漏和其他内存相关错误的风险。
  2. 提高效率:GC 自动回收不再使用的对象,释放内存空间供新对象使用,从而提高了内存的使用效率。
  3. 程序稳定性:通过及时清理无用对象,GC 有助于维护应用程序的性能稳定性,避免因内存耗尽导致的程序崩溃。
  4. 提高安全性:自动垃圾回收减少了手动内存管理可能带来的安全问题,如悬空指针和重复释放等。

有什么办法主动通知虚拟机进行垃圾回收?

有两种方式可以通知 JVM 进行垃圾回收。

  1. System.gc()** 方法**: 这个方法是最常用的请求垃圾回收的方式。当你调用 System.gc() 时,你实际上是在建议虚拟机执行垃圾回收。虚拟机可能会考虑这个建议,并在合适的时候执行垃圾回收。但是,这种方法并不保证虚拟机一定会立即执行垃圾回收,也不保证回收操作的效果。
  2. Runtime.getRuntime().gc()** 方法**: 这个方法的作用与 System.gc() 类似,也是向虚拟机建议执行垃圾回收。它通过获取当前运行时环境的实例来请求垃圾回收。

一般来说我们要谨慎使用这两种方式,以避免不必要的性能损耗。

在Java中,对象什么时候可以被垃圾回收

在 Java 中,对象变成垃圾回收的候选对象,即可被垃圾回收器回收的时机,主要取决于它们的可达性(Reachability)。以下几种情况可以判定为垃圾回收的候选对象:

  • 无引用: 当对象不再有任何活动引用指向它时,它就可以被垃圾回收。这意味着对象在程序中不再可达,比如所有引用该对象的变量都已经超出了其作用域或被设置为 null。
  • 仅有循环引用: 如果一组对象之间相互引用,但这组对象整体上不再被其他活动部分的程序所引用,那么这组对象也可以被垃圾回收。Java 的垃圾回收器能够识别并处理循环引用。
  • 对象所属的类已经被卸载: 如果一个对象的类的 ClassLoader 被垃圾回收,那么这个类的所有实例也可以被回收。这种情况在一些复杂的应用中可能会发生,尤其是在使用了自定义 ClassLoader 的情况下。
  • 终结器(Finalizer)的作用: 如果一个对象覆盖了 finalize() 方法,并且垃圾回收器发现了它,那么在对象被回收前,finalize() 方法会被调用。但这不是判断对象可以被回收的条件,而是在对象回收前的一种机制。因为 finalize() 方法的不确定性和性能问题,一般不推荐使用。

但是,即使对象变成了垃圾回收的候选对象,具体何时被回收仍然是不确定的。垃圾回收的具体时机取决于垃圾回收器的算法和内存使用情况。

JVM中的永久代中会发生垃圾回收吗

从 Java 8 开始,永久代已被元空间(Metaspace)所替代。但是,在 Java 8 之前的版本中,永久代是存在的,并且在永久代中也会发生垃圾回收。

Java 8 之前的版本(使用永久代)

在永久代中,确实会发生垃圾回收,但这种回收发生的频率和情况与堆内存中的垃圾回收有所不同。当类被卸载(这通常发生在使用了自定义类加载器并且该类加载器被回收的情况下)时,该类及其相关的元数据就会从永久代中移除。

在 Java 8 及以后的版本(使用元空间)

在元空间中,类的元数据仍然可以被回收。当类不再被使用时(例如,当类的加载器不再存在时),这些类及其元数据会被垃圾回收器回收。

说一下 JVM 有哪些垃圾回收算法?

标记-清除(Mark-Sweep)算法

此算法分为两个阶段:标记和清除。在标记阶段,算法标记出所有从根集合可达的对象。在清除阶段,未被标记的对象被认为是垃圾,并被清除。

但是,它可能产生大量内存碎片,导致后续可能无法为大对象找到足够的连续内存空间。

复制(Copying)算法

它将可用内存分为两块。每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后清理掉当前使用的这一块内存。

没有内存碎片。但它会使内存使用率低,因为任何时候只有一半的内存是被使用的。

标记-整理(Mark-Compact)算法

此算法在标记阶段和标记-清除算法相似。在整理阶段,它将所有存活的对象压缩到内存的一端,然后清理掉边界以外的内存。

它解决了内存碎片问题。但是整理过程中需要移动对象,可能会有较高的开销。

分代(Generational)收集算法

不同年龄的对象的生命周期不同。通常将内存分为年轻代(Young Generation)和老年代(Old Generation),分别应用不同的回收策略。

通过优化针对不同年龄段的对象的回收策略,可以在提高效率的同时减少回收引起的停顿时间。

说一下 JVM 有哪些垃圾回收器?

  1. Serial GC:
    • 类型: 单线程回收器。
    • 适用场景: 适用于小型应用和单核处理器环境。
    • 特点: 它在进行垃圾回收时会暂停所有应用线程(Stop-The-World)。
  1. Parallel GC (也称为 Throughput Collector):
    • 类型: 多线程回收器。
    • 适用场景: 适用于多核处理器环境,注重吞吐量。
    • 特点: 在年轻代使用并行方式执行,但老年代的垃圾回收仍然会触发全局暂停。
  1. Concurrent Mark Sweep (CMS) GC:
    • 类型: 并发回收器。
    • 适用场景: 适用于需要更短垃圾回收停顿时间的应用。
    • 特点: 它旨在减少应用停顿时间,通过并发标记和清除实现。
  1. G1 GC (Garbage-First Collector):
    • 类型: 并发和并行相结合的回收器。
    • 适用场景: 适用于大型堆内存和需要更精细控制垃圾回收停顿时间的应用。
    • 特点: 将堆划分为多个区域(Region),并按区域进行回收,目标是提供高吞吐量同时控制停顿时间。
  1. ZGC (Z Garbage Collector):
    • 类型: 可伸缩的低延迟垃圾回收器。
    • 适用场景: 适用于多核处理器和大堆内存的场景。
    • 特点: 旨在实现低停顿时间,同时支持大到几个 TB 的堆内存。
  1. Shenandoah GC:
    • 类型: 并发的低停顿时间回收器。
    • 适用场景: 适用于需要低延迟且堆内存较大的应用。
    • 特点: 类似于 ZGC,它的目标是减少垃圾回收导致的停顿时间,即使在处理大堆内存时也能保持低延迟。

详细介绍一下 CMS 垃圾回收器?

CMS 是专门用于回收老年代(Old Generation)的垃圾回收器。它的主要目标是获取最小的垃圾收集停顿时间,因此非常适合那些对响应时间有严格要求的应用。

工作流程

CMS 的工作过程可以分为几个主要阶段:

  1. 初始标记(Initial Mark):这个阶段会标记所有直接与 GC Root 相连的对象。这个阶段需要停止所有的应用线程(STW,Stop-The-World),但通常这个阶段很快。
  2. 并发标记(Concurrent Mark):在这个阶段,JVM 标记所有从 GC Root 直接或间接可达的对象。这个过程是并发进行的,即在应用程序运行的同时进行,不需要停止应用线程。
  3. 重新标记(Remark):因为在并发标记阶段,应用程序还在运行,可能会有一些对象的引用关系发生变化。重新标记阶段用来修正这些变化。这个阶段通常需要短暂的 STW。
  4. 并发清除(Concurrent Sweep):清除那些不再被引用的对象,回收内存空间。这个过程也是并发进行的。

CMS 的特点

  • 低停顿:CMS 设计的主要目标是减少垃圾收集造成的停顿时间。通过并发标记和清除,它能在应用程序运行时执行大部分工作,从而减少STW的时间。
  • 并发处理:能够与应用线程同时运行,减少了垃圾回收对应用响应时间的影响。
  • 老年代专用:CMS 只处理 JVM 中的老年代,新生代仍然使用其他垃圾回收器(如:ParNew或Serial)处理。

CMS 的缺点

  • 并发执行的代价:虽然减少了停顿时间,但并发执行会占用一部分CPU资源,可能导致应用程序的吞吐量有所下降。
  • 空间碎片化:由于 CMS 是基于标记-清除算法,它清除垃圾后可能会留下许多小的空闲内存块,导致空间碎片化。这可能需要定期的全面垃圾收集来整理这些碎片。
  • 对 CPU 资源敏感:由于其并发特性,CMS 对 CPU 资源比较敏感。在 CPU 资源受限的环境下,CMS 的性能可能会受到影响。

新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?

新生代垃圾回收器

新生代垃圾回收器主要负责管理 JVM 堆内存中的新生代区域。由于新生代中的对象生命周期短,这些回收器通常采用复制算法。常见的新生代垃圾回收器有:

  1. Serial GC:这是最基本的垃圾回收器,使用单线程进行垃圾回收。在执行垃圾回收时,会触发停顿(Stop-The-World),停止所有应用线程。适用于小型应用或者单核处理器环境。
  2. ParNew GC:是 Serial GC 的多线程版本。它在多核处理器上表现更好,但在进行垃圾回收时同样会触发停顿。
  3. Parallel GC(也称为 Throughput Collector):使用多线程进行垃圾回收,旨在提高吞吐量。在垃圾回收时也会发生停顿。

老年代垃圾回收器

老年代垃圾回收器负责管理 JVM 堆内存中的老年代区域。老年代中的对象通常有更长的生命周期,因此这些回收器通常采用标记-清除或标记-整理算法。常见的老年代垃圾回收器包括:

  1. Serial Old GC:是 Serial GC 的老年代版本,同样是单线程且在垃圾回收时会触发停顿。
  2. Parallel Old GC:是 Parallel GC 的老年代版本,使用多线程进行垃圾回收,目标是提高吞吐量。
  3. CMS (Concurrent Mark-Sweep):以最小化停顿时间为目标,通过并发标记和清除阶段减少应用暂停时间,但可能会产生较多的空间碎片。
  4. G1 (Garbage-First):是一种跨越新生代和老年代的垃圾回收器,它将堆分割成多个区域,并优先回收垃圾最多的区域。G1 GC 旨在提供更可预测的停顿时间,适用于大型堆内存。

什么时候会触发FullGC

Full GC 通常比新生代的垃圾回收更耗时,因为它会涉及到整个堆内存的回收活动,包括新生代、老年代以及永久代(或者在 Java 8 及以后版本的元空间 Metaspace)。以下是触发 Full GC 的一些常见情况:

  1. 老年代空间不足:当老年代空间不足以为新晋升的对象提供空间时,JVM 会触发 Full GC 来清理老年代并回收更多空间。
  2. 新生代晋升到老年代的空间不足:如果在新生代的垃圾回收后,存活的对象空间超过了老年代能提供的空间,JVM 也会触发 Full GC。
  3. 永久代(PermGen)或元空间(Metaspace)满:当类的元数据占用的空间超过了永久代或元空间的限制时,JVM 会触发 Full GC。在 Java 8 及以后版本中,元空间取代了永久代。
  4. System.gc() 调用:当应用程序显式调用 System.gc() 时,JVM 通常会执行 Full GC。然而,这个行为并不是完全保证的,因为垃圾回收是由 JVM 控制的。
  5. JVM 内部的垃圾回收算法决定:JVM 的垃圾回收算法可能决定执行 Full GC,例如在某些情况下进行内存压缩或在低内存情况下的资源回收。
  6. RMI 的垃圾回收:在使用 RMI 时,JVM 默认每分钟进行一次 Full GC,以回收 RMI 收集的对象。

JVM 的永久代中会发生垃圾回收么

JVM中的永久代是一个专门用于存储类的元数据、静态变量等数据的内存区域。在 Java 8 之前的版本中,永久代是非堆内存的一部分。永久代会发生垃圾回收,但是它回收的频率和方式与堆内存中的回收有所不同:

  1. 垃圾回收的触发时机:在旧版本的 JVM 中,当永久代接近其最大容量时,垃圾回收器会被触发以清理不再使用的类定义和元数据。这通常发生在大量类被加载和卸载的场景中,例如在某些应用服务器上或者使用动态代理生成大量类的情况下。
  2. 垃圾回收的方式:永久代的垃圾回收通常与堆内存的全局回收(Full GC)一起发生。当执行 Full GC 时,垃圾回收器不仅清理堆内存中的对象,也会检查并清理永久代中不再需要的类定义。
  3. Java 8 的变化:值得注意的是,在 Java 8 中,永久代被移除,取而代之的是元空间(Metaspace)。元空间不在虚拟机内存中而是使用本地内存,它主要用于存储类的元数据。元空间的引入改变了类元数据的存储方式和垃圾回收策略,使其更加灵活和高效。
  4. 垃圾回收的挑战:在永久代中进行垃圾回收的一个挑战是确定哪些类不再被使用。一个类不再被使用的条件包括:该类的所有实例都已被回收,对应的类加载器已经被回收,以及该类没有在任何地方被引用。

能详细说一下 CMS 收集器的垃圾收集过程吗?

CMS 收集器的垃圾收集过程可以分为几个主要阶段:

  1. 初始标记(Initial Mark)
    • 这是一个需要“Stop-The-World”(STW)的阶段,意味着在这个阶段中,所有的应用线程都会被暂停。
    • 在这个阶段,标记所有直接与 GC Roots 相连的对象。GC Roots 包括本地变量、活跃线程等。
  1. 并发标记(Concurrent Mark)
    • 这个阶段是并发执行的,即在执行垃圾收集的同时,应用程序的线程也在运行。
    • 在这个阶段中,JVM 遍历堆中的对象图,标记所有从 GC Roots 可达的对象。
  1. 预清理(Precleaning)
    • 这也是一个并发阶段,目的是为了减少最终标记的工作量。
    • 它主要是用来更新由于应用程序继续运行而发生变化的那部分对象的标记记录。
  1. 最终标记(Final Mark)
    • 这是第二个需要 STW 的阶段,但通常比初始标记阶段更短。
    • 在这个阶段,处理在并发标记和预清理阶段遗漏的那些在应用程序运行期间被修改的对象。
  1. 并发清除(Concurrent Sweep)
    • 在标记完成之后,开始并发地清理垃圾。
    • 在这个阶段,不再使用的对象被清理,但应用线程仍然在运行。
  1. 并发重置(Concurrent Reset)
    • 在垃圾收集完成后,重置 CMS 收集器的内部数据结构,为下一次垃圾回收做准备。

垃圾收集器应该如何选择?

  • 应用的性能需求
    • 响应时间优先:如果应用需要低延迟,例如在线交易处理系统,应选择如 G1 GC 或 ZGC 这样的低停顿时间收集器。
    • 吞吐量优先:对于批处理和后台处理等吞吐量更重要的场景,可以考虑 Parallel GC 或 CMS。
  • 堆的大小
    • 对于较小的堆(几百 MB 到几 GB),可以使用 Serial GC 或 Parallel GC。
    • 对于大堆(数十 GB 以上),G1 GC 或 ZGC 会是更好的选择。
  • 调优的复杂性和管理开销
    • 一些高级的收集器(如 CMS、G1 GC)提供了更多的调优选项,但也增加了管理和调优的复杂性。
    • 对于需要简单配置和维护的场景,简单的收集器(如 Serial GC)可能更合适。
  • 硬件资源
    • 如果资源(如 CPU 核心数)有限,可能需要避免使用那些对资源需求较高的收集器,例如 Parallel GC 或 G1 GC。
    • 在资源充足的情况下,可以选择更复杂、更高效的收集器。

能详细说一说G1 收集器吗?

G1(Garbage-First)是被设计用来替代旧的如 CMS(Concurrent Mark-Sweep)收集器。G1 收集器的主要目标是提供高吞吐量同时保持尽可能低的延迟。

工作原理

  • 堆分区
    • G1 收集器将堆内存分割成多个大小相等的区域(Region),这些区域可以是 Eden、Survivor 或 Old Generation。
    • 这种划分使得 G1 能够更灵活地管理堆,只回收价值最大的区域,从而控制回收周期的长度。
  • 回收过程
    • G1 收集器同时处理 Young 和 Old Generation,用来替代传统的 Young Generation(如 Parallel GC)和 Old Generation(如 CMS)收集器。
    • G1 通过跟踪每个区域中的垃圾量,优先回收包含最多垃圾的区域,以此提高回收效率。
  • 并发和停顿
    • G1 收集器的许多操作是并发进行的。例如,在处理 Old Generation 时,它会并发地标记活动对象。
    • G1 致力于保持较短的停顿时间,它允许用户指定期望的停顿时间目标(例如,不超过 100ms),并尽量在这个范围内完成回收工作。

特点

  • 可预测的停顿时间
    • 通过设定期望的停顿时间,G1 能够更好地满足需要低延迟的应用的需求。
  • 高效的垃圾回收
    • 回收最有价值区域的策略提高了回收效率,尤其是在堆内存较大的情况下。
  • 并发标记
    • 并发地标记活动对象,减少了全面垃圾回收的需要,从而降低了停顿时间。
  • 碎片整理
    • 在进行垃圾回收时,G1 还会进行碎片整理,这有助于避免内存碎片化,提高内存分配的效率。

适用场景

  • G1 特别适合于堆内存较大(超过 4GB)的应用程序。
  • 对于要求较短停顿时间的应用,G1 也是一个很好的选择。

有了 CMS,为什么还要引入 G1?

由于 CMS 对 CPU 资源比较敏感,在垃圾回收的过程还会产生内存碎片。而 G1 旨在填补 CMS 的不足:

  1. 更好的性能:G1 提供了更好的性能,尤其是在大内存应用中。它通过划分区域和优先级回收减少了全堆扫描的需要。
  2. 内存碎片处理:G1 通过整理来减少内存碎片,这是 CMS 面临的主要问题之一。
  3. 可预测的停顿:G1 允许用户指定停顿时间,这对于需要高响应性的应用来说非常重要。
  4. 更广泛的适用性:G1 的设计旨在适应更广泛的应用场景,包括那些拥有大量内存和多核处理器的服务器环境。

类加载机制

能说一下类的生命周期吗?

在 Java 中,类的生命周期是指类从加载到 JVM 中,直到卸载出 JVM 的整个过程。这个过程大致可以分为以下几个主要阶段:

  • 加载(Loading):
    • 在这个阶段,JVM 会通过类加载器读取类的二进制数据,并将其转换成一个 java.lang.Class 类的实例。这个过程包括检查类的格式、验证其正确性、分配内存,并初始化静态字段。
  • 链接(Linking):
    • 链接阶段可以进一步细分为验证、准备和解析三个子阶段:
      • 验证(Verification): 确保被加载的类的正确性,检查类是否符合 Java 虚拟机规范。
      • 准备(Preparation): 为类的静态变量分配内存,并初始化为默认值。
      • 解析(Resolution): 解析这个类中用到的其他类、接口、字段和方法的引用。
  • 初始化(Initialization):
    • 在初始化阶段,JVM 会执行类构造器 <clinit>() 方法的代码。这包括静态变量的赋值操作和静态代码块的执行。
  • 使用(Using):
    • 类一旦被加载并初始化后,就可以被程序使用了。这包括创建类的实例、调用类的方法、使用类的变量等。
  • 卸载(Unloading):
    • 类在不再被使用时,可以被 JVM 卸载。这通常在类的类加载器被垃圾回收时发生。类的卸载意味着该类的 java.lang.Class 实例被回收,同时该类的所有对象实例也都被回收。

描述一下JVM 加载class 文件的原理机制

JVM 加载 class 文件的原理机制大致分为如下几个关键阶段:

  • 类加载器(Class Loaders):
    • 类加载是由类加载器(ClassLoader)完成的。JVM 提供了几种内置的类加载器(比如启动类加载器、扩展类加载器和应用类加载器),同时也允许创建自定义的类加载器。
    • 类加载器首先会检查请求的类是否已经被加载。如果已加载,直接返回该类的 Class 对象;如果未加载,进入加载过程。
  • 查找 Class 文件:
    • 类加载器会根据类的全限定名(如 java.lang.String)在文件系统或者其他地方(如网络、ZIP 文件等)查找对应的 .class 文件。
  • 加载 Class 文件:
    • 一旦找到了对应的 Class 文件,类加载器会将该文件的内容(二进制数据)加载到内存中。这些数据包含了类的结构、方法、变量等信息。
  • 解析:
    • 加载到内存中的类数据会被解析成为一个 java.lang.Class 类的实例。这个实例代表了被加载的类,包含了该类的所有信息。
  • 验证:
    • 验证是确保加载的类满足 JVM 规范且不会危害到 JVM 自身安全的一个重要步骤。这包括对类的格式、方法和字段的访问性、以及其他一些基本的结构检查。
  • 准备:
    • 在准备阶段,JVM 会为类中的所有静态变量分配内存,并设置默认初始值。这些默认值通常是数据类型的零值(如 0、false 等)。
  • 解析引用:
    • 这一步涉及将类文件中的符号引用转换为直接引用。符号引用是一种抽象的引用,如类和接口的全限定名,而直接引用通常是指向内存中对象或方法的指针。
  • 初始化:
    • 最后阶段是类的初始化,这时会执行静态初始化器(静态代码块)和静态变量的赋值操作。

类加载器有哪些?

  • 启动类加载器(Bootstrap ClassLoader):
    • 这是最顶层的类加载器,用于加载 JVM 的核心类库,如 rt.jar 和其他核心库文件。它不是 java.lang.ClassLoader 的子类,因为 JVM 是用原生代码实现的。
  • 扩展类加载器(Extension ClassLoader):
    • 负责加载 Java 的扩展库,即 JAVA_HOME/jre/lib/ext 目录下的 JAR 包和类文件,或者由系统属性 java.ext.dirs 指定的路径下的类库。
  • 应用类加载器(Application ClassLoader):
    • 这是用于应用程序级别的类加载器,负责加载环境变量 CLASSPATH 或系统属性 java.class.path 指定路径下的类库。它是 java.lang.ClassLoader 的一个实例,是我们通常使用的默认类加载器。
  • 用户自定义类加载器:
    • Java 允许开发者通过继承 java.lang.ClassLoader 类来创建自己的类加载器。这使得开发者可以自定义类的加载方式,例如从加密文件中加载类,或者从网络等非标准来源加载。

类加载器之间存在父子关系,但并非通过继承实现,而是通过组合。当一个类加载器收到类加载的请求时,它首先会委托给其父加载器去尝试加载这个类,只有在父加载器加载失败时,它才会尝试自己去加载。这种委托机制确保了 Java 类的唯一性,例如,无论一个应用中有多少个类加载器,java.lang.Object 这个类都只会被加载一次。

什么是双亲委派模型?

双亲委派模型是 Java 中类加载器使用的一种特定机制,用于加载类和接口。

  • 当一个类加载器收到类加载的请求时,它首先不会尝试自己加载这个类,而是把这个请求委派给父类加载器去完成。
  • 这个过程会一直向上递归,直到达到最顶层的启动类加载器(Bootstrap ClassLoader)。
  • 只有当父类加载器无法完成这个请求(即它不支持加载请求的类)时,子类加载器才会尝试自己去加载这个类。

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

  • 保障安全性:
    • 双亲委派机制可以防止基础类库被随意替换。由于系统类由启动类加载器加载,因此即使用户编写了与系统类同名的类,也不会导致系统类被替换,这保护了 Java 核心类库的安全性。
  • 避免类的重复加载:
    • 在双亲委派模型下,类只会被加载一次。当一个类加载器收到类加载的请求时,它会首先尝试让其父类加载器去加载这个类,以此来保证不会加载已经加载过的类。这避免了同一个类被多次加载的情况,节省了资源,同时保持了类的单一性。
  • 保持命名空间的一致性:
    • 双亲委派模型帮助维护了 Java 类的命名空间。不同的类加载器可以定义不同的命名空间,相同名称的类可以存在于不同的类加载器及其命名空间中,而不会发生冲突。
  • 提升性能:
    • 由于类只加载一次,所以减少了类加载的次数,这在一定程度上提高了性能。特别是对于核心类库这种被频繁使用的类,这种优势更加明显。

如何破坏双亲委派机制?

破坏双亲委派机制通常意味着自定义类加载器以改变类加载的顺序或方式。在特定的应用场景中,比如热部署(hot deployment)或插件式架构,可能需要定制类加载过程以满足特殊需求。要破坏双亲委派机制,可以按照以下步骤进行:

  1. 自定义类加载器:
    • 继承 java.lang.ClassLoader 类来创建自己的类加载器。
  1. 重写 ****loadClass():
    • 在自定义类加载器中重写 loadClass()。通常,loadClass() 方法首先会调用父类加载器尝试加载类,如果失败,再尝试自己加载类。要破坏双亲委派模型,可以改变这个顺序。
  1. 直接加载类:
    • 在 loadClass() 方法中,可以先尝试自己加载类,而不是委派给父类加载器。如果自己无法加载,再考虑将请求委派给父类加载器。
  1. 使用 ****findClass():
    • 在自定义类加载器中实现 findClass() 方法。当 loadClass() 方法在父类加载器中找不到类时,将会调用 findClass()方法尝试加载类。在 findClass() 方法中,可以实现特定的类加载逻辑。

你知道 Java 中有哪些破坏了双亲委派机制?

在 Java 中,有几个典型的例子破坏了双亲委派机制:

JDBC 4.0 驱动加载

  • 在 JDBC 4.0 中,引入了一种服务提供者机制(SPI),允许 JDBC 驱动程序在 META-INF/services 目录下注册自己,而不需要通过代码显式加载驱动。这种机制实际上破坏了传统的双亲委派模型,因为它允许驱动程序类被应用程序类加载器加载,而不是始终由系统类加载器加载。

Tomcat 容器

  • Tomcat为每个部署的 Web 应用使用一个单独的类加载器,允许加载与其他应用隔离的类。这样,相同的类(例如,一个常见的库的不同版本)可以存在于不同的 Web 应用中,而不会相互干扰。这种机制破坏了标准的双亲委派模型,因为容器首先尝试使用自己的类加载器加载类,而不是委派给父类加载器。

你觉得应该怎么实现一个热部署功能?

热部署功能是可以在修改完代码后,不重启应用,实现类信息更新,以节省开发时等待启动时间。

在 Java 中,要实现热部署功能通常涉及动态地加载、更新和卸载类或资源。下面实现热部署的基本步骤:

自定义类加载器:

  • 实现一个自定义的类加载器,这个类加载器可以加载或重新加载改变了的类文件。热部署的关键是能够替换旧的类定义,这通常需要破坏标准的双亲委派模型。

类定义的隔离:

  • 保证每个版本的类都被隔离开来,以避免不同版本之间的冲突。这可以通过为每个版本的类创建不同的类加载器实例来实现。

监控类文件的改变:

  • 实现一个机制来监控类文件的改变。这可以通过文件系统监控,或者在开发环境中监听构建系统的输出。

重新加载改变的类:

  • 一旦检测到类文件有改变,使用新的或者专门的类加载器加载新版本的类。旧版本的类需要被垃圾回收,这通常意味着需要切断所有到旧类实例和类加载器的引用。

状态管理:

  • 在类重新加载时,可能需要保持应用的状态。这通常涉及在重新加载前保存状态,并在加载后恢复。

JVM 调优

JVM 的常见参数配置知道哪些?

  1. 堆内存大小设置:
    • -Xms<size>: 设置JVM启动时的初始堆内存大小。例如,-Xms256m 设置初始堆大小为 256 MB。
    • -Xmx<size>: 设置JVM可以使用的最大堆内存大小。例如,-Xmx1024m 设置最大堆大小为 1024 MB。
  1. 栈大小设置:
    • -Xss<size>: 设置每个线程的堆栈大小。例如,-Xss1m 设置线程堆栈大小为 1 MB。
  1. 年轻代与老年代大小设置:
    • -Xmn<size>: 设置年轻代的大小。
    • -XX:NewRatio=<ratio>: 设置年轻代(包括Eden和两个Survivor区)与老年代的比例。
  1. 垃圾收集器设置:
    • -XX:+UseG1GC: 使用G1垃圾收集器。
    • -XX:+UseParallelGC: 使用并行垃圾收集器。
    • -XX:+UseConcMarkSweepGC: 使用CMS垃圾收集器。
    • -XX:+UseSerialGC: 使用串行垃圾收集器。
  1. GC日志设置:
    • -Xloggc:<file-path>: 将GC日志记录到文件。
    • -XX:+PrintGCDetails: 输出更详细的GC日志信息。
    • -XX:+PrintGCDateStamps: 在GC日志中添加时间戳。
  1. 堆转储设置:
    • -XX:+HeapDumpOnOutOfMemoryError: 在发生内存溢出时自动进行堆转储。
    • -XX:HeapDumpPath=<file-path>: 指定堆转储的文件路径。
  1. 性能优化相关:
    • -XX:CompileThreshold=<count>: 设置方法调用次数阈值,超过这个阈值就会被JIT编译器编译成本地代码。
    • -XX:SurvivorRatio=<ratio>: 设置Eden区与一个Survivor区的大小比例。
    • -XX:MaxTenuringThreshold=<threshold>: 设置对象在年轻代中的最大晋升年龄。
  1. 类加载相关:
    • -XX:+TraceClassLoading: 跟踪类的加载信息。
    • -XX:+TraceClassUnloading: 跟踪类的卸载信息。

线上服务 CPU 占用过高怎么排查?

参考文章:

内存飙高问题怎么排查?

参考文章:

频繁 minor gc 怎么办?

参考文章:

频繁 Full GC 怎么办?

参考文章:

JVM中的垃圾收集器有哪些,它们的工作原理是什么?

JVM中的垃圾收集器主要包括以下几种:

1、 Serial收集器:它是一个单线程收集器,工作时会暂停所有其他工作线程("Stop-The-World"),它的优点是简单高效(与其他收集器的单线程比),适用于单核处理器的环境。

2、 ParNew收集器:可以看作是Serial收集器的多线程版本,主要用于新生代的垃圾收集,适合多核处理器环境。

3、 Parallel Scavenge收集器:也是一个新生代垃圾收集器,使用多线程收集,注重吞吐量(CPU用于运行用户代码的时间比率)。

4、 Serial Old收集器:是Serial收集器的老年代版本,单线程,采用标记-整理算法。

5、 Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,使用多线程并行收集,目标是提高系统吞吐量。

6、 CMS(Concurrent Mark Sweep)收集器:以获取最短回收停顿时间为目标,使用多线程并发标记和清除算法。

7、 G1(Garbage-First)收集器:采用分区堆(Heap)和增量式垃圾回收,目标是兼顾吞吐量和停顿时间。

JVM中的类加载器有哪些,它们各自的作用是什么?

JVM中的类加载器主要有三种:

1、启动类加载器(Bootstrap ClassLoader) :它负责加载存放在<JAVA_HOME>/jre/lib目录中,或者被-Xbootclasspath参数指定的路径中的类库。

2、扩展类加载器(Extension ClassLoader) :它负责加载<JAVA_HOME>/jre/lib/ext目录中的类库。

3、应用程序类加载器(Application ClassLoader) :它负责加载用户类路径(Classpath)上所指定的类库。

每个类加载器都有其特定的加载范围,这种层次关系保证了Java程序稳定运行。

JVM中垃圾回收的算法有哪些?

JVM垃圾回收的主要算法包括:

1、标记-清除算法(Mark-Sweep) :先标记出所有需要回收的对象,然后统一清除这些对象。

2、复制算法(Copying) :将内存分为两块,每次只使用其中一块,当这一块的内存用完了,就把还活着的对象复制到另一块上去。

3、标记-整理算法(Mark-Compact) :类似于标记-清除算法,但在清除后会进行内存整理,减少碎片。

4、分代收集算法(Generational Collection) :根据对象存活周期的不同将内存划分为几块,如年轻代、老年代等,采用适合各个年代的收集算法。

JVM如何判断对象是否死亡?

JVM判断对象是否死亡主要有两种方式:

1、引用计数法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

2、可达性分析:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

JVM中有哪些引用类型?

JVM中主要有四种引用类型:

1、强引用(Strong Reference) :普通的对象引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象。

2、软引用(Soft Reference) :内存不足时,会被垃圾回收器回收掉。

3、弱引用(Weak Reference) :只能生存到下一次垃圾收集之前,当垃圾回收器工作时,无论内存是否足够,都会回收掉只被弱引用关联的对象。

4、虚引用(Phantom Reference) :最弱的一种引用关系,无法通过虚引用来获取对象实例,它的存在仅仅是为了在这个对象被收集器回收时收到一个系统通知。

JVM的永久代(PermGen)和元空间(Metaspace)有什么区别?

永久代(PermGen)和元空间(Metaspace)是JVM中存储类元数据的区域,二者的主要区别如下:

1、存储位置:永久代是在JVM的堆内存中,而元空间位于本地内存。

2、大小限制:永久代的大小是固定的,容易出现内存溢出;元空间利用本地内存,所以默认情况下只受本地内存大小限制。

3、回收机制:永久代的回收主要针对常量池的回收和对类型的卸载;元空间提供了更好的性能,在使用本地内存的同时,减少了垃圾收集的频率。

JVM中的堆和栈有什么区别?

堆和栈是JVM中两个重要的内存区域,它们的主要区别如下:

1、用途:堆用于存储对象实例和数组,是垃圾回收的主要区域;栈用于存储局部变量、操作数栈和控制流。

2、线程共享性:堆是被所有线程共享的内存区域,而栈是线程私有的。

3、内存分配:堆的内存分配是动态的,适用于动态对象的存储;栈的内存分配是连续的,适用于临时变量的存储。

4、内存回收:堆的内存回收由垃圾回收器处理,栈的内存分配和回收是自动的。

JVM中Full GC触发的原因有哪些?

Full GC触发的原因主要有以下几点:

1、老年代空间不足:老年代存放长期存活的对象,当老年代空间不足时,会触发Full GC。

2、永久代或元空间不足:当存储类元数据的区域空间不足时,同样会触发Full GC。

3、System.gc()调用:系统调用System.gc()时,通常会触发Full GC。

4、JVM参数:由于某些JVM参数设置不当,如过小的堆空间,也可能触发Full GC。

5、Eden区、Survivor区向老年代晋升失败:如果Eden区或Survivor区中的对象在Minor GC后仍然存活,并且老年代无法容纳这些对象,也会触发Full GC。

JVM中的类加载器有哪些,它们各自的作用是什么?

JVM中的类加载器主要有以下几种:

1、启动类加载器(Bootstrap ClassLoader): 加载Java的核心库(JAVA_HOME/jre/lib/rt.jar等),是所有类加载器的父加载器。

2、扩展类加载器(Extension ClassLoader): 加载JAVA_HOME/jre/lib/ext目录中或者由java.ext.dirs系统属性指定的路径中的类库。

3、应用程序类加载器(Application ClassLoader): 加载用户类路径(Classpath)上的类库,如果应用中没有定义自己的类加载器,这将是默认的类加载器。

每个类加载器都有自己的职责范围,确保Java应用能够加载类的灵活性和安全性。

JVM内存区域划分及其作用是什么?

JVM的内存区域主要包括:

1、方法区(Method Area): 存储类信息、常量、静态变量等。

2、堆(Heap): 存放对象实例,是垃圾收集器管理的主要区域。

3、栈(Stack): 存放方法的局部变量表、操作数栈、动态链接等信息。

4、程序计数器(Program Counter Register): 当前线程所执行的字节码的行号指示器。

5、本地方法栈(Native Method Stack): 为Native方法服务。

这些区域各司其职,共同支持了Java程序的运行。

JVM中垃圾收集的算法有哪些?

JVM垃圾收集的主要算法包括:

1、标记-清除算法(Mark-Sweep): 标记出所有需要回收的对象,然后统一回收这些对象。

2、复制算法(Copying): 将内存分为两块,每次只使用其中一块,垃圾收集时将活动对象复制到另一块上。

3、标记-整理算法(Mark-Compact): 标记过程与标记-清除算法相同,但后续步骤是将所有存活的对象向一端移动,然后清理掉端边界以外的内存。

4、分代收集算法(Generational Collection): 根据对象存活周期的不同将内存划分为几块,比如年轻代、老年代等,各个年代使用不同的垃圾收集算法。

这些算法各有优劣,JVM根据具体情况选择合适的算法或算法组合进行垃圾回收。

JVM中的双亲委派模型。

JVM中的双亲委派模型是类加载器的一种工作机制,其主要特点如下:

1、工作流程: 当一个类加载器尝试加载某个类时,它首先不会尝试自己去加载这个类,而是把这个请求委托给父类加载器去完成,依次递归,如果父加载器无法完成这个加载(比如它的搜索范围中没有这个类),子加载器才会尝试自己去加载这个类。

2、优点: 这个模型可以避免类的重复加载,保护程序安全防止核心API被随意篡改。

3、破坏双亲委派: 在某些情况下,比如SPI(Service Provider Interface)服务,类加载器需要破坏双亲委派模型以便于服务提供者能够提供接口实现。

JVM中的永久代(PermGen)和元空间(Metaspace)有何区别?

永久代(PermGen)和元空间(Metaspace)是JVM内存的两个不同部分:

1、永久代(PermGen): 在JVM早期版本中,用于存储类的元数据、字符串常量等。它有固定的大小,容易发生OutOfMemoryError。

2、元空间(Metaspace): 在Java 8中,永久代被元空间所替代。元空间使用本地内存,存储类的元数据。相比永久代,它的大小受物理内存的限制,更加灵活。

3、性能优化: 元空间的引入减少了垃圾收集的频率,提升了性能,尤其是在加载大量类的应用中。

JVM优化的常见手段有哪些?

JVM优化通常涉及以下几个方面:

1、调整堆大小: 适当增加JVM堆的大小可以减少垃圾收集的频率,但过大的堆可能会增加垃圾收集的时间。

2、选择合适的垃圾收集器: 根据应用的特点选择最合适的垃圾收集器,如G1、CMS等。

3、线程堆栈大小(-Xss): 适当的线程堆栈大小可以减少内存的使用,但太小可能会导致栈溢出。

4、分析并优化代码: 优化应用代码,减少内存泄漏和不必要的对象创建。

5、JVM参数调优: 通过调整JVM启动参数,优化JVM的性能。

6、使用性能分析工具: 利用JProfiler、MAT、VisualVM等工具分析性能问题。

JVM中的Just-In-Time(JIT)编译器。

JVM中的Just-In-Time(JIT)编译器是一种提高程序运行效率的机制,其工作原理如下:

1、编译时机: JIT编译器在运行时将字节码转换为本地机器码,而非在程序运行之前。

2、优化代码: JIT编译器可以根据运行时的数据和行为对代码进行优化。

3、提升性能: JIT编译可以显著提高Java程序的性能,减少程序运行时间。

4、适应性: JIT编译器可以适应程序的运行特性,进行动态优化。

JVM中的类加载过程包括哪些阶段?

JVM中的类加载过程主要包括以下几个阶段:

1、加载(Loading): 读取类的二进制数据,并生成对应的Class对象。

2、验证(Verification): 确保加载的类符合JVM规范,没有安全问题。

3、准备(Preparation): 为类的静态变量分配内存,并设置默认初始值。

4、解析(Resolution): 将类中的符号引用转换为直接引用。

5、初始化(Initialization): 执行类构造器()方法的过程。

这个过程确保了类在使用前已被正确处理和初始化。

Java内存模型(JMM)中的happens-before原则是什么?

Java内存模型(JMM)中的happens-before原则定义了一组规则,用以保证多线程环境下的内存可见性,这些规则包括:

1、程序顺序规则: 一个线程内保证语句的执行顺序。

2、锁定规则: 解锁一个锁之前的操作对于接下来对这个锁的加锁操作是可见的。

3、volatile变量规则: 对一个volatile变量的写操作对后续对这个变量的读操作是可见的。

4、传递性: 如果操作A happens-before B,且B happens-before C,则A happens-before C。

5、线程启动规则: Thread对象的start()方法happens-before该线程的每个动作。

6、线程终止规则: 线程中的所有操作都happens-before其他线程检测到这个线程已经终止,通过Thread.join()方法或者isAlive()的返回值。

这些原则为开发者提供了编写线程安全代码的指导。

JVM中的Finalize方法有哪些问题?

JVM中的Finalize方法存在以下问题:

1、不确定性: Finalize方法的执行时间不确定,依赖于垃圾收集器的行为。

2、性能问题: Finalize方法的执行可能会导致性能下降。

3、资源释放不及时: 依赖Finalize方法释放资源可能导致资源不及时释放,特别是在IO资源和数据库连接等场合。

4、安全问题: 在Finalize方法中可以复活对象,可能会导致对象被意外地重新使用。

5、已被废弃: 从Java 9开始,Finalize方法被标记为废弃。

由于这些问题,通常建议避免使用Finalize方法,改用try-with-resources和其他清理资源的方法。

JVM中的弱引用、软引用、强引用和虚引用有何区别?

在JVM中,对象引用类型分为强引用、软引用、弱引用和虚引用,区别如下:

1、强引用(Strong Reference): 常规的对象引用,只要强引用存在,垃圾收集器永远不会回收被引用的对象。

2、软引用(Soft Reference): 对内存敏感的对象引用,只有在内存不足时,垃圾收集器才会考虑回收软引用指向的对象。

3、弱引用(Weak Reference): 弱于软引用的一种引用类型,垃圾收集器在下一次收集时,会回收只被弱引用指向的对象。

4、虚引用(Phantom Reference): 最弱的一种引用关系,无法通过虚引用来获取对象实例,主要用于跟踪对象被垃圾收集的活动。

这些不同类型的引用提供了更灵活的方式来控制对象的生命周期和内存管理。

JVM中的逃逸分析是什么,它是如何工作的?

逃逸分析是JVM优化技术之一,它分析对象的作用域和生命周期。其工作原理如下:

1、分析对象的动态作用域: 判断对象的使用范围是否会逃出方法或线程的范围。

2、栈上分配: 如果对象不会逃逸出方法范围,那么可以在栈上分配内存,从而减少垃圾收集的压力。

3、锁消除: 如果分析结果表明某些锁仅被单线程访问,那么这些锁可以被消除。

逃逸分析的目的是优化内存分配和同步操作,提高程序性能。

JVM调优中如何诊断和解决内存泄漏问题?

JVM调优中诊断和解决内存泄漏的方法包括:

1、分析内存使用: 使用JVM提供的工具(如jconsole、VisualVM)监控内存使用情况。

2、生成堆转储: 在内存使用较高时生成堆转储(Heap Dump),并使用MAT(Memory Analyzer Tool)等工具进行分析。

3、识别泄漏对象: 通过分析工具找出占用内存最多的对象和类。

4、查找引用链: 确定泄漏对象的引用链,找出泄漏的原因。

5、代码审查: 根据分析结果审查相关代码,寻找内存泄漏的根源。

6、修复和验证: 修改代码以解决内存泄漏问题,并再次进行测试和验证。

这些步骤需要结合具体的应用场景和代码特点来执行。

JVM中的栈溢出(StackOverflowError)是如何产生的,如何避免?

栈溢出(StackOverflowError)通常发生在以下情况:

1、深度递归调用: 方法反复递归调用自身,导致栈帧过多。

2、大量局部变量: 方法中定义了大量局部变量,占用过多栈空间。

避免栈溢出的方法:

1、优化递归逻辑: 转换为循环,减少递归深度。

2、增加栈大小: 通过-Xss调整线程栈的大小。

3、代码优化: 减少方法调用深度和局部变量的使用。

JVM中的内存分配与回收策略。

JVM中的内存分配与回收策略包括:

1、对象优先在Eden分配: 大部分情况下,对象首先在年轻代的Eden区分配。

2、大对象直接进入老年代: 较大的对象可能直接在老年代分配,避免在Eden区和Survivor区之间来回复制。

3、长期存活的对象进入老年代: 对象在年轻代经过多次GC后仍然存活,会被移动到老年代。

4、动态对象年龄判定: 为了更有效地进行垃圾收集,虚拟机会动态调整对象晋升老年代的年龄阈值。

5、分代收集算法: 年轻代使用复制算法,老年代使用标记-清除或标记-整理算法。

JVM中的直接内存(Direct Memory)是什么?

直接内存(Direct Memory)不是JVM堆内存的一部分,它是通过在Java代码中使用NIO库分配的内存,直接在操作系统的物理内存中分配。主要特点和用途:

1、避免内存复制: 直接内存访问避免了JVM堆和本地堆之间的内存复制,提高性能。

2、高效IO操作: 在NIO中,使用直接内存可以提高文件的读写效率。

3、内存管理: 直接内存的分配和回收不受JVM垃圾回收器管理,需要手动释放。

JVM的即时编译器(JIT)与解释器的工作机制有何不同?

JVM中的即时编译器(JIT)和解释器的主要区别在于:

1、器: 逐条将字节码解释执行,速度较慢,但几乎没有延迟。

2、即时编译器(JIT): 将字节码编译成本地机器码,提高执行效率,但有编译耗时。

3、选择机制: JVM会根据代码的执行频率选择使用解释器还是JIT编译器,热点代码(频繁执行的代码)通常会被JIT编译。

JVM中的安全点(Safepoint)和安全区域(Safe Region)。

JVM中的安全点(Safepoint)和安全区域(Safe Region)是垃圾收集过程中的概念:

1、安全点(Safepoint): 在这些点上,线程暂停执行,使得JVM可以安全地进行垃圾收集。

2、选择标准: 安全点的选择依据是“长时间执行”的指令,如方法调用、循环跳转等。

3、安全区域(Safe Region): 当线程执行到无法达到安全点时,会标记自己处于安全区域,如在执行长时间的阻塞操作时。

JVM中的方法区(Method Area)和运行时常量池。

JVM中的方法区和运行时常量池:

1、方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

2、运行时常量池: 方法区的一部分,用于存储编译期生成的各种字面量和符号引用。

3、Java 8变更: 在Java 8中,传统的永久代被元空间(Metaspace)所替代,方法区的概念仍然存在,但实现已经变化。

JVM中的类卸载机制是什么?

JVM中的类卸载机制:

1、触发条件: 当一个类的ClassLoader实例被回收,同时该类没有任何活跃的实例,且没有其他地方引用该类的方法或变量时,这个类就会被卸载。

2、回收过程: 类的卸载发生在垃圾收集过程中。

3、重要性: 在使用自定义类加载器频繁加载和卸载类的场景中,类卸载机制特别重要,以避免内存泄漏。

JVM中的引用计数法和可达性分析算法。

JVM中的引用计数法和可达性分析算法:

1、引用计数法: 每个对象有一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1。任何时刻计数器为0的对象可以被回收。

2、可达性分析算法: 通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。

3、主流选择: 在主流的JVM实现中,一般采用可达性分析算法来判断对象的存活,因为引用计数法无法解决对象之间相互循环引用的问题。

JVM中类加载机制的主要步骤有哪些?

JVM中类加载机制的主要步骤包括以下几个阶段:

1、加载(Loading): 在这个阶段,JVM会通过类加载器读取二进制数据,并将这些数据转化成Method Area内的运行时数据结构。在加载的过程中,JVM会对类进行解析。

2、验证(Verification): 验证是为了确保Class文件的字节流包含的信息符合当前JVM的要求,并且不会对JVM自身造成危害。

3、准备(Preparation): 在准备阶段,JVM会为类变量分配内存并设置默认初始值,这些变量所使用的内存都在Method Area中。

4、解析(Resolution): 解析阶段是将符号引用转换为直接引用的过程。符号引用来自于类文件的常量池部分,而直接引用是指向方法区的指针。

5、初始化(Initialization): 初始化是执行类构造器方法的过程。这个方法由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生。

这些步骤共同构成了JVM的类加载机制,保证了Java程序的稳定运行和安全。

JVM内存模型中各区域的作用是什么?

JVM内存模型主要包括以下几个区域及其作用:

1、方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量等数据。它是线程共享的区域。

2、堆(Heap): Java堆是JVM所管理的最大的一块内存区域,用于存储所有实例对象和数组。它是被所有线程共享的一块内存区域。

3、栈(Stack): 每个线程运行时都会创建一个栈,用于存储局部变量、操作数栈、动态链接、方法出口等信息。栈的生命周期和线程相同。

4、程序计数器(Program Counter Register): 是一小块内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

5、本地方法栈(Native Method Stack): 为虚拟机使用到的Native方法服务。

这些区域各自承担不同的职责,共同支持了JVM的运行时数据区域的需要。

JVM内存模型是如何划分的?

JVM内存模型主要划分为以下几个部分:

1、堆(Heap): 这是JVM内存中最大的一块,主要用于存储对象实例。堆内存被所有线程共享。在堆中,根据对象存活周期不同,进一步划分为新生代和老年代。

2、方法区(Method Area): 存储被加载的类信息、常量、静态变量等数据。它也是被所有线程共享的内存区域。

3、栈(Stack): 存储局部变量、操作数栈、动态链接等信息。每个线程创建时都会创建一个对应的栈,它们之间互不干扰。

4、程序计数器(Program Counter Register): 每个线程都有一个程序计数器,是线程私有的,用来存储指向下一条指令的地址。

5、本地方法栈(Native Method Stack): 主要用于处理本地方法调用,是线程私有的。

这种内存模型设计能够满足多线程环境下的内存需求,同时也便于垃圾收集器进行高效的内存回收。

JVM中类加载机制的过程是怎样的?

JVM的类加载机制包括以下几个主要步骤:

1、加载(Loading): JVM通过类加载器读取二进制数据,生成对应的Class对象。

2、链接(Linking): 包括验证(确保加载的类符合JVM规范)、准备(为类的静态变量分配内存并设置初始值)、解析(将符号引用转换为直接引用)三个阶段。

3、初始化(Initialization): 对类的静态变量赋予正确的初始值,执行静态代码块。

4、使用(Using): JVM应用程序调用类及其方法。

5、卸载(Unloading): 当类不再被使用且无法通过任何路径被访问到时,由垃圾收集器回收该类占用的内存,完成类的卸载。

类加载机制是JVM运行时数据区的重要组成部分,确保了Java程序的运行时动态加载、链接和初始化。

JVM如何处理对象的内存分配和回收?

JVM在处理对象的内存分配和回收时遵循特定的流程和策略:

1、内存分配: 当创建新对象时,JVM首先在堆内存的年轻代(Young Generation)中分配空间。年轻代通常分为Eden区和两个Survivor区。大多数新创建的对象首先被分配到Eden区。

2、对象晋升: 经过一定次数的垃圾回收后仍存活的对象,会从年轻代晋升到老年代(Old Generation)。老年代用于存放长期存活的对象。

3、垃圾回收: JVM通过垃圾回收器(Garbage Collector)定期回收无用对象。回收器主要有两类:针对年轻代的(如Parallel GC、G1 GC的年轻代模式)和针对老年代的(如CMS、G1 GC的老年代模式)。

4、回收算法: 垃圾回收算法包括标记-清除(Mark-Sweep)、标记-压缩(Mark-Compact)、复制算法(Copying)等。年轻代通常使用复制算法,而老年代常使用标记-清除或标记-压缩算法。

这个流程确保了JVM高效利用内存,同时维持应用程序的性能。

JVM监控和性能调优的常用工具有哪些?

JVM监控和性能调优的常用工具包括:

1、VisualVM: 一种多合一故障处理工具,用于获取Java应用程序的详细信息。它提供了CPU、内存、线程和GC的实时监控数据。

2、JConsole: Java监控和管理控制台,用于实时监控JVM的性能和资源消耗。可以监控内存使用情况、线程数量和对象创建速率。

3、Java Mission Control: 一个高级分析工具,用于收集Java应用程序运行时的详细性能和资源使用数据。它与JVM的Flight Recorder一起使用,用于收集低开销的诊断和监控数据。

4、MAT(Memory Analyzer Tool): 一种强大的Java堆分析工具,用于分析大型Java堆转储。它可以帮助识别内存泄漏和查找占用大量内存的对象。

5、GCViewer: 一款用于分析Java垃圾收集器日志的工具,可以帮助了解GC的行为和性能。

这些工具帮助开发人员理解和优化JVM的性能,确保Java应用运行高效稳定。

JVM在对象分配内存时的策略是什么?

JVM在对象分配内存时遵循特定的策略,确保内存的高效使用和应用程序性能的优化:

1、对象首次分配: 新创建的对象首先在堆内存的年轻代中分配空间。年轻代主要包括Eden区和两个Survivor区。大部分情况下,对象最初被分配在Eden区。

2、小对象与大对象: JVM对小对象和大对象使用不同的分配策略。小对象通常在年轻代中分配,而大对象可能直接在老年代中分配,以避免频繁的复制。

3、年龄判断和晋升: 对象在年轻代中每经历一次垃圾回收仍然存活,其年龄就会增加。达到一定年龄阈值后,对象会被晋升到老年代。这个阈值可以通过参数进行调整。

4、内存分配失败处理: 如果在年轻代中无法为新对象找到足够空间,JVM将触发一次Minor GC。如果GC后仍无法满足内存需求,对象可能直接在老年代中分配。

JVM中类加载机制的主要特点是什么?

JVM中的类加载机制具有以下几个主要特点:

1、全盘负责机制: 当一个类加载器负责加载某个类时,该类所依赖的其他类也由这个类加载器负责加载,除非显式使用另一个类加载器。

2、父委托模型: 类加载器在尝试自己加载类之前,先委托给父加载器进行加载。这保证了Java核心库的类型安全。

3、缓存机制: 一旦类被加载进内存,它们就会被缓存。后续的类加载请求会首先检查缓存,以提高加载效率。

4、三个阶段:加载、链接和初始化: 类加载的过程包括加载、链接(验证、准备、解析)和初始化。加载是指查找字节流并创建类的过程,链接是指验证并为类的静态字段分配内存,初始化则涉及执行类构造器。

JVM内存区域划分及其作用是什么?

JVM内存区域主要包括以下几个部分:

1、方法区(Method Area) :存储类信息、常量、静态变量等数据。

2、堆(Heap) :存放对象实例,是垃圾回收器的主要区域。

3、栈(Stack) :存放局部变量表、操作数栈、动态链接等信息。

4、程序计数器(Program Counter Register) :存储当前线程执行的字节码的行号指示器。

5、本地方法栈(Native Method Stack) :专门为执行本地方法服务。

这些区域的划分使得JVM能够高效地管理不同类型的数据。

JVM在对象分配内存时的策略是什么?

JVM在对象分配内存时遵循特定的策略,确保内存的高效使用和应用程序性能的优化:

1、对象首次分配: 新创建的对象首先在堆内存的年轻代中分配空间。年轻代主要包括Eden区和两个Survivor区。大部分情况下,对象最初被分配在Eden区。

2、小对象与大对象: JVM对小对象和大对象使用不同的分配策略。小对象通常在年轻代中分配,而大对象可能直接在老年代中分配,以避免频繁的复制。

3、年龄判断和晋升: 对象在年轻代中每经历一次垃圾回收仍然存活,其年龄就会增加。达到一定年龄阈值后,对象会被晋升到老年代。这个阈值可以通过参数进行调整。

4、内存分配失败处理: 如果在年轻代中无法为新对象找到足够空间,JVM将触发一次Minor GC。如果GC后仍无法满足内存需求,对象可能直接在老年代中分配。

JVM中类加载机制的主要特点是什么?

JVM中的类加载机制具有以下几个主要特点:

1、全盘负责机制: 当一个类加载器负责加载某个类时,该类所依赖的其他类也由这个类加载器负责加载,除非显式使用另一个类加载器。

2、父委托模型: 类加载器在尝试自己加载类之前,先委托给父加载器进行加载。这保证了Java核心库的类型安全。

3、缓存机制: 一旦类被加载进内存,它们就会被缓存。后续的类加载请求会首先检查缓存,以提高加载效率。

4、三个阶段:加载、链接和初始化: 类加载的过程包括加载、链接(验证、准备、解析)和初始化。加载是指查找字节流并创建类的过程,链接是指验证并为类的静态字段分配内存,初始化则涉及执行类构造器。

JVM中类加载机制的过程是怎样的?

JVM中类加载机制的过程主要包括以下几个步骤:

1、加载: 类加载器从文件系统或者网络中加载.class文件,生成对应的二进制数据,并在JVM内部创建一个Class对象。

2、链接: 验证加载的类信息是否符合JVM规范,为静态域分配存储空间,并解析该类中的符号引用转换为直接引用。

3、初始化: 执行类构造器()方法的过程。这个方法由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生。

JVM内存模型是如何划分的?

JVM的内存模型主要包括以下几个部分:

1、堆(Heap): 是JVM内存管理的主要区域,存储对象实例和数组。按照对象存活周期不同分为新生代和老年代。

2、方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量等数据。

3、程序计数器(Program Counter Register): 当前线程所执行的字节码的行号指示器。

4、虚拟机栈(JVM Stacks): 每个方法执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接等信息。

5、本地方法栈(Native Method Stacks): 为虚拟机使用到的Native方法服务。

垃圾回收算法有哪些?

JVM的垃圾回收算法主要包括以下几种:

1、标记-清除算法(Mark-Sweep): 首先标记出所有需要回收的对象,然后统一回收这些对象。

2、复制算法(Copying): 将内存分为两块,每次使用一块。当这一块用完,就把还活着的对象复制到另一块上,然后清除当前块的所有对象。

3、标记-整理算法(Mark-Compact): 类似于标记-清除算法,但在清除后会进行内存整理,减少内存碎片。

4、分代收集算法: 根据对象存活周期的不同将内存分为几块,如新生代、老年代,分别应用不同的垃圾回收算法。

简述JVM中的双亲委派模型。

JVM中的双亲委派模型是一种类加载机制,其工作原理如下:

1、类加载器请求: 当一个类加载器尝试加载某个类时,它首先不会尝试自己加载这个类,而是把这个请求委派给父类加载器去完成。

2、向上委派: 这个过程会一直向上委派,直到最顶层的启动类加载器。

3、加载检查: 如果父加载器可以完成类加载任务,就成功返回;如果父加载器无法完成此加载任务,子加载器才会尝试自己去加载。

JVM是如何处理对象的内存分配和回收的?

JVM在处理对象的内存分配和回收方面遵循以下机制:

1、内存分配: 当创建新对象时,JVM首先在堆内存的新生代中分配内存。若新生代内存不足,会触发Minor GC清理空间。对于大对象,直接在老年代分配。

2、内存回收: 通过垃圾回收器对堆内存中的对象进行回收。使用标记-清除、复制或标记-整理算法来识别并回收不再被引用的对象。

3、垃圾回收器的选择与调优: 根据应用程序的需求和特点选择合适的垃圾回收器,如Serial GC、Parallel GC、CMS、G1等,以及对其进行相应的调优。

4、分代回收: JVM将堆内存分为新生代和老年代,针对不同的代使用不同的垃圾回收策略,以提高回收效率。

5、性能监控与调优: 使用JVM提供的监控工具(如jVisualVM、jConsole)监控内存使用情况,及时调优以防止内存泄露和溢出。

详述JVM中的Minor GC和Full GC分别在什么情况下发生?

JVM中的Minor GC和Full GC发生的情况如下:

1、Minor GC: 主要发生在新生代。当新生代空间不足时(例如,Eden区满了),JVM会触发Minor GC。Minor GC会清理新生代中的无用对象,并将存活的对象移动到Survivor区或老年代。

2、Full GC: 发生在整个堆空间(包括新生代、老年代和永久代或元空间)。触发Full GC的情况包括老年代空间不足、永久代或元空间不足、System.gc()方法的调用等。Full GC的执行时间一般比Minor GC长,且会暂停所有的应用线程。

JVM中的类加载器有哪些,它们之间是如何工作的?

JVM中的类加载器主要有以下几种:

1、引导类加载器(Bootstrap ClassLoader): 加载Java核心类库,如rt.jar、resources.jar等。

2、扩展类加载器(Extension ClassLoader): 加载Java扩展库中的类,如jre/lib/ext目录下的jar包。

3、应用程序类加载器(Application ClassLoader): 加载用户路径(classpath)上的类库。

它们之间的工作关系遵循双亲委派模型:当一个类加载器收到类加载请求时,会先委托给父类加载器加载,只有在父类加载器无法完成时,自己才会尝试去加载。

JVM优化的常用方法有哪些?

JVM优化的常用方法包括:

1、选择合适的垃圾回收器: 根据应用需求选择适合的GC算法,如G1、CMS、Parallel GC等。

2、调整堆大小: 根据应用的内存需求调整JVM堆的大小,以优化内存使用和GC性能。

3、优化GC参数: 调整GC相关参数,如新生代和老年代的比例、垃圾回收的频率和时间等。

4、线程堆栈大小: 调整线程堆栈大小,以减少内存使用或提高性能。

5、监控和调优: 使用JVM监控工具(如JVisualVM)监控运行时的性能,根据监控结果进行相应的调优。

JVM中的永久代(PermGen)和元空间(Metaspace)有什么区别?

JVM中的永久代(PermGen)和元空间(Metaspace)的区别主要体现在:

1、存储内容: 永久代主要存储类的元数据、常量池、静态变量等,而元空间主要存储类的元数据。

2、内存分配区域: 永久代是JVM堆的一部分,而元空间不在虚拟机内存中,而是使用本地内存。

3、内存限制: 永久代的大小是固定的,超过限制会导致OutOfMemoryError;元空间的大小受到系统可用内存的限制,可以动态扩展。

4、垃圾回收: 永久代的垃圾回收比较少见,而元空间的垃圾回收更加频繁和高效。


尼恩JVM八股文

再有就是JDK不同版本的实现


张洪亮

还有一部分比较深入JVM了

各个组件深入面试题


牛客看到的我不熟悉的面经整理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值