JVM面试

介绍JVM

执行Java字节码的抽象机器,主要负责解释执行编译后的Java程序代码,提供一个平台无关的运行环境,“一次编写,到处运行”。

  1. 内存管理:

    1. JVM 负责管理 Java 程序的内存使用。它将内存划分为不同的区域,如堆、栈、方法区等。

      • 堆内存用于存储对象实例,是垃圾回收的主要区域。栈用于存储方法调用的局部变量和执行上下文。方法区存储类信息、常量池等。

      • JVM 通过垃圾回收机制自动回收不再使用的对象,以释放内存空间。不同的垃圾回收算法和策略可根据应用需求进行配置。

  2. 垃圾回收:

    1. 垃圾回收是 JVM 的重要功能之一。它通过识别和回收不再使用的对象来释放内存。

    2. 最新的 Java 版本通常会引入更高效的垃圾回收算法和优化,以提高垃圾回收的性能和效率。

      • 常见的垃圾回收器包括串行垃圾回收器、并行垃圾回收器、CMS(并发标记清除)垃圾回收器和 G1 垃圾回收器等。

  3. 类加载:

    1. JVM 负责加载 Java 类文件,并将其转换为可执行的字节码。

    2. 类加载过程包括加载、链接和初始化。加载阶段将类文件读取到内存中,链接阶段进行验证、准备和解析,初始化阶段执行类的静态初始化代码。

      • JVM 支持动态类加载,可以在运行时根据需要加载和卸载类。

  4. 字节码执行:

    1. JVM 执行 Java 字节码,实现 Java 程序的逻辑。

    2. 字节码执行引擎可以解释执行字节码,也可以将字节码编译为本地机器码以提高执行效率。

      • 最新的 Java 版本通常会对字节码执行进行优化,如即时编译(JIT)技术,以提高程序的性能。

  5. 安全管理:

    1. JVM 提供了安全机制,防止恶意代码的攻击和非法操作。

    2. 它包括类文件验证、访问控制、安全管理器等功能,确保 Java 程序在安全的环境中运行。

      • Java 的安全模型可以限制程序对系统资源的访问,并提供加密、数字签名等安全功能。

  6. 多线程支持:

    1. JVM 支持多线程编程,提供了线程同步和线程调度的机制。

    2. 线程可以在 JVM 中并发执行,共享堆内存和其他资源。

      • Java 的并发包提供了丰富的工具和类,用于实现高效的多线程编程。

JVM内存区域分布

  1. 堆(Heap):堆是 JVM 中最大的内存区域,用于存储对象实例和数组。对象的创建和垃圾回收都在堆上进行。堆是被所有线程共享的,它在Java虚拟机启动时被创建。

  2. 栈(Stack)

    1. 虚拟机栈(Java Virtual Machine Stacks):线程私有,用于存储方法调用的栈帧。栈帧包含了局部变量、操作数栈、方法返回地址等信息。先进后出FILO,-Xss 10M

    2. 本地方法栈(Native Method Stacks):与 Java 虚拟机栈类似,但是用于存储本地方法(非 Java 方法)的调用信息。本地方法是使用其他语言(如C、C++)编写的方法,通过JNI(Java Native Interface)调用在Java程序中执行。

  3. 程序计数器(Program Counter Register):较小的内存区域,用于指示当前线程正在执行的字节码指令的地址(分支、 循环、 跳转、 异常、 线程恢复)。每个线程都有自己独立的程序计数器,退出CPU时记录下一指令。

  4. 方法区(Method Area):方法区用于存储类信息、常量、静态变量等数据。在 Java 8 之前,方法区也被称为永久代(PermGen),但在 Java 8 及之后,方法区被实现为元空间(Metaspace)。

    1. 运行时常量池(Runtime Constant Pool):运行时常量池是方法区的一部分,用于存储编译期生成的各种字面量和符号引用。

