当前位置:   article > 正文

JVM面试题:85道JVM虚拟机面试题及答案_jvm面试题总结及答案

jvm面试题总结及答案

面试题 1 .简述Java堆的结构?什么是堆中的永久代(Perm Gen space)?

JVM整体结构及内存模型

试题回答参考思路:
1、堆结构
JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
堆内存是由存活和死亡的对象组成的,存活的对象是应用可以访问的,不会被垃圾回收。
死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。
2、永久代(Perm Gen space)
永久代主要存在类定义,字节码,和常量等很少会变更的信息。并且永久代不会发生垃圾回收,如果永久代满了或者超过了临界值,会触发完全垃圾回收(Full Gc)

  • 永久代中一般包含:
  • 类的方法(字节码…)
  • 类名(Sring对象)
  • .class文件读到的常量信息
  • class对象相关的对象列表和类型列表 (e.g., 方法对象的array)
  • JVM创建的内部对象
  • JIT编译器优化用的信息

而在java8中,已经移除了永久代,新加了一个叫做元数据区的native内存区。
3、元空间
元空间和永久代类似,都是对JVM中规范中方法的实现。
不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
因此,默认情况下,元空间的大小仅受本地内存的限制。
类的元数据放入native memory,字符串池和类的静态变量放入java堆中。这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
4、采用元空间而不用永久代的原因
为了解决永久代的OOM问题,元数据和class对象存放在永久代中,容易出现性能问题和内存溢出。
类及方法的信息等比较难确定其大小,因此对于永久代大小指定比较困难,大小容易出现永久代溢出,太大容易导致老年代溢出(堆内存不变,此消彼长)。
永久代会为GC带来不必要的复杂度,并且回收效率偏低。

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]

面试题 2 . 如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?

当对象的所有引用被置为null时,这个对象确实成为了垃圾收集(GC)的候选对象,但垃圾收集器并不会立即释放这个对象所占用的内存。
在Java和许多其他管理内存的编程语言中,垃圾收集的执行并不是立即发生的,它依赖于垃圾收集器的算法和策略以及当前内存使用情况。
垃圾收集器通常在以下几种情况下运行:

  1. 内存不足:当程序运行过程中可用内存不足时,垃圾收集器会被触发运行以回收无用的对象,从而释放内存。
  2. 系统空闲:在系统不那么忙碌时,垃圾收集器可能会运行,以优化内存的使用和系统的响应。
  3. 程序员请求:在某些语言中,程序员可以显式请求进行垃圾收集,虽然这不一定立即触发GC,但它可以增加GC运行的可能性。

因此,即使对象的引用被置为null,也不能保证垃圾收集器会立即运行来清理这些对象。实际上,具体的释放时间取决于垃圾收集器的实现细节和当前的内存状况。

面试题 3 . 描述行( serial )收集器和吞吐量( throughput )收集器的区别 ?

首先,行收集器,也就是Serial GC,它是一个单线程的垃圾收集器。如下图:

这意味着它在执行垃圾收集时,会暂停所有其他应用线程,也就是我们常说的“Stop-the-World”暂停。
行收集器因为其简单和单线程的特性,非常适合于单核处理器或内存资源相对较小的环境中。
它通常用在客户端应用或者较小的服务器上。
然后是吞吐量收集器,也称为Throughput GC或Parallel GC,它使用多个线程来并行地进行垃圾收集。

这种方式可以有效地提高垃圾收集的速度,尤其是在多核处理器的环境下。
吞吐量收集器的目标是最大化应用的运行时间比例,减少垃圾收集占用的时间,从而提高整体的应用吞吐量。
这使得它非常适合那些对吞吐量要求较高的后端服务和大数据处理系统。
总的来说,选择哪种收集器,主要取决于你的应用场景和具体需求。如果是在资源有限或单核CPU的环境下,行收集器可能是个更好的选择。而在多核心和需要高吞吐量的环境下,吞吐量收集器则更加合适。

面试题 4:在Java中对象什么时候可以被垃圾回收?

在Java中,一个对象是否可以被垃圾回收主要取决于是否还有活跃的引用指向这个对象。
具体来说,有几个条件可以使对象成为垃圾回收的候选者:

  1. 无引用指向:当一个对象不再有任何活跃的引用指向它时,它就可以被垃圾回收。这是最常见的情况,比如当对象的引用被显式设置为null,或者引用所在的作用域结束时。
  2. 循环引用:即使是两个或多个对象相互引用,但它们不再被其他活跃的引用或根对象引用时,这些对象也可以被视为垃圾并回收。Java的垃圾收集器可以识别这种情况。
  3. 从可达性分析:Java使用可达性分析(Reachability Analysis)来确定对象是否存活。在这种分析中,一系列称为“根”(roots)的对象被用作起点,这通常包括活跃线程的栈帧中的本地变量、静态字段和特定的方法区域。从这些根出发,GC将标记所有可以访问到的对象。未被标记的对象,即那些从根节点出发无法到达的对象,被认为是可以回收的。

  1. 弱引用和软引用:Java还提供了弱引用(WeakReference)和软引用(SoftReference)等机制,通过这些引用类型引用的对象可以在JVM需要内存时被垃圾回收器回收。弱引用的对象在垃圾收集时总是会被回收,而软引用的对象则在内存不足时会被回收。

因此,总结来说,一个Java对象在没有任何有效的引用指向它,或者只通过弱引用或软引用被访问时,就可以被视为垃圾并可能在下一次垃圾收集时被清理掉。这个过程依赖于垃圾收集器的具体实现和触发条件。

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]

面试题5:JVM的永久代中会发生垃圾回收么?

是的,在JVM的早期版本中,永久代是确实存在的,并且确实会发生垃圾回收。
永久代主要用来存储JVM加载的类信息、常量以及方法数据。尽管永久代中的对象通常有较长的生命周期,但它仍然可能涉及垃圾回收,特别是在类卸载的时候或者清理常量池中不再使用的数据时。
这种垃圾回收通常发生在进行全局垃圾收集(Full GC)的时候。
不过,需要注意的是,类的卸载和常量池的清理并不是特别频繁,因为它们的生命周期通常跟应用程序的生命周期一样长。
另外,从Java 8开始,Oracle的JVM不再使用永久代的概念了,转而使用元空间(Metaspace)来替代。

元空间不再使用JVM堆的一部分,而是直接使用本地内存。
在元空间中,仍然会发生垃圾回收,主要是为了清理不再使用的类元数据。
这种变化主要是为了提高性能以及更灵活地管理内存。
所以,如果我们谈论的是Java 8及以后的版本,那么应该关注元空间的垃圾回收,而不是永久代。

面试题 6 . 阐述什么是分布式垃圾回收( DGC )?它是如何工作的?

DGC叫做分布式垃圾回收。RMI使用DGC来做自动垃圾回收。
因为RMI包含了跨虚拟机的远程对象的引用,垃圾回收是很困难的。DGC使用引用计数算法来给远程对象提供自动内存管理
分布式垃圾回收的工作原理基本上是通过跟踪网络中对象的引用来实现的。主要有以下几个步骤:

  1. 引用跟踪:DGC系统会监控和记录每个对象在不同节点间的引用情况。每当一个节点对远程对象建立或解除引用时,这种改变会被记录下来。
  2. 引用计数:DGC常用的一种方法是引用计数。每个对象会有一个与之相关的计数器,记录着对该对象的引用次数。当某个节点创建对另一个节点上对象的引用时,该对象的引用计数增加;当引用被释放时,引用计数减少。
  3. 回收判定:当对象的引用计数归零时,意味着没有任何活跃的引用指向这个对象,此时该对象可以被回收。
  4. 资源回收:一旦确定对象可以被回收,相关的垃圾回收操作会被触发,释放占用的资源。
  5. 周期性检查:由于引用计数可能存在循环引用的问题,DGC系统通常会周期性地执行全局垃圾回收算法来检查和回收那些因为循环引用而未被清理的对象。

分布式垃圾回收面临的主要挑战是确保效率和准确性,特别是在大规模分布式系统中。延迟、网络分区和节点故障都可能影响DGC的准确性和性能。因此,实现一个高效且可靠的DGC机制是一个具有挑战性的任务,通常需要在系统设计阶段就进行细致的规划和测试。

面试题 7 . 请描述什么是Java虚拟机?

1:Java虚拟机(JVM)一种用于计算机设备的规范,可用不同的方式(软件或硬件)加以实现。编译虚拟机的指令集与编译微处理器的指令集非常类似。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。
Java虚拟机(JVM)是可运行Java代码的假想计算机。只要根据JVM规格描述将解释器移植到特定的计算机上,就能保证经过编译的任何Java代码能够在该系统上运行。
2:Java虚拟机是一个想象中的机器,在实际的计算机上通过软件模拟来实现。Java虚拟机有自己想象中的硬件,如处理器、堆栈、寄存器等,还具有相应的指令系统。
3:Java虚拟机规范定义了一个抽象的——而非实际的——机器或处理器。这个规范描述了一个指令集,一组寄存器,一个堆栈,一个“垃圾堆”,和一个方法区。一旦一个Java虚拟机在给定的平台上运行,任何Java程序(编译之后的程序,称作字节码)都能在这个平台上运行。Java虚拟机(JVM)可以以一次一条指令的方式来解释字节码(把它映射到实际的处理器指令),或者字节码也可以由实际处理器中称作just-in-time的编译器进行进一步的编译。

面试题 8 . 解释静态变量在什么时候加载?编译期还是运行期?

静态变量是类级别的变量,在Java中,它们随类一起加载。关于静态变量何时加载的问题,我们可以这样解释:
静态变量在运行期被加载和初始化。具体来说,它们是在类被Java虚拟机加载到内存中后、类被第一次引用时初始化的。这个过程是在运行时发生的,而非编译期。
完整的Java对象创建过程如下:

让我详细解释一下这个过程:

  1. 类加载:首先,当Java程序运行时,Java虚拟机(JVM)通过类加载器加载包含静态变量的类。这一步涉及到查找字节码并从类的全路径名生成一个Class对象。
  2. 链接:加载后,类进入链接阶段,这个阶段验证类中的字节码,为静态变量分配内存,并且可能将变量设置为默认值,如null、0或false。
  3. 初始化:在链接阶段之后,类的初始化阶段开始。在这个阶段,静态变量按照它们在代码中出现的顺序显式初始化,如果它们有初始化表达式的话。此外,如果有静态初始化块(static block),它也会在这个时候执行。

这个初始化过程确保在任何静态方法或静态变量被访问之前,所有的静态变量已经被设置为指定的初始值。因此,虽然类的结构和静态变量的声明在编译期被确定并存储在类的字节码中,实际上它们的加载和初始化则发生在运行期。这种机制确保了Java类和对象的初始化顺序得到正确的管理和执行。

面试题 9 . JVM自身会维护缓存吗?

在Java虚拟机(JVM)中,确实有几种机制可以被视为内部的“缓存”功能,这些功能主要用于提高程序执行的效率和性能。
以下是几个例子:

  1. 方法区(或元空间):虽然方法区本身不是传统意义上的缓存,但它存储了类的结构信息,包括运行时常量池、字段和方法数据,以及方法的代码。这可以看作是一种缓存机制,因为它避免了每次执行时重新加载和解析类文件。
  2. 字符串池(String Intern Pool):JVM为了优化字符串存储和重用,维护了一个特殊的字符串池。当使用字面量创建字符串时,JVM首先检查池中是否已存在相同的字符串。如果存在,就返回现有的字符串引用,这样可以节省内存并提高性能。
  3. 即时编译器(JIT)的代码缓存:JIT编译器在运行时将热点代码(即频繁执行的代码)编译成本地机器代码,以提高程序的执行速度。这些编译后的代码被存储在一个特定的缓存区域中,从而在后续的执行中可以直接使用,避免了重复的解释执行开销。
  4. 堆中的缓存结构:某些应用可能会在JVM堆内显式创建缓存(如应用级的数据缓存),用于存储经常访问的数据对象。虽然这不是JVM自身维护的缓存,但是它是在JVM的管理下进行的。
  5. Biased Locking:这是JVM用于优化对象锁的一种机制。通过偏向锁,JVM可以减少无竞争访问同步块时的锁开销,通过在对象头中缓存线程ID来实现。
  6. 类加载器缓存:类加载器在加载类时,会缓存类信息,这样相同的类在下次加载时可以直接使用已缓存的数据,加快了类加载的速度。

面试题 10 . 请详细列举 JVM有哪些参数,分别如何设置与调优 ?


对于8G内存,我们一般是分配4G内存给JVM,正常的JVM参数配置如下:

-Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8
  • 1

调优的话要具体场景具体分析,可以通过监控确认FullGC频率和次数,通过dump文件分析大对象等手段。
可以从代码层面优化或者是调整年轻代、老年代、幸存区的内存大小,来达到调优的目的

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]

面试题 11 .简述Java体系中的堆和栈的概念和运行原理 ?

在 Java 中,堆和栈分别表示两种不同的内存分配方式,各自用于存储不同类型的数据和具有不同的管理机制。

  • 概念:栈(Stack)是线程的运行内存,按照先进后出的原则存储局部变量、方法参数和调用信息。每个线程都有自己独立的栈内存,栈中的每一帧代表一次方法调用。
  • 运行原理
    • 方法调用时,栈中创建新的帧(stack frame)来保存局部变量、操作数栈和其他信息。
    • 方法执行完毕后,该方法对应的栈帧被弹出并释放内存。
    • 栈空间通常较小,主要用于存储基本数据类型、对象引用和局部变量。
    • 存取速度快,仅次于寄存器
    • 栈数据可以共享
  • 栈的特性
    • 栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:
      int a = 3;
      int b = 3;
      编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着 处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。
      这时,如果再令a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a 值的改变不会影响到b的值。
      要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利 于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。

  • 概念:堆(Heap)是 JVM 中用于存放对象和数组的共享内存区域,由所有线程共享。它的大小一般较大,用于动态分配内存。
  • 运行原理
    • 对象在堆中动态分配,由垃圾收集器负责管理其生命周期。
    • JVM 的垃圾回收机制定期扫描堆,释放无用对象的内存。
    • 根据不同的垃圾回收算法,堆可以划分为不同区域,如新生代、老年代和永久代(或元空间)。
    • 存取速度慢(由于需要在运行时动态分配内存)