方法区的永久代改为元空间的原因

  1. 解决内存溢出问题:永久代的大小是固定的,在某些情况下可能会导致内存溢出问题。而元空间使用本地内存,其大小可以根据系统的实际内存情况进行动态调整,从而避免了由于永久代大小限制而导致的内存溢出问题。

  2. 提高性能:元空间的内存分配和垃圾回收由操作系统直接管理,而不是由 JVM 进行管理。这可以提高内存的分配和回收效率,从而提高应用程序的性能。

  3. 更好的内存管理:元空间使用本地内存,与 JVM 堆内存分离,可以更好地进行内存管理。这可以避免由于堆内存不足而导致的方法区内存不足的问题,同时也可以提高内存的利用率。

  4. 与云环境更好的兼容性:在云环境中,应用程序的内存使用情况可能会受到限制。元空间的动态调整特性可以更好地适应云环境的需求,从而提高应用程序的可扩展性和灵活性。

堆内存分为新生代、老年代

新生代

  • 存储新创建对象,先在Eden区

  • Eden满后,触发1次Minor GC

    • 仍然存活的对象,复制到survivor区

    • 清空Eden区

    • 下次满后,Eden+Survivor区,一起复制到另一个Survivor区

    • 年龄达到一定阈值,移到老年代(或者对象大小超出了新生代的容量)

老年代

  • 存储长期存活的对象

  • 垃圾回收频率相对较低,通常为

    • Major GC(Major Garbage Collection):

      • 触发条件:老年代内存空间不足

      • 影响:时间长,程序可能会短暂停顿

    • Full GC(Full Garbage Collection):

      • 触发条件(多个或)

        • 老年代空间不足,且无法通过 Major GC 释放足够的空间时

        • 元空间空间不足时

        • 显示调用System.gc()

      • 影响

        • 回收整个堆内存,包括新生代、老年代

        • 时间更长,程序停顿时间可能更长,性能影响大

垃圾回收器是如何区分垃圾对象和活跃对象

  • 引用计数法(Reference Counting)

    • 维护每个对象的引用计数,当对象被引用时计数加一,当引用失效时计数减一。当对象的引用计数为零时,回收。

    • 无法解决循环引用问题,即两个或多个对象相互引用,没有其他引用指向它们。

  • 可达性分析法(Reachability Analysis)

    • 从根对象(如方法区中的类静态变量、本地方法栈中的引用等)开始,通过对象之间的引用链,逐个遍历对象,并标记可达的对象。未被标记的对象则被认定为垃圾对象。

    • 可解决循环引用问题,因为只有从根对象出发能够到达的对象才会被标记为可达。

GC ROOT对象

GC ROOT对象是指那些被认为是存活对象的根节点。垃圾回收器通过从这些根节点出发,遍历对象引用链,判断哪些对象是可达的,哪些对象是不可达的,从而进行垃圾回收。

常见的GC ROOT对象包括以下几种:

  • 方法区中的类静态属性:类的静态属性存储在方法区中,它们随着类的加载而被创建,并且在整个程序的生命周期内存在。这些静态属性作为GC ROOT对象,因为它们可以被其他对象引用。

  • 方法区中的常量引用:方法区中的常量池存储着常量的引用,包括字符串常量、类名、方法名等。这些常量引用也被视为GC ROOT对象,因为它们被其他对象引用。

  • 活动线程的本地变量和调用栈:活动线程中的本地变量和调用栈中的对象引用也被认为是GC ROOT对象。这些对象引用可以通过方法调用链追溯到活动线程的栈帧,因此它们是垃圾回收的起点。

  • JNI(Java Native Interface)引用:JNI允许Java程序调用本地代码,而本地代码中可能会创建和引用Java对象。JNI引用的对象也被视为GC ROOT对象,因为它们可能被本地代码持有引用。

垃圾回收算法

  • 垃圾收集算法(Garbage Collection Algorithm)——可达性

    • 垃圾回收器使用不同的算法来执行垃圾回收操作。

    • 常见的算法

      • 标记-清除算法(Mark and Sweep):这是最基本的垃圾回收算法。它分为两个阶段:标记阶段和清除阶段。首先,从根节点开始,标记所有被引用的对象。然后,在清除阶段,遍历整个堆,清除所有未被标记的对象。该算法的缺点是会产生内存碎片。

      • 复制算法(Copying):该算法将堆内存分为两个区域,通常是相等大小的两块。当一块区域被填满后,将存活的对象复制到另一块区域中,然后清除原来的区域。该算法的优点是不会产生内存碎片,但代价是需要额外的空间。

      • 标记-压缩算法(Mark and Compact):该算法结合了标记-清除算法和复制算法的优点。首先,标记所有被引用的对象。然后,将存活的对象向一端移动,并清除边界之外的内存。这样可以解决标记-清除算法的内存碎片问题。

      • 分代算法(Generational):该算法基于一个观察:大部分对象的生命周期很短。根据这个观察,堆内存被划分为几个代,通常是新生代和老年代。新生代中的对象经常被回收,而老年代中的对象则存活更久。这样,可以针对不同代使用不同的垃圾回收算法,以提高效率。

让一个被判定为垃圾的对象不被回收

  1. 使用强引用:将对象保持在一个强引用中,这样垃圾回收器就不会回收它。强引用是 Java 中最常见的引用类型,只要存在强引用指向对象,对象就不会被回收。

  2. 使用 finalize() 方法:你可以在对象的类中重写 finalize() 方法。在 finalize() 方法中,你可以执行一些清理操作或重新建立对象的引用,从而防止对象被回收。然而,需要注意的是,finalize() 方法的调用时机是不确定的,可能不执行,并且不建议过度依赖它,因为它可能会导致性能问题和不确定性。

  3. 使用软引用或弱引用:Java 提供了软引用(SoftReference)和弱引用(WeakReference)来管理对象的引用。软引用和弱引用不会阻止对象被垃圾回收,但它们可以提供一种机制,在对象被回收之前执行一些额外的操作或获取一些通知。

  4. 避免对象被孤立:确保对象在程序中仍然被其他对象引用,或者将其存储在一个可以被访问到的位置,以避免它被判定为孤立的垃圾对象。

引用类型

强引用(StrongReference):当一个对象被强引用变量引用时,垃圾回收器不会回收该对象,除非该强引用变量不再引用该对象。

软引用(SoftReference):内存充足时不会被垃圾回收器回收,只有在内存不足时才会被回收。常用于实现缓存。

弱引用(WeakReference):垃圾回收时会被立即回收,无论内存是否充足。常用于解决循环引用的问题,避免对象之间的循环引用导致内存泄漏。

虚引用(PhantomReference):放弃。

减少Full GC发生

Full GC 的发生,可以采取以下一些措施:

  • 合理调整堆内存的大小,避免内存不足的情况。

  • 优化对象的创建和使用,尽量减少不必要的对象创建。

  • 避免在代码中显式调用 System.gc() 方法。

  • 监控和分析垃圾回收的日志,了解垃圾回收的情况,及时发现和解决可能存在的问题。

STW(Stop-The-World)是什么,以及它在何时发生

是一种暂停所有应用线程的机制,用于执行特定的操作或任务。当STW发生时,所有的应用线程都会被暂停,直到特定的操作或任务完成后才会恢复执行。

  • 垃圾回收(Garbage Collection):

    • 在STW期间,垃圾回收器会扫描堆中的对象,并标记和回收垃圾对象。

    • 一旦垃圾回收完成,应用线程才会被恢复。

    • 为了确保对象的一致性和安全性。

  • 类加载和卸载(Class Loading and Unloading):

    • 在STW期间,执行类加载或卸载的操作。

    • 确保类加载和卸载的过程是线程安全的。

  • JIT编译器优化(Just-In-Time Compiler Optimization):

    • 在STW期间,JIT编译器会分析和优化字节码,以提高程序的执行效率。

    • 编译为本地机器码。

    • 确保字节码的一致性和正确性。

  • 安全点检查:方法调用、循环迭代等,如正在执行非原子操作、阻塞操作、长时间操作。在垃圾回收前检查。