总结

  • 栈用于方法调用过程中对局部变量和方法信息的临时存储,效率高但容量有限。
  • 堆用于存放动态分配的对象和数组,并由垃圾回收器管理内存释放。

面试题 12 .64 位 JVM 中,int 的长度是多数?

在 64 位 JVM 中,int 数据类型的长度依然是 32 位(4 字节)。这在 Java 语言规范中有明确规定,不受运行环境的 32 位或 64 位体系结构的影响。
Java 基础数据类型的长度是固定的,独立于硬件架构:

  • byte: 8 位(1 字节)
  • short: 16 位(2 字节)
  • int: 32 位(4 字节)
  • long: 64 位(8 字节)
  • char: 16 位(2 字节)
  • float: 32 位(4 字节)
  • double: 64 位(8 字节)

因此,在 64 位 JVM 中,int 仍然保持 32 位(4 字节)。

面试题 13 .Serial 与 Parallel GC之间的不同之处?

在 Java 虚拟机 (JVM) 中,Serial 垃圾收集器和 Parallel 垃圾收集器是两种不同的垃圾收集策略,它们的主要区别在于并发性和目标使用场景。

Serial 垃圾收集器

新生代采用复制算法,老年代采用标记-整理算法
serial.jpeg

  • 工作方式
    • 使用单线程的方式进行垃圾收集。
    • 在执行垃圾收集时会暂停所有应用线程(“Stop-the-World”)。
    • 对于新生代,采用标记-复制(mark-copy)算法;对于老年代,采用标记-整理(mark-compact)算法。
  • 优点
    • 实现简单且容易调试。
    • 对于单核/双核机器、低内存应用等小型应用的性能较好。
  • 缺点
    • 因为是单线程,所以垃圾收集期间应用会完全暂停,对于多核/高并发的应用效率较低。

Parallel 垃圾收集器

新生代采用复制算法,老年代采用标记-整理算法
parallel.jpeg

  • 工作方式
    • 使用多线程并行执行垃圾收集工作。
    • 新生代和老年代都能并行处理,使用多线程标记、复制和整理对象。
    • 也需要在执行垃圾收集时暂停应用线程(“Stop-the-World”)。
  • 优点
    • 在多核机器上可以充分利用多核能力,加快垃圾收集速度。
    • 可通过设置线程数进行调优。
  • 缺点
    • 并行算法增加了一定的复杂性。
    • 在应用负载较低或是内核较少的情况下,性能提升不明显。

总结

  • 并发:Serial GC 适用于单线程执行垃圾收集,而 Parallel GC 能够利用多线程并行执行。
  • 适用场景:Serial GC 适合内存小、核心数少的应用;Parallel GC 更适合多核、高内存的应用场景。

面试题 14 .简述 JVM 选项 -XX:+UseCompressedOops 有什么作用?

JVM 选项 -XX:+UseCompressedOops 主要用于 64 位 JVM 上的堆内存优化,简称为“压缩 OOP(普通对象指针)”

背景

  • 在 64 位 JVM 中,指针的长度为 64 位(8 字节),这导致对象引用的大小也为 8 字节,比 32 位 JVM 中的 4 字节增加了一倍。
  • 对象指针的增加会使得堆内存中的对象大小增加,导致对内存的使用效率降低。

64为虚拟机对象头如下:
clipboard.png

压缩 OOP

  • -XX:+UseCompressedOops 选项会启用压缩的普通对象指针(OOPs),将对象引用压缩为 32 位。
  • 通过在堆内存地址中添加偏移量和对齐方式,实现了 32 位的对象引用可以映射到更大的地址空间,从而在不影响指针计算性能的情况下压缩指针大小。

作用

  • 内存占用减少:由于每个对象引用占用的内存减少了一半,启用压缩 OOP 后,堆内存利用率明显提高。
  • 性能提升:内存占用的降低可使更多对象保持在缓存中,从而降低内存访问延迟,提高程序运行效率。

适用场景

  • 一般适用于大多数 64 位 JVM 上的应用程序,特别是当应用程序需要分配大量对象并且堆大小相对较大时。

面试题 15 .怎样通过 Java 程序来判断 JVM 是 32 位 还是 64 位?

在 Java 中,可以通过检查系统属性 os.arch 来确定 JVM 是 32 位还是 64 位。该属性表示操作系统的体系结构。一般来说,如果属性值包含“64”,则表示是 64 位 JVM,否则是 32 位。
例如:

public class JVMArchitecture {
    public static void main(String[] args) {
        String architecture = System.getProperty("os.arch");
        System.out.println("JVM Architecture: " + architecture);

        if (architecture.contains("64")) {
            System.out.println("This is a 64-bit JVM.");
        } else {
            System.out.println("This is a 32-bit JVM.");
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

说明:

  1. System.getProperty(“os.arch”) 获取当前操作系统架构的系统属性。
  2. 判断返回值中是否包含“64”来区分 32 位或 64 位。
  3. 注意该属性值还可能与操作系统的体系结构有关,在虚拟机明确为 64 位的情况下,仍可能返回“32”或其他值。

面试题 16 .32 位 JVM 和 64 位 JVM 的最大堆内存分别是多少?

在 Java 中,JVM 最大堆内存的限制取决于虚拟机架构(32 位或 64 位)、操作系统和硬件资源
一般情况下,32 位和 64 位 JVM 的最大堆内存如下:

32 位 JVM

  • 由于 32 位地址空间的限制,理论上最大堆内存为 4GB。
  • 实际上,由于系统库、栈和其他内存占用,32 位 JVM 通常只能使用 1.5GB 到 3GB 左右的堆内存,具体取决于操作系统和 JVM 的实现。

64 位 JVM

  • 64 位架构的地址空间理论上可支持数百 TB 的内存。
  • 实际最大堆大小主要由物理内存和操作系统的限制决定。
  • 通常来说,64 位 JVM 最大堆内存可以设置到数十 GB 或更高。

总结

  • 32 位 JVM 由于地址空间的限制,最大堆内存较小,适用于内存需求较低的应用。
  • 64 位 JVM 允许更大的堆内存,适合内存需求较大的应用。

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]

面试题 17 .解释 Java 堆空间及 GC?

Java 堆空间(Heap)是 JVM 的一部分内存区域,用于存储由程序动态分配的所有对象。
它在 JVM 启动时创建,所有线程共享。垃圾收集器(GC)会在堆空间中自动管理内存,定期清理不再使用的对象。

Java 堆空间

image.png

  1. 新生代(Young Generation)
    • 新生代存储新创建的对象。
    • 通常细分为三个区域:伊甸园区(Eden Space)和两个幸存者区(Survivor Space)。
    • 对象在伊甸园区创建,并在达到某些条件后移动到幸存者区。
  2. 老年代(Old Generation)
    • 老年代存储生命周期较长的对象。
    • 新生代经过多次垃圾收集后幸存下来的对象会被晋升到老年代。
  3. 永久代/元空间(Permanent Generation / Metaspace)
    • 在早期版本的 Java 中,永久代存储类元数据。
    • 从 Java 8 开始,永久代被移除并替换为元空间,存储在 JVM 本地内存中而不是堆中。

image.png

Java 垃圾收集(GC)

垃圾收集器是负责自动管理堆空间的内存分配和回收的组件。它的目标是移除不再被使用的对象,释放内存以供其他对象使用。常见的垃圾收集算法和收集器包括:
垃圾收集算法.png

  1. 标记-清除算法
    • 首先标记不再使用的对象,然后清除它们。
    • 可能导致堆空间碎片化。

标记清除.jpeg

  1. 标记-整理算法
    • 在标记阶段之后,将存活的对象整理到堆的一端,再清除剩余的无用空间。

标记整理.jpeg

  1. 标记-复制算法
    • 将新生代分为两个区域,存活的对象从一个区域复制到另一个,原始区域被清空

标记复制.jpeg

  1. 垃圾收集器

垃圾收集器.png

  • Serial GC:单线程,适用于较小的应用。
  • Parallel GC:多线程,适用于多核机器。
  • CMS(Concurrent Mark-Sweep)GC:低停顿时间,适合交互式应用。
  • G1(Garbage-First)GC:适用于大型堆和高性能应用,替代 CMS。
  • **ZGC:**JDK11引入的一款新的垃圾收集器,旨在提供较低的停顿时间,同时尽可能提升垃圾收集效率

面试题 18 .解释能保证 GC 执行吗?

在 Java 中,不能绝对保证垃圾收集(GC)在特定时间执行。
垃圾收集的行为由 JVM 控制,并根据具体的内存需求和配置参数自动决定何时触发。

原因和解释:

  1. 自动管理
    • Java 使用自动内存管理系统,开发者不需要手动释放内存。
    • 垃圾收集器在 JVM 认为合适的时机才会启动。通常,当堆内存不足或达到某个阈值时,GC 就会执行。
  2. 多种垃圾收集器
    • 不同的垃圾收集器有不同的策略和算法,会影响垃圾收集的时机和频率。例如,Serial GC、Parallel GC、CMS、G1 和 ZGC 等都在其特定情况下会以不同方式工作。
  3. System.gc() 提示
    • System.gc()Runtime.getRuntime().gc() 提供一种“建议性”调用,提示 JVM 执行 GC。
    • 但是这只是建议,JVM 有权忽略它,具体是否执行取决于垃圾收集器的策略和当前内存状态。

虽然我们可以通过配置或提示来增加垃圾收集的机会,但无法严格控制它在特定时间执行。
JVM 会根据应用程序的运行状况和当前的内存使用情况来自动决定最佳的垃圾收集时间。

面试题 19 .怎么获取 Java 程序使用的内存?堆使用的百分比?

在 Java 中,可以使用 Runtime 类或 Java Management Extensions (JMX) 来获取 Java 程序使用的内存和堆内存使用的百分比。

通过 Runtime

Runtime 类提供了获取 JVM 内存使用信息的方法
例如:

public class MemoryUsageExample {
    public static void main(String[] args) {
        // 获取当前 JVM 的运行时实例
        Runtime runtime = Runtime.getRuntime();

        // 获取最大可用内存
        long maxMemory = runtime.maxMemory();
        // 获取当前已分配的总内存
        long totalMemory = runtime.totalMemory();
        // 获取当前空闲内存
        long freeMemory = runtime.freeMemory();
        // 计算已使用的内存
        long usedMemory = totalMemory - freeMemory;

        // 打印信息
        System.out.printf("最大堆内存: %.2f MB%n", maxMemory / (1024.0 * 1024));
        System.out.printf("已分配的总堆内存: %.2f MB%n", totalMemory / (1024.0 * 1024));
        System.out.printf("已使用的堆内存: %.2f MB%n", usedMemory / (1024.0 * 1024));
        System.out.printf("堆内存使用百分比: %.2f%%%n", (usedMemory * 100.0) / maxMemory);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

通过 JMX

Java Management Extensions (JMX) 提供了更高级的内存管理信息。可以使用 MemoryMXBean 获取详细的堆和非堆内存使用情况:

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;

public class JMXMemoryUsageExample {
    public static void main(String[] args) {
        // 获取 JVM 的内存管理 MXBean
        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
        
        // 获取堆内存的使用情况
        MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
        long maxHeapMemory = heapMemoryUsage.getMax();
        long usedHeapMemory = heapMemoryUsage.getUsed();

        // 打印堆内存信息
        System.out.printf("最大堆内存: %.2f MB%n", maxHeapMemory / (1024.0 * 1024));
        System.out.printf("已使用的堆内存: %.2f MB%n", usedHeapMemory / (1024.0 * 1024));
        System.out.printf("堆内存使用百分比: %.2f%%%n", (usedHeapMemory * 100.0) / maxHeapMemory);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

面试题 20 .简述JVM内存区域总共分为哪两种类型 ?

JVM 内存区域大致可以分为两种类型:堆(Heap)和非堆(Non-Heap)内存。
image.png

1. 堆内存(Heap Memory)

堆内存是 JVM 中的主要内存区域,用于存储 Java 对象实例。堆内存在 JVM 启动时分配,并由垃圾收集器(GC)管理。

  • 新生代(Young Generation)
    包含刚创建的对象。在经过一次或多次垃圾收集仍存活的对象会被移动到老年代。
  • 老年代(Old Generation)
    包含生命周期较长的对象,新生代晋升的对象最终存储在老年代。

2. 非堆内存(Non-Heap Memory)

非堆内存是 JVM 使用的其他内存区域,包括:

  • 方法区(Method Area)
    存储类结构、方法、常量池等信息,是堆外的逻辑部分。方法区在 Java 8 以后被包含在元空间中。
  • 栈(Java Stack)
    每个线程的私有内存区域,存储线程运行时的栈帧,包括局部变量和方法调用信息。
  • 本地方法栈(Native Method Stack)
    用于执行本地方法,存储本地方法的调用状态。
  • 程序计数器(PC Register)
    每个线程的私有区域,存储当前正在执行的指令地址。

总结起来,JVM 内存区域可划分为堆和非堆两种类型,并进一步细分为不同的区域,用于高效地存储和管理应用程序的不同类型数据。

面试题 21 .简述JVM的关键名词 ?

以下是一些主要的 JVM 关键名词及其解释:

  1. 类加载器(Class Loader)
    • 用于加载 Java 类文件到 JVM 中,并为每个类创建对应的 Class 对象。
    • 常见的类加载器有引导类加载器、系统类加载器和自定义类加载器。
  2. 堆内存(Heap Memory)
    • JVM 中用于存储 Java 对象的内存区域。
    • 包括新生代和老年代。
  3. 新生代(Young Generation)
    • 堆内存的一部分,存放新创建的对象,包含伊甸园区和两个幸存者区。
    • 对象在这里经常被垃圾收集器回收。
  4. 老年代(Old Generation)
    • 堆内存的一部分,存放生命周期较长的对象。
    • 从新生代晋升的对象存放在这里。
  5. 元空间(Metaspace)
    • 用于存储类的元数据,包括类的结构、字段、方法等。
    • 元空间在 Java 8 及以后版本中取代永久代,使用本地内存。
  6. 垃圾收集器(Garbage Collector, GC)
    • 自动管理内存,负责发现和回收不再使用的对象。
    • 常见的垃圾收集器包括 Serial、Parallel、CMS、G1 和 ZGC。
  7. 栈(Java Stack)
    • 每个线程的私有内存区域,存储线程运行时的栈帧,包括局部变量、操作数栈和方法调用信息。
  8. 程序计数器(PC Register)
    • 每个线程的私有区域,存储当前正在执行的指令地址。
  9. 本地方法栈(Native Method Stack)
    • 用于执行本地方法的栈空间,通常用于 JNI(Java Native Interface)。
  10. 即时编译器(Just-In-Time Compiler, JIT)
  • 在运行时将字节码编译为机器码,以提高执行性能。
  1. 逃逸分析(Escape Analysis)
  • 优化技术,用于分析对象的生命周期和作用域,以决定是否将对象分配在栈上而非堆上。

面试题 22 .解释JVM运行时内存分配 ?

JVM 运行时内存主要划分为多个区域,每个区域用于不同类型的数据和任务。
JVM内存模型.png

1. 堆(Heap)

  • 用途
    用于存储由程序动态分配的所有对象实例,在 JVM 启动时创建,是垃圾收集器(GC)主要管理的区域。
  • 划分
    • 新生代:用于存储新创建的对象,细分为以下区域:
      • 伊甸园区(Eden Space):大多数新对象首先分配在这里。
      • 幸存者区(Survivor Space):有两个,分别命名为 S0 和 S1。伊甸园区中幸存的对象会被移动到这两个区域中。
    • 老年代:存放生命周期较长的对象,新生代中经过多次垃圾收集仍然存活的对象最终会晋升到老年代。
    • 元空间(Metaspace):在 Java 8 之后取代永久代,存储类的元数据。

2. 栈(Stack)

  • 用途: 每个线程有独立的栈内存区域,用于存储局部变量、方法调用信息和操作数栈。
  • 栈帧: 每次方法调用会生成一个栈帧来存储局部变量、操作数栈、返回地址等信息。方法执行完毕后,栈帧被弹出并释放。

3. 本地方法栈(Native Method Stack)

  • 用途: 用于执行本地方法,通常用于 Java Native Interface(JNI)和与操作系统交互的任务。

4. 程序计数器(PC Register)

  • 用途: 存储每个线程当前正在执行的指令地址。因为每个线程都独立执行,所以它们各自拥有独立的程序计数器。

5. 其他区域

  • 直接内存(Direct Memory): 非堆内存,直接由操作系统管理,NIO 库使用它来实现快速的 I/O 操作。
  • 代码缓存(Code Cache): 存储即时编译器(JIT)生成的本地代码。

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]

面试题 24 .简述如何确定当前对象是垃圾 ?

在Java中,判断一个对象是否可以被视为垃圾,并由垃圾收集器(GC)回收,主要通过可达性分析(Reachability Analysis)来完成。这个方法不依赖于对象的引用计数,而是看对象是否可以从一组称为“GC Roots”的对象访问到。

GC Roots 包括:

  • 在虚拟机栈(Stack Frames)中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

垃圾判定的过程:

  1. 标记阶段:从所有的GC Roots出发,遍历引用链。所有被访问到的对象被标记为活动的,即不是垃圾。
  2. 清除阶段:所有未被标记的对象,将被GC回收。

假设有以下类结构和对象引用情况:

class A {
    B bInstance;
}

class B {
    int value;
}

public class TestGC {
    public static void main(String[] args) {
        A a = new A();
        a.bInstance = new B();
        // 情况1
        a = null;

        // 情况2
        // a.bInstance = null;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在上面的代码中:

情况1

当执行 a = null; 后,对象 A 的实例不再有任何栈帧中的引用指向它,变成了不可达状态。由于 A 的实例是唯一引用 B 的实例的对象,因此,B 的实例也会随着 A 的实例变成不可达。在下一次垃圾收集时,这两个对象都可能被回收。

情况2

如果只执行 a.bInstance = null;,则只有 B 的实例变成不可达状态,因为不再有引用指向它。A 的实例仍然由局部变量 a 引用,因此它仍然是可达的,不会被回收。
通过这种方式,Java的垃圾收集器可以确定哪些对象是“垃圾”并应当被回收,从而管理内存使用,防止内存泄漏。

面试题 25 .解释GCrooot 包括哪些?

GC Roots 包括:

  1. 虚拟机栈中的引用变量
    • 这包括了所有的本地变量表中的引用变量。例如,所有在方法中定义的局部变量,它们直接存储在每个线程自己的栈帧中。
  2. 方法区中的类静态属性引用的对象
    • 静态属性归整个类所有,存储在方法区,因此静态属性引用的对象也是GC Roots。
  3. 方法区中的常量引用的对象
    • 常量值也可能引用对象,这些对象也会被视为GC Roots。
  4. 本地方法栈中JNI(即通常所说的本地方法)引用的对象
    • 本地方法栈中由JNI引用的对象也是GC Roots。

例如:

class MyClass {
    private static B staticObj = new B();  // 类静态属性引用的对象
    private int[] numbers = new int[10];   // 实例引用的对象

    public static void main(String[] args) {
        A localA = new A();                // 虚拟机栈中的引用变量
        int[] localNumbers = {1, 2, 3};    // 虚拟机栈中的引用变量
        localA.doSomething();
    }
}

class A {
    public void doSomething() {
        String localStr = "hello";         // 虚拟机栈中的引用变量
        System.out.println(localStr);
    }
}

class B {
    private long id;
    private String value;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在这个例子中:

  • localAlocalNumbers 是由 main 方法中的本地变量引用的对象,因此是GC Roots。
  • staticObj 是一个静态属性,它引用了一个 B 类型的对象,所以这个 B 类的对象也是GC Roots。
  • localStrdoSomething 方法中被定义为局部变量,因此在该方法执行期间也是GC Roots。

通过这些GC Roots作为起点,垃圾收集器可以遍历所有可达的对象。不可达的对象,即那些从任何GC Roots都无法到达的对象,被视为垃圾,可能会在垃圾收集过程中被回收。这种方法确保了只有真正不再被使用的内存会被清理。

面试题 26 .简述JVM对象头包含哪些部分 ?

在Java虚拟机(JVM)中,对象头(Object Header)是所有Java对象在内存中的重要组成部分。
对象头主要包含两个部分:标记字(Mark Word)和类型指针。对于数组类型的对象,还包括一个长度部分
对象头.png

1. 标记字(Mark Word)

标记字是对象头中用于存储对象自身的运行时数据的部分,其大小通常为32位或64位,具体取决于JVM的位数和内存模型。标记字包含了以下信息:

  • 哈希码:系统在调用**hashCode()**方法时生成的,用于确定该对象的哈希码。
  • 垃圾收集信息:如年龄、标记状态等,这些信息被垃圾收集器用来进行对象的回收处理。
  • 锁状态信息:包括锁的标记,表明对象处于被锁定状态、轻量级锁定状态还是无锁状态等。这是实现Java中的同步机制的关键。
  • 偏向线程ID:在启用偏向锁的情况下,记录偏向的线程ID,提升同一线程重复获取锁的性能。

2. 类型指针

类型指针指向它的类元数据(Class Metadata),即指向描述类(包括类的方法、字段等信息)的数据结构的指针,JVM通过这个指针来确定对象属于哪个类。

  • 类元数据的位置:这使得JVM能够获取有关对象类型的所有必要信息,如其方法表、字段数据等。

3. 数组长度(仅数组对象)

对于数组对象,对象头还包含一个长度部分,记录数组的长度。这是因为数组的长度不是固定的,而且Java中数组的长度在创建后不可变,所以存储长度信息对于数组操作非常重要。

面试题 27 .详细阐述GC算法有哪些 ?

垃圾收集算法.png

  1. 标记-清除算法
    • 首先标记不再使用的对象,然后清除它们。
    • 可能导致堆空间碎片化。

标记清除.jpeg

  1. 标记-整理算法
    • 在标记阶段之后,将存活的对象整理到堆的一端,再清除剩余的无用空间。

标记整理.jpeg

  1. 标记-复制算法
    • 将新生代分为两个区域,存活的对象从一个区域复制到另一个,原始区域被清空

标记复制.jpeg

面试题 28 .请简述JVM中类的加载机制 ?

类加载机制.png
1.类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
这个步骤有两个问题:
1.如何划分内存。
2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
划分内存的方法:

  • “指针碰撞”(Bump the Pointer)(默认用指针碰撞)

如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

  • “空闲列表”(Free List)

如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录

解决并发问题的方法:

  • CAS(compare and swap)

虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。

  • 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过**-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+UseTLAB**),-XX:TLABSize 指定TLAB大小。

3.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
5.执行方法
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。

面试题 29 .简述JVM常见的垃圾收集器 ?

  1. 垃圾收集器

垃圾收集器.png

1. Serial Collector

  • 类型:单线程收集器。
  • 算法:使用标记-清除-压缩(Mark-Sweep-Compact)算法。
  • 特点:简单高效(在单核处理器或较小的数据集上)。在进行垃圾收集时,需要暂停其他所有的工作线程(Stop-The-World)。
  • 适用场景:适合单核处理器或者小内存资源的应用。

2. Parallel Collector(也称为Throughput Collector)

  • 类型:并行的多线程收集器。
  • 算法:同样使用标记-清除-压缩算法。
  • 特点:多线程进行垃圾收集,主要目标是达到一个可接受的吞吐量(应用时间 vs 垃圾收集时间)。
  • 适用场景:适用于多核服务器,重视吞吐量及CPU资源的有效利用。

3. CMS(Concurrent Mark-Sweep)Collector

  • 类型:并发的垃圾收集器。
  • 算法:使用标记-清除算法。
  • 特点:最小化应用线程停顿的时间,实现垃圾收集的同时允许工作线程运行。
  • 适用场景:适用于需要低延迟的应用场景,如WEB服务器、交互式应用。

4. G1(Garbage-First)Collector

  • 类型:并发的垃圾收集器,使用不同的堆布局。
  • 算法:基于“区域”的内存布局,通过将堆分成多个区域(Region)来管理,使用一系列高效的算法优化标记-压缩过程。
  • 特点:提供了更细粒度的内存管理,可以明确控制停顿时间,以期达到高吞吐量。
  • 适用场景:适用于具有较大堆内存且需要可预测的停顿时间的应用。

5. ZGC(Z Garbage Collector)

  • 类型:可伸缩的低延迟垃圾收集器。
  • 算法:基于染色指针和读屏障技术,可以实现几乎所有的垃圾收集活动都与应用线程并发执行。
  • 特点:目标是支持高吞吐量并保持低延迟,可以处理多TB的堆内存而停顿时间不超过10毫秒。
  • 适用场景:高性能服务器和需要极低停顿时间的应用。

6. Shenandoah

  • 类型:并发的垃圾收集器。
  • 算法:与ZGC类似,采用先进的并发算法来缩短GC的停顿时间。
  • 特点:它的主要目标是减少GC停顿时间,实现在收集过程中应用程序线程仍可运行。
  • 适用场景:适用于对响应时间有严格要求的应用,无论堆的大小。

面试题 30 .简述JVM 分代收集算法 ?

JVM的分代收集算法是基于一个核心观察:不同对象的生命周期不同。有些对象很快就变得不可达(如局部变量或临时对象),而有些则可能存活较长时间甚至贯穿整个应用程序的生命周期(如缓存数据或单例对象)。
根据这一观察,JVM的堆内存被分为几个不同的区域,以更有效地管理内存,从而优化垃圾收集性能。
主要包括以下几个部分:

1. 新生代(Young Generation)

  • 目的:大多数新创建的对象首先被分配到新生代。由于许多对象出生后不久即不可达,新生代通常会频繁地执行垃圾收集(也称为Minor GC)。
  • 组成:新生代通常分为三部分:一个Eden区和两个幸存者区(Survivor spaces),通常称为From和To。
  • 工作原理:对象最初在Eden区分配。Minor GC时,存活的对象从Eden区和一个Survivor区复制到另一个Survivor区,同时未被引用的对象将被清理。对象每经历一次复制,年龄增加,当达到一定年龄阈值后,如果仍然存活,它们将被晋升到老年代。

2. 老年代(Old Generation)

  • 目的:存放经过多次Minor GC仍然存活的对象。这些对象通常有更长的生命周期。
  • 工作原理:老年代的垃圾收集频率较低但通常更耗时,因为它包含的对象都是难以被回收的。老年代的垃圾收集通常称为Major GC或Full GC。

3. 永久代(PermGen)或元空间(Metaspace,Java 8引入)

  • 目的:用于存放JVM加载的类信息、常量以及静态资源,这部分区域与对象的生命周期不直接相关。
  • 变化:在Java 8中,永久代已被元空间替代,元空间使用本地内存,从而避免了固定大小的永久代带来的问题。

垃圾收集算法的应用:

  • 新生代:通常采用复制算法,因为它能快速清理掉大量的死亡对象。
  • 老年代:通常采用标记-清理(Mark-Sweep)或标记-压缩(Mark-Compact)算法,因为这些区域的对象较为稳定,复制算法的内存浪费率相对较高。

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]

面试题 31 .请列举JDK1.8 和 1.7做了哪些JVM优化 ?

JDK 1.8 相较于 JDK 1.7 在JVM方面进行了几项重要的优化和更新:
image.png

1. PermGen 到 Metaspace 的转变

  • JDK 1.7: 在JDK 1.7及以前,JVM使用PermGen(永久代)来存储类的元数据。
  • JDK 1.8: 引入了Metaspace(元空间),用于替代永久代。Metaspace不在虚拟机内存而是使用本地内存,这样做的好处包括避免永久代容易发生的内存溢出错误,和动态地扩展空间以适应不同应用的需要。

2. 垃圾收集器的优化

  • JDK 1.7: 包含了多种垃圾收集器,如Parallel GC, CMS, Serial GC等。
  • JDK 1.8: 加入了几项垃圾收集的优化,比如G1垃圾收集器的改进。G1成为更多推荐的替代老年代垃圾收集器,特别是在具有大内存、多核服务器的环境下。G1垃圾收集器旨在降低Full GC的停顿时间,并提高性能。

3. JVM参数优化

  • JDK 1.7JDK 1.8: JDK 1.8提供了更多的调优参数供开发者和运维人员调整应用性能,例如可以更细粒度地控制Metaspace的增长和回收。

4. String 常量池的改进

  • JDK 1.7: String常量池已从方法区移到堆中,这是JDK 1.7的变更。
  • JDK 1.8: 继续优化堆中的String常量池,减少重复字符串的内存占用,提高了字符串操作的性能。

5. JIT编译器的增强

  • JDK 1.8: 引入了基于调用的动态编译(profile-guided optimization),使得JIT编译器可以根据程序运行时的实际性能数据进行优化,进一步提高执行效率。

6. 其他内部优化

  • JDK 1.8 加强了类加载机制的性能,改善了锁机制的实现,包括对synchronized锁的优化,使得Java程序在多线程环境下运行得更快。

面试题 32 .内存泄漏和内存溢出有什么区别 ?

内存泄漏(Memory Leak)

内存泄漏是指程序在申请内存后,无法适时释放已不再使用的内存。这导致了无用内存的累积,可能最终耗尽系统资源,影响程序或系统的性能。长期运行的系统如果发生内存泄漏,可能导致内存资源枯竭,甚至导致系统崩溃。
例如: 在Java中,内存泄漏常见的情况是集合类持有对象引用而未能释放:

import java.util.*;

public class MemoryLeak {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        while (true) {
            Object o = new Object();
            list.add(o);
            o = null;  // 即使设置为null,集合中仍然持有对象引用
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在这个示例中,虽然对象o被设置为null,意图释放引用,但由于ArrayListlist仍然持有所有创建的对象引用,这些对象不能被垃圾回收,从而导致内存泄漏。

内存溢出(Memory Overflow or Out of Memory)

内存溢出是指程序在运行过程中,因为需求的内存超过了系统可供分配的最大内存,导致申请新内存失败。这通常会引发错误或异常,如Java中的OutOfMemoryError
例如: 在Java中,内存溢出的一个常见例子是请求的数据量超过了JVM允许的堆大小限制:

public class OutOfMemory {
    public static void main(String[] args) {
        int[] bigArray = new int[Integer.MAX_VALUE];  // 尝试分配巨大的数组空间
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这段代码尝试创建一个非常大的数组,如果JVM的最大堆内存设置不足以存储这么多数据,将抛出OutOfMemoryError

区别总结

  • 内存泄漏:程序不再需要的内存没有被释放。如果泄漏持续发生,可能最终导致内存溢出。
  • 内存溢出:程序试图使用超出系统可供的内存。通常是由于一次性或累计请求过多内存造成。

面试题 33 .简述JVM内存泄漏的分类(按发生方式来分类)?

内存泄漏的发生方式可以按照其产生的原因和上下文进行分类。下面是几种常见的内存泄漏分类及其示例:

1. 常发性内存泄漏

发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏
例如静态字段引用
静态字段的生命周期跟类的生命周期一样长,因此静态字段如果持续持有对象引用,那么这些对象也不会被回收。

public class StaticFieldLeak {
    private static List<Object> objects = new ArrayList<>();

    public void add(Object object) {
        objects.add(object);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这个例子中,objects是一个静态字段,它会持续持有所有加入的对象,除非显式清空或者程序结束。

2. 偶发性内存泄漏

发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的
例如集合对象中的内存泄漏
如果对象被存储在集合中,并且在不需要时没有被移除,这些对象会一直保留在集合中,从而导致内存泄漏。

public class CollectionLeak {
    private List<Object> cacheData = new ArrayList<>();

    public void processData() {
        Object data = new Object();
        cacheData.add(data);
        // 在适当的时候忘记从cacheData中移除data
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这个示例中,虽然data只在processData方法中需要,但加入到cacheData后,如果没有适当的移除机制,data会一直占用内存。

3. 监听器和回调

如果注册了监听器或回调而未适当取消注册,那么这些监听器或回调可能会持续存在,导致所有相关对象无法被垃圾回收。
示例

public class ListenerLeak {
    private final List<EventListener> listeners = new ArrayList<>();

    public void addListener(EventListener listener) {
        listeners.add(listener);
    }
    // 忘记提供一个移除监听器的方法
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

如果ListenerLeak的实例不断添加监听器而不提供移除它们的方式,那么这些监听器及其相关的对象都不能被回收。

4. 内部类和匿名内部类的引用

非静态内部类和匿名内部类会隐式持有对其外部类实例的引用。如果内部类的实例被长时间持有,那么外部类的实例也不能被回收。
示例

public class OuterClass {
    private int size;
    private final String name;

    public OuterClass(String name) {
        this.name = name;
    }

    public Runnable createRunnable() {
        return new Runnable() {
            public void run() {
                System.out.println(name);
            }
        };
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在这个例子中,如果Runnable对象被长时间持有(例如,被提交到一个长期运行的线程中),那么由于Runnable隐式地持有对OuterClass实例的引用,OuterClass的实例也会一直保持在内存中。

面试题 34 .简述JVM内存溢出的原因及解决方法 ?

1**、内存溢出的可能原因 **

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

  3. 代码中存在死循环或循环产生过多重复的对象实体;

  4. 使用的第三方软件中的BUG;

  5. 启动参数内存值设定的过小
    **2、内存溢出的解决方案: **
    第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
    第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
    第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
    **重点排查以下几点: **

  6. 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。

  7. 检查代码中是否有死循环或递归调用。

  8. 检查是否有大循环重复产生新对象实体。

  9. 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。

  10. 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。

面试题 35 .请简述JVM中栈上分配和内存逃逸解释 ?

在Java虚拟机(JVM)中,栈上分配和内存逃逸是两个与对象分配位置和性能优化相关的重要概念。
它们与JVM如何处理对象的内存分配和回收有关。

栈上分配(Stack Allocation)

栈上分配是指在JVM中将对象分配在调用线程的栈内存上,而不是在堆上。这样做的主要优势是提高内存分配的效率和降低垃圾收集的开销,因为栈内存能够随着方法调用的结束而自动清理,不需要单独的垃圾回收过程。
特点

  • 高效:栈上分配通常比堆分配更快,因为栈是线程私有的,分配仅仅是移动栈指针,不涉及额外的同步开销。
  • 自动内存管理:随着方法的结束,栈帧被弹出,所有栈上的对象也随之被回收,这消除了垃圾收集的需要。

在Java中,栈上分配并不是默认行为,但JVM的即时编译器(JIT)可以通过逃逸分析技术来实现条件性的栈上分配。

内存逃逸(Escape Analysis)

内存逃逸分析是一种编译时优化技术,用于确定对象的作用域和生命周期,从而决定是否可以将对象分配在栈上。如果一个对象在方法外部没有被引用,它就被认为是“不逃逸”的,可以在栈上安全地分配。
如何工作

  • 编译器在编译阶段分析对象的使用范围和引用方式。
  • 如果确定对象只在创建它的方法中被使用,并且不会被返回或赋给其他变量,那么这个对象就被视为“不逃逸”。
  • 对于不逃逸的对象,编译器可以选择在栈上分配内存,避免堆分配,从而提高性能。

例如

public class EscapeAnalysis {
    public static void main(String[] args) {
        Point p = new Point(10, 20);
        System.out.println("Point: (" + p.x + ", " + p.y + ")");
    }
    static class Point {
        int x, y;
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在上面的代码中,Point 对象在 main 方法中创建并立即使用。这个对象没有逃逸出 main 方法的范围,因此理论上可以在栈上分配。

1. 利用分析工具

VisualVM
  • 功能:VisualVM是一个可视化工具,它可以监控、分析和对Java应用程序的运行时行为进行故障排除。它能够提供堆转储(Heap Dump),帮助识别内存泄漏。
  • 操作:运行应用,使用VisualVM连接到对应的Java进程,监控内存消耗,并在必要时生成堆转储文件。

Eclipse Memory Analyzer (MAT)
  • 功能:MAT是一个强大的Java堆分析工具,用于分析堆转储文件,查找内存泄漏,查看内存消耗的对象。
  • 操作:通过VisualVM或其他方式获取堆转储文件,然后用MAT打开分析,MAT会提供可能的内存泄漏点。

2. 监控运行时内存使用情况

使用JConsole或JVisualVM等JMX工具实时监控Java应用的内存使用情况。这些工具可以帮助开发者观察到内存使用随时间增长的趋势。如果发现内存持续上升且不下降,那么很可能存在内存泄漏。

3. 代码审查

定期进行代码审查也是发现内存泄漏的一种有效方法。特别关注以下方面:

  • 长生命周期的对象持有短生命周期对象的引用
  • 集合类(如List, Map, Set等)中的对象是否被及时移除
  • 监听器和其他回调是否被正确移除

4. 利用分析API

在代码中使用分析API(如Java的Runtime类),定期打印自由内存和总内存的使用情况,可以帮助开发者了解应用的内存使用模式。

5. 单元测试与压力测试

  • 单元测试:通过单元测试检查内存的分配和释放是否符合预期。
  • 压力测试:通过模拟高负载情况来观察应用在压力下的内存表现,查看是否有异常的内存增长。

6. 使用编程技巧

  • 使用弱引用和软引用:对于不确定需要长时间存活的对象,可以考虑使用WeakReferenceSoftReference,这样可以在JVM内存不足时帮助垃圾回收器回收这些对象。

面试题 37 .请简述Minior GC、MajorGC与Full GC ?

Minor GC

  • 定义:Minor GC是指发生在新生代(Young Generation)的垃圾收集。
  • 触发原因:当新生代的内存不足时触发,这通常发生在应用频繁创建新对象时。
  • 过程:Minor GC会清理新生代,它使用复制算法,将存活的对象从一个Survivor空间移动到另一个Survivor空间或者晋升到老年代。未存活的对象简单地被丢弃,因此Minor GC一般比较快。
  • 频率:Minor GC的发生频率较高,因为新生代通常较小并且对象存活率低。

Major GC

  • 定义:Major GC是指清理老年代(Old Generation)的垃圾收集。
  • 触发原因:当老年代的内存不足时触发,这通常是因为对象从新生代晋升而导致老年代填满。
  • 过程:Major GC通常使用标记-清除或标记-压缩算法,涉及到整个老年代区域的清理。标记-压缩算法会移动存活的对象,以减少内存碎片。
  • 影响:Major GC比Minor GC耗时更长,因为涉及的数据量更大,并且可能需要移动对象来优化内存空间。

Full GC

  • 定义:Full GC是指对整个Java堆(新生代、老年代)以及方法区(或元空间)的完全垃圾收集。
  • 触发原因:Full GC可以由多种原因触发,包括但不限于:
    • 显式调用System.gc()
    • JVM内存分配失败,如老年代或方法区(元空间)空间不足;
    • JVM启动参数触发,例如进行某些特定的性能监测操作。
  • 过程:Full GC是最耗时的GC类型,涉及到整个堆以及方法区的清理。在Full GC期间,所有应用线程通常会被暂停,直到GC完成。
  • 频率:Full GC的发生频率通常较低,但取决于系统的内存压力和GC策略。

面试题 38 .解释什么是双亲委派机制?它有什么作用?

双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载
双亲委派机制.png

工作原理

  1. 加载请求:当一个类加载器接收到类加载的请求时,它首先不会自己直接去加载这个类。
  2. 委托父加载器:它会将这个加载任务委托给它的父加载器。
  3. 递归委托:这个过程会递归进行,即父加载器再向它的父加载器委托,一直到达顶层的启动类加载器(Bootstrap ClassLoader)。
  4. 尝试加载:如果最顶层的类加载器无法完成这个加载任务(不认识这个类),则委托链会逐级反向,每个加载器都会尝试加载这个类。
  5. 成功加载:一旦类被加载,就不会继续向下传递请求。如果所有的加载器都不能加载这个类,则抛出ClassNotFoundException

作用与优势

1. 避免类的重复加载:由于在顶层类加载器已经加载的类不会被子加载器再次加载,因此保证了同一个类在JVM中的唯一性。
2. 安全性:防止核心API被随意篡改。例如,用户不能定义一个称为java.lang.Object的类,因为启动类加载器会首先加载Java的核心类,这样通过双亲委派机制就保证了JVM核心API的类型安全。
3. 稳定性:系统类优先加载,这保证了Java核心库的类不会被自定义的类所替代,系统的稳定性得以提高。
我们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:

  1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
  3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。
//ClassLoader的loadClass方法,里面实现了双亲委派机制
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 检查当前类加载器是否已经加载了该类
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {  //如果当前加载器父加载器不为空则委托父加载器加载该类
                    c = parent.loadClass(name, false);
                } else {  //如果当前加载器父加载器为空则委托引导类加载器加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {  //不会执行
            resolveClass(c);
        }
        return c;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

为什么要设计双亲委派机制?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

面试题 39 .设置堆空间的最大值(-Xmx)应该要考虑哪些因素?

确定适合服务器配置的 -Xmx(Java堆内存的最大值)需要考虑服务器的核心数和总内存,以及其他因素,如其他应用的内存需求、操作系统占用、JVM内部需求(如元空间、代码缓存等)和具体应用的内存需求。

1. 4核8GB服务器

对于一个4核8GB的服务器:

  • 操作系统和其他应用保留:通常操作系统和其他基本服务会占用一部分内存。假设留给操作系统和其他应用大约2GB内存,这在许多情况下都是一个保守的估计。
  • 可用于JVM的内存:这样,大约有6GB的内存可以分配给JVM。
  • -Xmx设置:通常,可以将 -Xmx 设置为可用内存的70%-80%左右,以保证留有足够的内存给JVM以外的需求,这意味着大约设置为4GB到4.8GB之间。

2. 8核16GB服务器

对于一个8核16GB的服务器:

  • 操作系统和其他应用保留:如果运行的是更多的或者更大的应用,可能需要更多的内存保留给系统和其他程序。假设这次我们保留大约3GB给操作系统和其他应用。
  • 可用于JVM的内存:这将留下大约13GB的内存用于JVM。
  • -Xmx设置:同样使用70%-80%的原则,可以将 -Xmx 设置在9GB到10.4GB之间。

其他因素考虑:

  • JVM的其他内存需求:JVM还需要内存用于非堆区域如元空间(MetaSpace)、直接内存(Direct Memory)、JVM内部处理等。确保分配给 -Xmx 的内存之外,还有足够的内存满足这些需求。
  • 应用的内存需求:如果你对应用的内存需求有明确的了解,应该根据这些具体需求来调整 -Xmx 的值。
  • 并发和响应时间:高并发应用可能需要更多的堆内存来处理大量的用户请求,同时保证良好的响应时间。

实践建议:

在确定 -Xmx 值时,理论和实践可能会有所不同。建议在生产环境中进行监控和调整。可以开始于一个较低的设置,根据实际运行中的内存使用情况(通过JVM监控工具)逐步调整,直到找到最佳平衡点。此外,对于不同类型的应用,其内存消耗模式可能会有很大差异,因此应该针对具体应用进行调整和优化。

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]

面试题 40 .Java8默认使用的垃圾收集器是什么?

Java8默认的垃圾收集器是Parallel Garbage Collector,也称为Parallel GC

Parallel Garbage Collector

Parallel GC 是一种使用多个线程进行垃圾收集的收集器,主要目的是增加吞吐量。它在新生代使用并行的标记-复制(Mark-Copy)算法,在老年代使用并行的标记-清除-整理(Mark-Sweep-Compact)算法。该垃圾收集器在处理大量数据且拥有多核处理器的服务器环境中表现尤为出色,因为它能充分利用多核硬件的优势。

特点和考虑

  • 优点:高吞吐量,适用于需要处理大量任务或计算的多核服务器。
  • 缺点:在进行垃圾收集时会引发较长的停顿时间(Stop-The-World),这可能对实时性或交互性有较高要求的应用造成影响。

其他常见的Java 8可用的垃圾收集器:

  • CMS (Concurrent Mark Sweep):目标是减少在垃圾收集时产生的停顿时间。CMS收集器在回收内存时使用的是并发的标记-清除算法,适用于互联网或服务端应用,这些应用通常优先考虑较短的响应时间。
  • G1 (Garbage-First):从Java 7 Update 4开始提供,到Java 9时成为默认垃圾收集器。G1收集器旨在为具有大内存空间的多核机器提供高吞吐量和低延迟,它通过将堆划分为多个区域来管理整个GC过程,并且能够优先处理那些最可能回收大量空间的区域。

切换垃圾收集器

在Java 8中,虽然默认使用Parallel GC,但可以通过JVM启动参数来切换使用不同的垃圾收集器。例如:

  • 使用CMS收集器:-XX:+UseConcMarkSweepGC
  • 使用G1收集器:-XX:+UseG1GC

面试题 41 .请简述什么是并行垃圾收集?

并行垃圾收集,是指使⽤多个GC worker 线程并行地执行垃圾收集,能充分利用多核CPU的能力,缩短垃圾收集的暂停时间。
工作流程如下:
parallel.jpeg
除了单线程的GC,其他的垃圾收集器,比如 PS,CMS, G1等新的垃圾收集器都使用了多个线程来并行执行GC⼯作

面试题 42 .说一下什么是STW?什么是安全点,什么是安全区域 ?

在Java虚拟机(JVM)的垃圾收集过程中,“Stop-The-World”(STW)是一个非常重要的概念,它涉及到垃圾收集时对应用程序的影响。
此外,“安全点”(Safepoint)和"安全区域"(Safe Region)是与STW密切相关的机制,用于确保在执行垃圾收集时,应用程序的状态是一致且可预测的。

Stop-The-World (STW)

STW是指在执行垃圾收集过程中,JVM会暂停应用程序的所有线程(除了垃圾收集线程),确保在垃圾收集执行期间不会有任何线程对堆进行修改,从而使得垃圾收集器能在一个一致的内存快照上工作。

STW事件可以由任何类型的垃圾收集引起,无论是Minor GC、Major GC还是Full GC。

影响: STW事件会导致应用程序的暂时性停顿。这些停顿的时间长度取决于堆的大小、堆中对象的数量以及垃圾收集器的类型和配置。STW是影响高性能应用实时响应能力的主要因素之一。

安全点 (Safepoint)

安全点是程序执行中的特定位置,JVM只能在这些点上暂停线程,进行垃圾收集。这些点通常在执行过程中不会改变堆内存的操作,如方法调用、循环迭代、异常跳转等。
选择安全点的原因: 选择这些点是因为在这些位置上,所有对象的引用关系等都处于一种易于管理的、一致的状态。这使得垃圾收集器可以准确地确定对象的可达性,从而正确地进行垃圾回收。
实现: 当JVM发出STW请求时,所有线程将继续执行,直到它们达到最近的一个安全点。一旦所有线程都达到安全点,STW事件就会发生。

安全区域 (Safe Region)

安全区域是在程序执行的某个阶段中,线程处于一种“不会引用堆中对象”的状态,或者说所有引用关系在这段时间内不会发生变化。当线程处于这样的安全区域时,即使它没有达到安全点,垃圾收集也可以安全地执行。
应用场景: 安全区域通常用于处理那些不能立即响应STW请求的线程状态,例如线程处于阻塞状态或执行非常长的操作无法达到安全点。

面试题 43 .请解释CMS、G1垃圾回收器中的三色标记 ?

三色标记概述

在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。漏标的问题主要引入了三色标记算法来解决。
三色标记算法是把Gc roots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色:

  • 黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
  • 灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
  • 白色: 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。

三色标记.png

垃圾收集过程中的三色标记应用

CMS 垃圾回收器

CMS是一种以获取最短停顿时间为目标的收集器,使用标记-清除算法。其过程可以概括为:

  1. 初始标记(STW):标记所有从GC根直接可达的对象。这一阶段会很快,但需要停顿所有应用线程(STW)。
  2. 并发标记:从初始标记的对象开始,并发地(与应用线程一同运行)遍历所有可达对象。这里应用了三色标记,以确保在应用继续运行的情况下正确标记所有存活的对象。
  3. 重新标记(STW):修正在并发标记期间因应用程序继续运作而产生的变动。这通常比初始标记慢,但比完整堆扫描快。
  4. 并发清除:清除未标记(仍为白色)的对象。
G1 垃圾回收器

G1收集器旨在提供一个更可预测的垃圾收集行为,适用于大堆内存的系统,它通过划分内存为多个区域(Region)来管理内存,具体步骤包括:

  1. 初始标记(STW):标记所有从GC根直接可达的对象。
  2. 根区域扫描:处理任何与存活对象有引用的区域。
  3. 并发标记:在整个堆中,并发标记所有可达的对象,应用三色标记方法。
  4. 最终标记(STW):处理自并发标记以来的变更。
  5. 筛选回收:根据每个区域中的垃圾比例以及过去的收集性能,优先回收价值最大的区域。

三色标记的问题和解决策略

在并发GC环境下,三色标记可能会遇到“漏标”问题,即由于并发修改,一些本应标记的对象被遗漏。为了解决这个问题,CMS和G1都采用了各种策略(如增量更新、原始快照、读写屏障等),以确保在并发修改场景下的正确性。
三色标记算法是现代垃圾收集技术中的一个关键部分,它帮助GC算法在应用程序运行时高效、准确地识别和处理内存中的对象。

面试题 44 .请描述GC的Java四种引用 ?

image.png

1. 强引用(Strong Reference)

  • 描述:这是最常见的引用类型。如果一个对象具有强引用,那么它通常不会被垃圾回收器回收。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 例如
String strong = new String("I am a strong reference");
  • 1

2. 软引用(Soft Reference)

  • 描述:软引用是用来描述一些还有用但非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果回收后还没有足够的内存,才会抛出内存溢出异常。
  • 使用场景:适用于实现内存敏感的缓存。
  • 例如
import java.lang.ref.SoftReference;
SoftReference<String> soft = new SoftReference<>(new String("I am a soft reference"));
  • 1
  • 2

3. 弱引用(Weak Reference)

  • 描述:弱引用比软引用更弱,它只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存空间足够与否,都会回收掉只被弱引用关联的对象。
  • 使用场景:适用于实现没有特定约束的缓存。
  • 例如
import java.lang.ref.WeakReference;
WeakReference<String> weak = new WeakReference<>(new String("I am a weak reference"));
  • 1
  • 2

4. 虚引用(Phantom Reference)

  • 描述:虚引用完全不会对对象的生存时间构成影响,也无法通过虚引用来获取一个对象实例。为一个对象设置虚引用的唯一目的是能在这个对象被垃圾回收器回收时收到一个系统通知。
  • 使用场景:虚引用主要用来跟踪对象被垃圾回收的活动。
  • 例如
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> phantom = new PhantomReference<>(new String("I am a phantom reference"), queue);
  • 1
  • 2
  • 3
  • 4

面试题 45 .请解释 GC 回收机制 ?

Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的。
GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控
Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理
**GC的触发 **
GC的触发通常由以下几种情况:

  • 系统认为可用堆内存不足。
  • 程序员手动调用System.gc()。
  • JVM内部策略。

面试题 46 .简述JVM中程序计数器是什么?

在Java虚拟机(JVM)中,程序计数器(Program Counter, PC)是一个较小的内存区域,它是JVM中的一种运行时数据区。程序计数器为每个正在执行的线程保留一个独立的空间,因此它也被称为“线程私有”的内存。

功能和作用

程序计数器的主要功能是存储当前线程所执行的字节码的指令地址。如果正在执行的是一个Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,则程序计数器的值为undefined。这种机制确保了在多线程环境中线程切换后能恢复到正确的执行位置。

重要性

  1. 线程隔离:每个线程都有自己独立的程序计数器,这意味着每个线程的执行是独立的,互不影响。这对于Java虚拟机能够支持多线程环境至关重要。
  2. 错误恢复和线程恢复:程序计数器帮助恢复上下文,因此在执行Java多线程时,即使发生线程暂停,也能准确记录和恢复线程执行的位置。
  3. 无内存溢出风险:程序计数器是JVM中少数几个没有规定任何OutOfMemoryError情况的区域。因为它的生命周期随线程,通常也足够小。

假设一个线程正在执行一个Java方法,此方法中包含多条字节码指令。
程序计数器会指向当前正在执行的字节码指令的地址。
当线程执行到新的字节码指令时,程序计数器的值会更新为新指令的地址。如果线程执行的是Native方法,则程序计数器中的值不确定。

面试题 47 .请解释Java 虚拟机栈的作用?

Java虚拟机栈(JVM Stack)是Java虚拟机用来存储线程执行Java方法(或称为函数)的内存模型的一部分。每个线程在Java虚拟机中都有自己的虚拟机栈,这个栈与线程同时创建。虚拟机栈的主要作用是管理Java程序中方法的调用和执行。

主要功能和作用

  1. 存储局部变量:虚拟机栈中包含了多个栈帧,每个栈帧都对应着一个方法调用。栈帧中存储了这个方法需要的局部变量,包括各种基本数据类型、对象的引用(指针),以及方法执行过程中的临时数据。
  2. 控制方法执行:每个栈帧中还包含有对应方法的操作数栈和指向当前方法被调用时指令地址的程序计数器。操作数栈用于执行方法中的操作,如表达式求值等。
  3. 支持方法调用和返回值:当一个方法被调用时,一个新的栈帧会被创建并压入该线程的虚拟机栈中;方法返回时,对应的栈帧被销毁,返回值(如果有的话)被传递给调用者。
  4. 处理异常:如果方法执行过程中抛出异常,虚拟机栈会被用来寻找异常处理器,处理异常或者将异常传递给上层方法。

性能和异常

  • 内存管理:虚拟机栈是动态的,可以根据执行的方法动态调整大小(不过在大多数现代JVM实现中,栈的大小在线程创建时被指定并不会动态改变),这有助于优化内存使用。
  • StackOverflowError:如果线程请求的栈深度超过虚拟机栈允许的深度,将抛出StackOverflowError
  • OutOfMemoryError:如果虚拟机栈无法申请到足够的内存时,将抛出OutOfMemoryError

例如:

public class Example {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = 30;
        sum(a,b)
        System.out.println("a: " + a);
        System.out.println("b: " + b);
        System.out.println("c: " + c);
    }

    public static int sum(int a, int b) {
        return a + b;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

image.png
在这个例子中,当main方法执行时,一个栈帧被创建并压入虚拟机栈中,用来存储main方法的信息和局部变量。当main方法中调用sum方法时,又一个栈帧被创建并压入栈中来处理sum方法的执行。一旦sum方法执行完毕,其结果被返回到main方法,sum方法的栈帧就从栈中弹出。

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]

面试题 48 .请解释Java本地方法栈的作用?

Java本地方法栈(Native Method Stack)是Java虚拟机(JVM)中的一个重要组成部分,它主要用于支持Java的本地方法执行。本地方法是指用Java以外的语言(如C或C++)编写的方法,这些方法通常是为了访问操作系统的底层资源,或者为了性能考虑而实现的。

作用和特点

  1. 支持本地方法调用:当Java程序中调用本地方法时,JVM会使用本地方法栈来处理和存储本地方法的状态信息。这包括参数传递、局部变量的存储以及方法执行过程中的状态管理。
  2. 独立的栈空间:本地方法栈与Java虚拟机栈(负责Java方法的执行)是分开的。这意味着运行本地方法的过程和运行Java方法的过程是相互独立的,从而可以优化本地方法的执行效率和资源管理。
  3. 错误处理:当本地方法执行失败或出现错误时(例如内存访问错误),本地方法栈可以记录相关信息,帮助开发者或系统管理员诊断问题。
  4. 性能优化:由于本地方法直接运行在操作系统上,可以通过本地方法栈的有效管理,降低方法调用的开销,提高程序的执行效率。

使用场景

  • 系统级操作:例如文件操作、网络通信或是线程管理等,这些功能往往需要直接与操作系统交互,使用本地方法可以直接调用操作系统的API。
  • 性能关键的操作:在一些性能敏感的应用场景中,可能需要借助更底层、更快的语言来实现某些功能,比如数学计算库、图形处理等。

例如:AtomicInteger的compareAndSet方法就是使用的本地方法栈的方法

AtomicInteger atomicInteger = new AtomicInteger(5);

// 尝试将值从5更新为10
boolean success = atomicInteger.compareAndSet(5, 10);

if (success) {
    System.out.println("Update successful, new value: " + atomicInteger.get());
} else {
    System.out.println("Update failed, current value: " + atomicInteger.get());
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

面试题 49 .请简述JVM的方法区的作用 ?

JVM的方法区就像是一个图书馆,用来存放Java程序里所有类的信息。想象一下,每个类都是一本书,这些书里记录了类的结构,比如它有哪些方法(就是类里可以做的事情),有哪些变量等等。方法区还存放了这些方法的具体指令,也就是说,当你的程序运行时,它会来这个“图书馆”查找需要的信息。
除此之外,方法区还有一个特别的角落叫做“运行时常量池”,这里面存放的是一些常用的数字、字符串之类的常量信息,方便程序快速使用。
还有,方法区也负责存放静态变量。静态变量不像普通变量那样随着对象的创建而存在,它是跟着类一起加载的,可以被类的所有对象共享。
所以,你可以把方法区看作是存放所有类信息和部分特殊数据的地方,是程序能正确运行不可缺少的一部分。从Java 8开始,原来的方法区有了新名字,叫做元空间,但角色和功能基本没变。

面试题 50 .请简述运行时常量池的作用 ?

运行时常量池(Runtime Constant Pool)是Java虚拟机(JVM)中的一部分,它属于每个类或接口的一部分。
简单来说,它的作用主要包括以下几点:

  1. 存储字面量和符号引用:运行时常量池主要存储两类数据:字面量(如文本字符串、声明为final的常量值等)和符号引用(如类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等)。
  2. 支持动态链接:运行时常量池中的符号引用在类被加载后,会被转换为直接引用。这一过程称为动态链接,它有助于实现Java的多态和方法的调用。
  3. 缓存:运行时常量池还可以看作是一种缓存,Java虚拟机通过这个常量池来重用相同的常量,这样可以节省内存空间并提高性能。

运行时常量池是每个类和接口的私有资产,它随着类或接口的加载而创建,在类或接口被卸载时销毁。简而言之,运行时常量池是一个类中固定和动态数据的存储空间,是实现Java代码在虚拟机中高效运行的关键组件之一。

面试题 51 .请解释JVM 直接内存 ?

JVM的直接内存(Direct Memory)不是Java虚拟机运行时数据区的一部分,而是在Java堆外分配的内存。这部分内存的使用与JVM的堆内存相互独立,主要有以下几个特点和作用:

  1. 性能优化:直接内存通常用于在Java程序和操作系统之间传输大量数据时。使用直接内存可以显著提高性能,因为它能减少在Java堆和原生内存中数据传输时的复制过程。这是因为直接内存直接对原生(native)内存的操作,避开了JVM堆的干预。
  2. NIO(New Input/Output):直接内存最常见的使用场景是通过Java的NIO(New Input/Output)库,它允许Java程序通过所谓的“直接缓冲区”直接操作内存。这种方法主要用于高速的数据传输,如网络通信或文件I/O。
  3. 内存管理:虽然直接内存的访问速度快于Java堆内存,但其分配和释放通常比堆内存要慢,并且其管理不像自动内存管理那样方便。直接内存的分配和释放通常需要通过编码手动控制。
  4. 风险:直接内存的使用增加了内存泄漏的风险,因为它的生命周期不由JVM的垃圾收集器管理。不当的使用直接内存可能导致内存泄漏或系统崩溃。

总的来说,JVM的直接内存是一种高效的数据处理方式,适用于需要高性能I/O的场景。
然而,它的使用需要谨慎,以避免内存溢出等问题。

面试题 52 .请描述堆溢出的原因?

堆溢出(Heap Overflow)通常是指在Java虚拟机(JVM)中,当对象的内存分配需求超过了堆内存(Heap)的最大限制时发生的一种错误,这会导致OutOfMemoryError。具体原因可以从以下几个方面来理解:

  1. 内存分配过大:单个对象或者请求的数组大小超过了JVM堆的最大限制。这种情况较为罕见,但在请求大量数据处理时可能会发生。
  2. 对象长时间未被回收:程序中存在大量对象被长时间引用而不被释放,例如,对象被存储在全局集合中而没有适当的清除机制。这会导致JVM的堆内存中积累了大量不再需要的对象,从而耗尽内存。
  3. 内存泄漏:应用程序代码中存在内存泄漏,即对象不再被使用后仍然被引用,垃圾收集器无法回收这些对象。例如,静态集合类属性如果被错误地使用,可能会无意中保持对对象的引用,阻止垃圾回收。
  4. 垃圾收集器效率低:在某些情况下,垃圾收集器的回收效率不高,可能导致有效内存迅速耗尽,尤其是在内存使用高峰时。
  5. 堆大小配置不当:JVM启动时堆的大小是可以配置的,如果堆的最大值设置得过小,随着应用程序运行和对象创建的增多,可用内存可能不足以支撑程序的需要。
  6. 递归调用过深:虽然严格来说是栈溢出,但在一些使用大量递归的数据处理(如深度优先搜索)中,如果没有适当的终止条件或管理,也可能间接引起堆内存的过度使用。

面试题 53 .请描述栈溢出的原因 ?

栈溢出(Stack Overflow)是由于程序执行时栈内存空间被耗尽而引发的错误,常见于Java虚拟机(JVM)中。具体可能导致栈溢出的原因有下面几个可能的原因:

  1. 深度递归调用
    • 原因:递归调用中,每次方法调用时都会在调用栈上添加一个新的栈帧,包括局部变量和方法信息。如果递归没有及时退出或深度过大,会不断消耗栈空间。

例如:计算阶乘的递归函数,如果没有正确处理递归终止条件,可能导致栈溢出。

java
Copy code
public int factorial(int n) {
    if (n == 1) return 1;
    else return n * factorial(n - 1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. 大量嵌套或不断的方法调用
    • 原因:方法调用过多也会逐渐消耗栈空间,尤其是在一些深层嵌套的调用场景中。

例如:多个方法相互调用,形成长链。

java
Copy code
public void method1() {
    method2();
}
public void method2() {
    method3();
}
public void method3() {
    method1();  // 如果调用关系复杂,可能导致调用链过长
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  1. 大量的局部变量
    • 原因:每个方法的局部变量都会占用栈空间。如果一个方法中定义了大量的局部变量,尤其是数据占用较大的数组或对象,将快速耗尽栈空间。

例如:在方法内部定义一个大数组。

java
Copy code
public void largeLocalVariables() {
    int[] largeArray = new int[100000];  // 大数组占用栈空间
    // 使用数组
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. 栈帧过大
    • 原因:每个方法调用都会创建一个栈帧,栈帧中包含局部变量表、操作数栈等信息。栈帧占用的空间过大会导致栈空间迅速耗尽。

例如:一个方法中有多个大数据类型的局部变量。

java
Copy code
public void largeStackFrame() {
    double[] largeDoubleArray = new double[10000];
    long[] largeLongArray = new long[10000];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. JVM栈大小设置不当
    • 原因:JVM启动时可以设置栈的最大大小。如果设置得过小,栈空间可能不足以支持正常的方法调用。

例如:在JVM启动参数中设置较小的栈大小,如 -Xss256k

面试题 54 .请描述运行时常量池溢出的原因?

运行时常量池(Runtime Constant Pool)是每个类或接口的一部分,存储在Java虚拟机(JVM)的方法区内。
运行时常量池主要用于存储编译期生成的各种字面量和符号引用,这包括类和接口的全限定名、字段名、方法名及其他常量。
运行时常量池溢出是指这个存储区域由于存储的数据过多而超过了方法区的限制,导致OutOfMemoryError
以下是造成运行时常量池溢出的几种原因:

  1. 大量使用字符串常量和字符串拼接
    • 在Java中,字符串常量都是存储在运行时常量池中的。如果程序中使用了大量的字符串常量,或者有大量的字符串拼接操作(特别是使用了**String.intern()**方法),这些字符串都会被存储在常量池中,可能会导致常量池快速填满。
  2. 频繁使用String.intern()方法
    • String.intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,会将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。频繁使用这个方法可以使运行时常量池中的条目急剧增加。
  3. 大量动态生成类
    • 某些应用(如某些框架或动态代理技术)可能会动态生成大量的类。每一个类都可能有自己的运行时常量池,过多的类意味着需要更多的常量池空间。
  4. 持续加载和卸载类
    • 在一些Java应用中,特别是在容器环境如Tomcat中,频繁地加载和卸载类可能导致老的类在方法区(或PermGen space/Metaspace)中未被完全清理,这使得运行时常量池所占的空间不断增加。

好消息是,自Java 8起,运行时常量池已从永久代(PermGen)移至堆内存中的元空间(Metaspace),因此默认情况下,元空间的大小仅受系统可用内存的限制,运行时常量池溢出的情况有所减少,但在内存受限的环境中仍需谨慎管理内存使用,避免无节制的资源消耗。

面试题 55 .请描述方法区溢出的原因?

方法区(Method Area),在Java虚拟机(JVM)中,主要用于存放已被虚拟机加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据。
自Java 8起,传统的方法区(有时被称为永久代PermGen)被元空间(Metaspace)所取代。
方法区溢出,或在Java 8之后的版本中称为元空间溢出,主要由以下几个因素引起:

  1. 大量加载类
    • 在一些Java应用程序中,特别是使用动态生成类的框架(如某些OSGi环境、动态代理技术、热部署功能等),可能会加载大量的类到方法区。如果这些类不被卸载,它们会占据方法区的空间,最终可能导致方法区溢出。
  2. 大量使用反射和代理
    • 应用程序中大量使用反射、代理和CGLib这类字节码操作库,会动态生成很多运行时类,这些类如果长时间存在于方法区,也可能引起溢出。
  3. 永久代/元空间大小限制
    • 虽然元空间的大小受到物理内存的限制,但如果JVM启动时设置了元空间的大小限制较低,而应用程序又需要加载大量类和元数据,同样可能引起溢出。
  4. 大量常量的创建
    • 方法区也用于存储运行时常量池,如果程序中大量使用了常量,尤其是通过**String.intern()**方法存储了大量字符串,这同样可以耗尽方法区的空间。
  5. 内存泄漏
    • 如果类加载器不被垃圾收集器回收(例如,因为存在对这些类加载器的引用),那么加载的类也不会被卸载,从而导致方法区的内存泄漏。

解决方法区溢出的策略包括优化类加载的管理,避免过度使用反射和动态代理,适当增大方法区的大小设置,以及及时检测和处理可能的内存泄漏。对于使用较多第三方库和框架的复杂应用,需要特别注意这些方面的管理。

面试题 56 .Java对象分配内存的方式有哪些?

.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
这个步骤有两个问题:
1.如何划分内存。
2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
**划分内存的方法: **
**1、“指针碰撞”(Bump the Pointer)(默认用指针碰撞) **
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
**2、“空闲列表”(Free List) **
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录

面试题 57 .请思考对象分配内存是否线程安全?

通常情况下,JVM对于对象内存分配确保了高度的线程安全性
解决并发问题的方法:

  • CAS(compare and swap)

虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。

  • 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过**-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+UseTLAB**),-XX:TLABSize 指定TLAB大小。

面试题 58 .请描述对象的内存布局 ?

在Java虚拟机(JVM)中,对象的内存布局是标准化的,主要由以下几个组成部分构成:

  1. 对象头(Object Header)
    • 标记字段(Mark Word):这部分用于存储对象自身的运行时数据,如哈希码、锁信息(锁状态标记)、线程持有的锁、偏向线程ID、偏向时间戳等。这些信息用于系统级的低级同步和垃圾回收。
    • 类型指针:这个指针指向对象所属类的元数据,JVM通过这个指针确定对象的类信息。在某些JVM实现中,这部分可能被优化掉,通过其他方式来确定对象类型。
  2. 实例数据(Instance Data)
    • 这部分存储对象的实际有效数据,包括各种字段。这些数据的排列顺序和数据类型都可能影响到内存的使用。通常,为了内存的有效利用和快速访问,相同宽度的字段会被分组在一起。
  3. 对齐填充(Padding)
    • 为了使对象的总大小是8字节的整数倍(这在大多数现代计算机系统中是一个优化选择),可能在实例数据之后加入一些对齐填充。对齐填充并不是必须的,只有当对象的总大小不是8字节倍数时,才会添加。

32位虚拟机对象头
32位对象头.png
64位虚拟机对象头
对象头.png
每个部分都有其特定功能和重要性,合理的布局可以优化对象的存取效率及降低内存的使用。例如:

  • 对象头是关键部分,因为它包含了必要的元数据和同步信息,使得对象可以在JVM中安全有效地管理。
  • 实例数据则是对象中存储的实际用户数据,是最核心的部分。
  • 对齐填充虽然不包含实际信息,但它确保了内存访问的高效性。

面试题 59 .简述Java对象的访问方式有哪些?

在Java中,访问对象主要有以下几种方式,这些方式反映了Java虚拟机(JVM)如何从内存中定位和操作对象数据:

  1. 使用句柄访问
    • 在这种访问方式中,Java堆中会有一个句柄池。每个Java对象在堆上的分配都将有一个对应的句柄。这个句柄包含了对象数据与对象类型数据的引用。具体来说,当代码需要访问对象时,首先通过引用找到句柄,然后通过句柄找到对象数据和类型数据。这种间接访问的好处是,对象被移动时(例如在垃圾收集期间的压缩过程),只需改变句柄中的对象指针,而无需改变引用本身,这使得引用的更新成本较低。
  2. 直接指针访问
    • 在直接指针访问方式中,Java对象的引用直接指向对象数据的内存地址,同时,对象数据区域的起始部分包含了指向其类型数据的指针。这种方法减少了一次指针解引用的开销,提高了访问速度。对象移动时(如GC期间),需要更新所有的引用地址,这比句柄访问的成本更高,但访问速度更快。
  3. 内联(Inline)访问
    • 在一些高级的JVM实现中,特别是涉及即时编译器(JIT)优化时,可能会使用内联技术来优化对象访问。例如,方法中频繁访问的字段可以在方法调用时直接内联到调用者代码中,减少访问开销。内联是一种编译时优化,不仅限于对象访问,也用于方法调用。

例如,HotSpot JVM采用直接指针访问方式,因为它在大多数场景下提供了较好的性能平衡。

面试题 60 .请说一下什么是ZGC ?

ZGC(Z Garbage Collector)是从Java 11开始引入的一种垃圾收集器,主要目标是为大型应用提供低延迟的垃圾回收解决方案。
ZGC支持平台.png
ZGC设计的主要特点和目标包括:
ZGC目标.png

  • 支持TB量级的堆。我们生产环境的硬盘还没有上TB呢,这应该可以满足未来十年内,所有JAVA应用的需求了吧。
  • 最大GC停顿时间不超10ms。目前一般线上环境运行良好的JAVA应用Minor GC停顿时间在10ms左右,Major GC一般都需要100ms以上(G1可以调节停顿时间,但是如果调的过低的话,反而会适得其反),之所以能做到这一点是因为它的停顿时间主要跟Root扫描有关,而Root数量和堆大小是没有任何关系的。
  • 奠定未来GC特性的基础
  • 最糟糕的情况下吞吐量会降低15%。这都不是事,停顿时间足够优秀。至于吞吐量,通过扩容分分钟解决。

另外,Oracle官方提到了它最大的优点是:

它的停顿时间不会随着堆的增大而增长!也就是说,几十G堆的停顿时间是10ms以下,几百G甚至上T堆的停顿时间也是10ms以下。

由于ZGC的这些特性,它特别适用于需要处理大规模数据且对延迟敏感的应用,如大数据分析、高频交易系统等。不过,由于ZGC在某些场景下可能会增加CPU的负担,并且对系统资源的管理(如内存页的管理)有更高的要求,因此在选择使用ZGC之前需要仔细评估应用的特性和需求。

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]

面试题 61 . 请简述JVM有哪些内存分配与回收策略?

内存分配策略

  1. 对象优先在Eden区分配
    • 大部分情况下,对象首先在年轻代的Eden区分配。当Eden区填满时,进行一次Minor GC(也称为Young GC)。
  2. 大对象直接进入老年代
    • JVM对于需要大量连续内存空间的Java对象(如大数组或大对象),通常不在Eden区分配,而是直接在老年代中分配,以避免在Eden区及两个Survivor区之间来回复制,减少垃圾收集时的开销。
  3. 长期存活的对象将进入老年代
    • 在年轻代中经过多次GC依然存活的对象,其年龄会增加。当对象年龄增加到一定阈值(默认为15)时,会被移动到老年代中。
  4. 动态对象年龄判定
    • 为了更好地适应不同应用的需求,JVM可以动态地调整对象晋升老年代的年龄。如果在Survivor空间中相同年龄所有对象大小的总和超过Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
  5. 空间分配担保
    • 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果不足,VM会查看HandlePromotionFailure设置是否允许担保失败;如果不允许,那么进行一次Full GC。

垃圾回收策略

  1. 标记-清除算法
    • 这是最基本的收集算法,分为“标记”和“清除”两个阶段:标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
  2. 复制算法
    • 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
  3. 标记-整理算法
    • 用于老年代的垃圾收集。标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
  4. 增量收集算法
    • 垃圾收集线程和用户线程交替执行,不需要长时间停顿用户线程。增量收集算法适合交互较强的程序。

面试题 62 .请列举一些常用的JVM监控调试工具 ?

以下是一些常用的JVM监控和调试工具:

  1. VisualVM
    • 免费工具,提供一窗口化界面,用于监控Java应用程序。它可以显示本地和远程JVM进程的详细性能数据,包括CPU、内存、线程和堆转储分析等。
  2. JConsole
    • Java自带的JMX(Java Management Extensions)客户端工具,用于监控基于Java的应用程序。它提供了关于内存使用、线程使用、类加载和JVM运行的实时数据。
  3. Java Mission Control (JMC)
    • 原先是Oracle的商业产品,现在作为OpenJDK的一部分免费提供。它包括Java Flight Recorder(JFR),可以用来收集低开销的详细运行时信息。
  4. HeapDump
    • 用于生成和分析Java堆转储的工具。堆转储包含了程序中所有对象的信息,包括类、字段、引用等,这对于诊断内存泄漏非常有用。
  5. Eclipse Memory Analyzer (MAT)
    • 一个功能强大的内存分析工具,用于查找内存泄漏和查看内存消耗的对象。它可以读取堆转储文件,并通过图形界面展示内存使用情况。
  6. GCViewer
    • 一个轻量级的工具,用于分析Java应用的垃圾收集行为。它可以读取JVM生成的GC日志,并生成易于理解的图表,帮助识别内存回收问题。
  7. jstack
    • 是一个命令行工具,用于生成Java应用程序中的当前线程快照。非常有用于分析线程死锁和其他线程相关问题。
  8. jstat
    • 命令行工具,用于监控垃圾收集和JVM编译的统计信息。
  9. jmap
    • 命令行工具,用于生成堆内存映射。它可以帮助开发者了解内存分配情况,特别是哪些对象占用了过多的堆空间。

面试题 63 .请准确描述什么是类加载 ?

类加载(Class Loading)是Java虚拟机(JVM)中将类的Class文件加载到内存中的过程,并为之生成对应的java.lang.Class对象。Java的类加载过程涉及到加载、链接和初始化三个主要步骤:
类加载机制.png

  1. 加载(Loading)
    • 在这一步,类加载器(ClassLoader)从文件系统、网络或其他来源读取Java类的二进制数据(.class文件),然后根据这些数据在JVM内部创建一个java.lang.Class对象。这个对象封装了类的元数据,包括类的名称、方法信息、字段信息等。
  2. 链接(Linking)
    • 链接过程又包括验证、准备和解析三个阶段:
      • 验证(Verification):确保被加载的类符合Java语言规范和JVM规范,没有安全问题。
      • 准备(Preparation):为类变量分配内存并设置类变量的默认初始值,这些变量使用的内存在方法区中分配。
      • 解析(Resolution):将类、接口、字段和方法的符号引用转换为直接引用。
  3. 初始化(Initialization)
    • 在初始化阶段,JVM负责执行类构造器**()**方法的代码。这个方法由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生。初始化阶段是执行这些代码,赋予类变量初始的正确值。

Java的类加载机制是动态的,类在首次被使用时才被加载,这就是Java中所说的“延迟加载”。这种机制帮助减少内存的使用,并且分散了类加载的负载,优化了应用的启动时间。

面试题 64 .请列举哪些Java类加载器 ?

双亲委派机制.png

  • 启动类加载器(Bootstrap ClassLoader):负责加载JVM基础核心类库;
  • 扩展类加载器(Extension ClassLoader):负责加载扩展目录中的类库;
  • 系统类加载器(System ClassLoader):负责加载系统类路径(classpath)上的应用程序类。

开发者可以根据需要自定义类加载器,以实现特定的加载策略,比如从加密包中加载类,或者通过网络加载类等。这些自定义的加载器在遵循双亲委派模型(即先委派给父加载器尝试加载,失败后再由自身尝试加载)的前提下,提供了极大的灵活性。

面试题 65 .简述Java 的内存模型JMM?

Java内存模型(Java Memory Model,JMM) 是《Java虚拟机规范》中定义的一种用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的内存访问效果的一种内存访问模型。从JDK5开始 JMM才正真成熟,完善起来。

Java内存模型的主要目的是定义程序中各种变量(Java中的实例字段,静态字段和构成数组中的元素,不包括线程私有的局部变量和方法参数)的访问规则。

Java内存模型规定了所有的变量都存储在主内存(Main Memory)。每条线程都有自己的工作内存(Working Memory),用来保存被该线程使用的变量的主内存副本。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同线程之间无法之间访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
JMM内存模型图.png

面试题 66 .简述Java内存交互的模型 ?

关于一个变量如何从工作内存拷贝到工作内存,如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成
。Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可在分的(对 double 和 long 类型的变量来说,load、store、read 和 write操作在某些平台上允许有例外)。

  1. lock(锁定):作用于主内存的变量,它把一个变量表示为一条线程独占的状态。
  2. unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。
  3. read(读取):作用域工作内存的变量,它把一个变量的值从工作内存传输到线程的工作内存中,以便随后的 load 使用。
  4. load(载入):作用于工作内存中的变量,它把 read 操作从主内存中得到的变量值放到工作内存中的变量副本中。
  5. use(使用):作用于工作内存中的变量,它把工作内存中的一个变量值传给执行引擎,每当虚拟机遇到一个需要使用变量值的字节码指令时将会执行这个操作。
  6. assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接受的值赋给工作内存中的变量,每当虚拟机遇到一个需要使用变量赋值的字节码指令时执行此操作。
  7. store(赋值):作用于工作内存中的变量,它把工作内存中的一个变量传递到主内存中,以 write 操作使用。
  8. write(赋值):作用于主内存中的变量,他把 store 操作传递来的值放入到主内存的变量中。

Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

  1. 不允许 read 和 load、store 和 write 操作之一单独出现。
  2. 不允许一个线程丢弃它最近的assign操作,即一个变量在工作内存中被修改后必须把该变化同步回主内存。
  3. 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程工作内存同步回主内存中。
  4. 一个新的变量只能在主内存中 “诞生”,不允许在工作内存中直接使用一个未被初始化的(load 或 assign)的变量,即对一个变量实施 use 或 store 操作之前,必须先执行 load 或 assign操作。
  5. 一个变量在同时个时刻治愈系一条线程对器进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次 lock 后,必须执行相同次数的 unlock 操作,变量才会被解锁。
  6. 如果对一个变量执行 lock 操作,将会情况工作内存中的值,在执行引擎使用这个这个变量前,需要重新执行 load 或 assign 操作对 变量进行初始化。
  7. 如果一个变量实现没有被 lock 操作锁定,那就不会允许对他执行 unlock 操作,也不允许去 unlock 一个被其它线程锁定的变量。
  8. 对一个变量执行 unlock 操作前,必须先把此变量同步回主内存中(执行 store ,write 操作)

面试题 67 .JVM 对 Java 的原生锁做了哪些优化?

JVM(Java虚拟机)对Java的原生锁(synchronized关键字)进行了多种优化以提升性能,主要包括以下几种:

  1. 偏向锁(Biased Locking): 偏向锁是一种优化技术,用于减少没有竞争的同步块的开销。当锁被一个线程获取后,它会在锁对象的头部记录该线程的ID,表示该线程偏向于此锁。以后该线程进入和退出同步块时,不需要进行全面的锁释放和获取过程,只需要简单地检查锁对象头的标记即可。
  2. 轻量级锁(Lightweight Locking): 当锁是偏向锁,但检测到有其他线程尝试获取同一个锁时,偏向锁会升级为轻量级锁。轻量级锁通过在锁对象的头部存储锁记录(Lock Record)的指针,以及将线程栈帧中的一部分作为锁记录来实现。这种方式避免了操作系统层面的互斥,但仍涉及CAS操作(比较并交换)。
  3. 重量级锁(Heavyweight Locking): 如果轻量级锁失败(例如,有多个线程竞争相同的锁),则会升级为重量级锁。这种锁使用操作系统的互斥锁(如Mutex)来实现线程间的同步。这种锁的开销最大,但在高度竞争的环境下是必要的。
  4. 锁消除(Lock Elision): JVM通过静态分析代码,检测到某些锁其实并不是必须的(例如,一个被锁定的对象只能被一个线程访问),则可以完全消除这些锁。这种优化能减少不必要的同步开销。
  5. 锁粗化(Lock Coarsening): JVM还会在运行时进行锁粗化的优化。如果它检测到连续的锁定操作只操作同一个对象,它会将锁的范围扩大,将多个锁合并为一个范围更大的锁,以减少锁获取和释放的次数。

面试题 68 .当Java中出现了内存溢出,我们一般怎么排错?

当Java程序出现内存溢出错误时,意味着程序在执行过程中申请的内存超过了Java虚拟机(JVM)分配给它的内存限制。为了排查和解决这个问题,以下是一些常用的排错步骤:
** 1、查看错误信息 **
首先,检查Java程序抛出的异常信息。通常,内存溢出错误会抛出java.lang.OutOfMemoryError异常,可能会伴随有其他的详细信息,如"Java heap space"(Java堆空间)或"PermGen space"(永久代空间)。
** 2、分析堆栈跟踪 **
查看堆栈跟踪以确定哪个部分的代码导致了内存溢出错误。堆栈跟踪将显示代码的调用层次结构,从中可以看到哪些方法在错误发生时被调用。
** 3、检查内存配置 **
确认Java虚拟机的内存配置是否合理。内存溢出错误可能是由于分配给Java堆、栈或永久代的内存不足所致。可以通过修改JVM启动参数中的-Xmx(最大堆内存)和-Xms(初始堆内存)选项来增加可用的内存。
** 4、检查代码逻辑 **
检查代码是否存在内存泄漏的情况。内存泄漏是指程序在不再使用某些对象时未能释放对它们的引用,导致这些对象无法被垃圾回收器回收。常见的内存泄漏情况包括未关闭的文件、未释放的数据库连接、长生命周期的缓存等。使用内存分析工具可以帮助确定是否存在内存泄漏问题。
** 5、调整内存使用 **
如果确认代码逻辑正确且没有明显的内存泄漏问题,可以尝试优化代码以减少内存使用。例如,使用合适的数据结构、及时释放不再使用的对象、避免创建过多的临时对象等。
Java出现内存溢出怎么排错?
** 6、增加硬件资源 **
如果经过以上步骤后仍然无法解决内存溢出问题,可能是因为程序的内存需求超过了系统的硬件资源限制。此时可以考虑增加物理内存或迁移到更高配置的服务器。
** 7、使用内存分析工具 **
Java提供了多种内存分析工具,如VisualVM、Eclipse Memory Analyzer等。这些工具可以帮助识别内存泄漏、查看对象的引用关系、分析内存使用情况等,有助于更深入地排查内存溢出问题。
**总结 **
在处理内存溢出错误时,重要的是要通过分析和排查确定导致问题的根本原因。这需要结合实际情况和调试工具来进行逐步排查,以找到解决方案并确保代码的稳定性和性能

面试题 69 .简述什么时候会触发FullGC ?

Full GC(全面垃圾收集)是Java虚拟机(JVM)中清理内存的一种方式,它涉及到所有堆区域的清理,包括年轻代、老年代以及永久代(或元空间,取决于使用的JVM版本)。Full GC通常比较耗时,会暂停所有应用线程(即“Stop-The-World”),因此理解何时会触发Full GC对于优化Java应用性能非常重要。以下是一些常见的触发Full GC的情形:

  1. 老年代空间不足: 当老年代(Old Generation)填满时,没有足够空间分配给即将晋升的对象,JVM会触发Full GC来清理老年代,释放空间。
  2. 永久代或元空间不足: 如果使用的是较旧版本的JVM,可能会有永久代(PermGen),而在Java 8及以上版本,这个区域被替换为元空间(Metaspace)。这些区域存放类的元数据、常量池等。如果这些区域填满,也会触发Full GC。
  3. System.gc()调用: 应用程序可以通过调用**System.gc()**请求JVM执行垃圾收集。虽然JVM可以忽视这个请求,但大多数情况下,这会触发Full GC。
  4. JVM内部的GC算法要求: 根据JVM的垃圾回收器的选择和配置,例如CMS(Concurrent Mark-Sweep)回收器在无法并发清理足够空间时,会退回到Full GC。G1垃圾回收器在某些情况下也会进行全区域的清理。
  5. 分配巨型对象失败: 在使用G1等垃圾回收器时,如果巨型对象(大于Region大小的对象)无法被分配到连续的空间中,可能会触发Full GC来释放足够的连续空间。
  6. JVM启动参数: 某些JVM启动参数,如**-XX:+DisableExplicitGC**,可以控制由**System.gc()**引起的Full GC行为,而其他参数可能影响GC的行为和触发时机。

面试题 70 .简述描述一下JVM加载class文件的原理机制?

loadClass的类加载过程有如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 验证:校验字节码文件的正确性
  • 准备:给类的静态变量分配内存,并赋予默认值
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下节课会讲到动态链接
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块

类加载.png
类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
类加载器的引用:这个类到类加载器实例的引用
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]

面试题 71 .简述Java对象创建过程 ?

对象的创建
对象创建的主要流程:
类加载机制.png
1.类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。

2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
这个步骤有两个问题:

1.如何划分内存。
2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

划分内存的方法:
1、“指针碰撞”(Bump the Pointer)(默认用指针碰撞)

如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

2、“空闲列表”(Free List)

如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录

解决并发问题的方法:

  • CAS(compare and swap)

虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。

  • 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过**-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+UseTLAB**),-XX:TLABSize 指定TLAB大小。

3.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
5.执行方法
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。

面试题 72 .简述Java类的生命周期吗 ?

类的生命周期包括这几个部分:
加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
链接,链接又包含三块内容:验证、准备、初始化
1)验证,文件格式、元数据、字节码、符号引用验证;
2)准备,为类的静态变量分配内存,并将其初始化为默认值;
3)解析,把类中的符号引用转换为直接引用初始化,为类的静态变量赋予正确的初始值使用,new出对象程序中使用卸载,执行垃圾回收

面试题 73 .简述述Java的对象结构 ?

Java对象由三个部分组成:对象头、实例数据、对齐填充。
对象头由两部分组成:
第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)
第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)
**32位虚拟机对象头: **
32位对象头.png
**64位虚拟机对象头: **
对象头.png

面试题 74 .简述如何判断Java对象可以被回收 ?

判断对象是否存活一般有两种方式:
**1、引用计数: **
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。

此方法简单,无法解决对象相互循环引用的问题。

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

面试题 75 .简述Java调优命令有哪些 ?

下面列出一些常用的Java性能调优命令:

1. jps (Java Virtual Machine Process Status Tool)

  • 描述:列出当前机器上的所有Java进程。
  • 常用选项
    • -l:输出应用的完整包路径。
    • -v:输出传递给JVM的参数。

2. jstat (JVM Statistics Monitoring Tool)

  • 描述:用于收集并监控Java虚拟机各种性能指标的工具,如类加载、内存、垃圾回收等。
  • 常用选项
    • -gc:显示垃圾收集统计信息。
    • -classpath:显示每个已加载类的类加载器统计信息。

3. jinfo (Configuration Info for Java)

  • 描述:实时查看和调整Java应用程序的配置。
  • 常用选项
    • -flags:打印出所有JVM参数。
    • :调整或查询指定Java进程的指定JVM参数。

4. jmap (Memory Map for Java)

  • 描述:生成堆转储快照(heap dump)。此快照可以用来分析堆中的对象。
  • 常用选项
    • -heap:显示Java堆详细信息。
    • -histo:显示堆中对象的统计信息。

5. jstack (Stack Trace for Java)

  • 描述:用于生成Java应用程序的线程堆栈信息,非常有用于分析死锁或其他线程同步问题。
  • 常用选项
    • -l:显示关于锁的附加信息。

面试题 76 .简述Minor GC与Full GC分别在什么时候发生 ?

Minor GC

Minor GC主要处理Java堆中的年轻代(Young Generation)。年轻代是新生成的对象的主要存放地,这个区域相对较小,因此Minor GC比较频繁但通常很快。

  • 触发时机
    • 当年轻代空间不足时触发。具体来说,是当新创建的对象没有足够的空间存放在年轻代的Eden区或Survivor区时。
    • 新对象在创建时直接进行分配,如果Eden区满了,就会触发一次Minor GC,目的是清理掉那些短命的对象,为新对象腾出空间。

由于年轻代中的对象生命周期通常很短,很多对象在Minor GC后就被清除了,少数存活的对象则会被移动到Survivor区或老年代。

Full GC

Full GC涉及整个Java堆的清理,包括年轻代、老年代以及元空间(如果使用的是Java 8及以上版本的永久代已被替换为元空间)。Full GC比Minor GC慢得多,且会引发更长时间的停顿。

  • 触发时机
    • 老年代(Old Generation)空间不足时。当年轻代中的对象经过一定次数的Minor GC后仍然存活,它们将被晋升到老年代。如果老年代的可用空间不足以容纳更多的晋升对象,就会触发Full GC。
    • 元空间不足时。如果元空间中分配的内存不足以存放新的类定义或其他元数据,也会触发Full GC。
    • System.gc()方法被显式调用时。这个方法的调用会建议JVM执行Full GC,但具体执行时机取决于JVM的垃圾回收策略和状态。
    • JVM内部的垃圾收集器决定进行Full GC。例如,在并发标记清除(CMS)收集器的失败回退过程中,如果并发收集失败,JVM可能会退回到Full GC。

面试题 77 .简述Java对象一定分配在堆中吗?有没有了解逃逸分析技术 ?

「对象一定分配在堆中吗?」 不一定的,JVM通过「逃逸分析」,那些逃不出方法的对象会在栈上分配。

**「什么是逃逸分析?」 **
逃逸分析(Escape Analysis),是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。
**
「逃逸分析的好处」 **
栈上分配,可以降低垃圾收集器运行的频率。同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同
步。标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并且GC频率也会减少。

面试题 78 .简述Java虚拟机为什么使用元空间替换了永久代 ?

在Java 8中,Java虚拟机(JVM)用元空间(Metaspace)替换了原先的永久代(PermGen,Permanent Generation)。
这一改变是为了解决永久代存在的几个问题并改进JVM的性能、可扩展性和可维护性。
下面是为什么使用元空间替换永久代的主要原因:

1. 内存限制问题

永久代有一个固定的最大内存限制,这意味着所有的类元数据、字符串常量和静态变量都必须在这个固定大小的空间中管理。这种固定大小的设置在很多大型或者动态生成很多类的Java应用中导致了频繁的Full GC和OutOfMemoryError。

2. 内存泄漏风险

由于永久代的大小是固定的,应用如果加载了大量的类或者使用了大量的反射,容易发生永久代内存不足的情况,从而引起内存泄漏。这在长时间运行的应用中尤为常见,例如应用服务器和JEE应用。

3. 垃圾收集效率

永久代是使用标记-整理算法进行垃圾收集的,这意味着清理未使用的类和常量较为复杂和耗时。这增加了Full GC的时间,从而影响到应用的响应时间和吞吐量。

4. 可维护性和扩展性

永久代的管理和优化对JVM的开发者来说是一个挑战。它需要JVM维护一个额外的、与Java堆分开的内存区域,这增加了JVM的复杂性和维护难度。

5. 利用本地内存管理

元空间不再使用JVM堆的一部分,而是直接使用本地内存(即操作系统内存),这样做的好处是利用了操作系统本身更成熟的内存管理技术,减少了Java堆的压力,提高了性能。

6. 动态扩展

与永久代不同,元空间的默认大小只受系统可用内存的限制。这意味着元空间可以根据需要动态扩展,减少了因为内存不足导致的系统崩溃的可能性,并可以更灵活地适应不同的应用需求。

7. 简化Java内存模型

元空间的引入简化了Java的内存模型,使开发者更容易理解和优化Java应用的内存使用。
通过使用元空间代替永久代,Java的内存管理变得更加现代化和高效,同时解决了之前由永久代大小固定和难以管理带来的一系列问题。这些优势使得JVM能更好地支持大规模应用和微服务架构。

面试题 79 .简述什么是Stop The World ? 什么是OopMap?什么是安全 点?

进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW。
在HotSpot中,有个数据结构(映射表)称为「OopMap」。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过程中,也会在「特定的位置」生成 OopMap,记录下栈上和寄存器里哪些位置是引用。
这些特定的位置主要在:
1.循环的末尾(非 counted 循环)
2.方法临返回前 / 调用方法的call指令后
3.可能抛异常的位置
这些位置就叫作「安全点(safepoint)。」 用户程序执行时并非在代码指令流的任意位置都能够在
停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。

面试题 80 .简述Java什么是指针碰撞 ?

一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。
当类加载检查通过后,Java虚拟机开始为新生对象分配内存。
如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是 指针碰撞

面试题 81 .简述什么是Java空闲列表 ?

如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表

面试题 82 .简述JVM什么是TLAB(本地线程分配缓存)?

可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,这就是TLAB(Thread Local Allocation Buffer,本地线程分配缓存) 。虚拟机通过 -XX:UseTLAB 设定它的。

面试题 83 .简述如何选择垃圾收集器?

  1. 如果你的堆大小不是很大(比如 100MB ),选择串行收集器一般是效率最高的。
    参数: -XX:+UseSerialGC
  2. 如果你的应用运行在单核的机器上,或者你的虚拟机核数只有单核,选择串行收集器依然是合适的,这时候启用一些并行收集器没有任何收益。
    参数: -XX:+UseSerialGC 。
  3. 如果你的应用是“吞吐量”优先的,并且对较长时间的停顿没有什么特别的要求。选择并行收集器是比较好的。
    参数: -XX:+UseParallelGC 。
  4. 如果你的应用对响应时间要求较高,想要较少的停顿。甚至 1 秒的停顿都会引起大量的请求失败,那么选择 G1 、 ZGC 、 CMS 都是合理的。虽然这些收集器的 GC 停顿通常都比较短,但它需要一些额外的资源去处理这些工作,通常吞吐量会低一些。
    参数:
    -XX:+UseConcMarkSweepGC
    -XX:+UseG1GC
    -XX:+UseZGC 等。
    从上面这些出发点来看,我们平常的 Web 服务器,都是对响应性要求非常高的。选择性其实就集中在 CMS 、 G1 、 ZGC 上。而对于某些定时任务,使用并行收集器,是一个比较好的选择

面试题 84 .简述什么是 tomcat 类加载机制?

当 tomcat启动时,会创建几种类加载器: Bootstrap 引导类加载器 加载 JVM启动所需的类,以及标准扩展类(位于 jre/lib/ext 下) System 系统类加载器 加载 tomcat 启动的类,比如bootstrap.jar,通常在 catalina.bat 或者 catalina.sh 中指定。位于 CATALINA_HOME/bin 下
tomcat类加载器.png
tomcat的几个主要类加载器:

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;

从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。

面试题 85 .简述SafePoint(安全点) 是什么 ?

safepoint又叫安全点
比如 GC 的时候必须要等到 Java 线程都进入到 safepoint 的时候 VMThread 才能开始
执行 GC,

  1. 循环的末尾 (防止大循环的时候一直不进入 safepoint,而其他线程在等待它进入safepoint)
  2. 方法返回前
  3. 调用方法的 call 之后
  4. 抛出异常的位置

最后说一句(求关注,求赞,别白嫖我)

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。
这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]

本文,已收录于,我的技术网站 [cxykk.com:程序员编程资料站],有大厂完整面经,工作技术,架构师成长之路,等经验分享

求一键三连:点赞、分享、收藏

点赞对我真的非常重要!在线求赞,加个关注我会非常感激!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/960412
推荐阅读
相关标签
  

闽ICP备14008679号