分享使用过的堆栈分析工具

  1. jstack:这是 JDK 自带的工具,可以生成 Java 进程的线程快照,帮助我们分析线程的状态、阻塞情况等。

    1. 找到要分析的 Java 进程的 PID。

    2. 在命令行中运行 jstack PID,其中 PID 是进程的 ID。

  2. VisualVM:这是一个功能强大的 Java 性能分析工具,可以监控 Java 应用程序的内存使用情况、线程状态、类加载情况等,并提供了丰富的分析和调试功能

    1. 连接到要分析的 Java 应用程序。

    2. 在 VisualVM 的界面中,可以查看应用程序的内存使用情况、线程状态、类加载情况等。

    3. 可以使用各种插件进行更深入的分析和调试。

  3. MAT(Memory Analyzer Tool):这是一款专门用于分析 Java 堆内存的工具,可以帮助我们找出内存泄漏和内存溢出的原因。

    1. 打开 MAT,并导入要分析的堆转储文件。

    2. 在 MAT 的界面中,可以查看堆内存的使用情况、对象的引用关系等。

VisualVM 发现 OOM(内存溢出)、线程死锁和性能问题

  1. OOM(内存溢出):

    1. 启动 VisualVM 并连接到要分析的 Java 应用程序。

    2. 在 VisualVM 的界面中,查看“内存”选项卡。

    3. 观察内存使用情况图表,如果发现内存使用持续增长且接近或超过可用内存限制,可能存在 OOM 风险。

    4. 可以进一步查看“堆dump”信息,分析对象的分配和引用情况,找出可能导致内存泄漏的对象。

  2. 线程死锁:

    1. 在 VisualVM 的“线程”选项卡中,查看线程的状态。

    2. 如果发现线程长时间处于阻塞状态(BLOCKED 或 WAITING),可能存在死锁情况。

    3. 点击具体的线程,可以查看线程的堆栈跟踪信息,找出可能导致死锁的代码位置。

    4. 检查线程之间的资源竞争和同步操作,以确定死锁的原因。

  3. 性能问题:

    1. 关注“CPU”和“内存”选项卡,查看应用程序的 CPU 和内存使用情况。

    2. 如果 CPU 使用率过高或内存使用波动较大,可能存在性能问题。

    3. 在“抽样器”中,可以进行 CPU 和内存采样,以获取更详细的性能数据。

    4. 分析采样结果,找出消耗 CPU 或内存较多的方法或代码段。

    5. 还可以查看“垃圾回收”选项卡,了解垃圾回收的频率和时间,以评估对性能的影响。

JVM 调优方面的经验和实践

  1. 内存设置:根据应用程序的需求和硬件资源,合理设置 JVM 的内存大小。可以通过调整 -Xmx(最大堆内存)和 -Xms(初始堆内存)等参数来优化内存使用。

  2. 垃圾回收器选择:JVM 提供了多种垃圾回收器,如 Serial、Parallel、CMS、G1 等。根据应用程序的特点和性能要求,选择合适的垃圾回收器。

  3. 垃圾回收参数调整:对于选择的垃圾回收器,可以进一步调整相关参数,如 -XX:SurvivorRatio-XX:MaxTenuringThreshold 等,以优化垃圾回收的效率。

  4. 堆内存分析:使用工具(如 jconsole、jvisualvm 等)分析堆内存的使用情况,找出可能的内存泄漏或内存溢出问题,并进行相应的优化。

  5. 代码优化:通过优化代码,减少对象的创建和内存分配,提高代码的性能和效率。

  6. 监控和日志分析:密切关注应用程序的性能指标和日志,及时发现和解决潜在的问题。

  7. 压力测试:在实际环境中进行压力测试,以评估 JVM 的性能和稳定性,并根据测试结果进行调优。

单例模式是否可能导致内存泄漏

一般不会。但是,如果单例对象,引用了其他大对象,就有可能。

因为大对象被单例对象引用,而单例对象不会被垃圾回收。

  • 47
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值