一、JVM常见面试题目
1.什么是类加载器,有哪些常见的类加载器?
类加载器是Java虚拟机(JVM)的一部分,它的主要任务是在类的加载过程中,从文件系统、网络或其他来源动态加载类的字节码,并将其转换为可以在JVM中运行的类。在JDK 9及之后的版本中,类加载器完全由Java实现。
常见的类加载器有以下几种:
- 启动类加载器(Bootstrap ClassLoader):这是JVM的顶层加载类,主要负责加载核心类库,如java.lang、java.util等。这些类库是JVM启动时必须加载的。
- 平台类加载器(Platform ClassLoader):在JDK 9及之后的版本中,原本的扩展类加载器(Extension ClassLoader)被重命名为平台类加载器。它主要负责加载平台扩展类库,这些类库通常位于$JAVA_HOME/lib/ext目录下。
- 应用程序类加载器(Application ClassLoader):这个类加载器负责加载应用程序classpath下的类文件。在大多数情况下,它是开发人员与类加载器交互的入口。
- 自定义类加载器:除了上述三种默认的类加载器,Java还允许开发者根据需要自定义类加载器。自定义类加载器通常通过继承java.lang.ClassLoader类并重写其findClass方法来实现。这样,开发者可以根据自己的需求,从特定的位置(如数据库、网络等)加载类。
参考回答:类加载器是JVM的一部分,负责加载类的字节码到内存中。常见的类加载器有启动类加载器、平台类加载器(在JDK 9+中替代了扩展类加载器)、应用程序类加载器和自定义类加载器。
2.什么是双亲委派机制,以及如何打破双亲委派机制?
双亲委派模型是Java类加载机制中的一个核心概念,它确保了类加载的安全性和一致性。在双亲委派模型中,当一个类加载器收到类加载请求时,它不会自己首先尝试加载,而是将这个请求委派给它的父类加载器去完成。只有当父类加载器无法完成加载请求时,子类加载器才会尝试自己加载。
双亲委派模型的主要作用有两点:首先,它避免了类的重复加载,每个类只会被加载一次。其次,它确保了Java核心API的稳定性,因为自定义的类加载器无法加载Java核心类,从而避免了可能的代码冲突和安全风险。
在Java中,可以通过继承ClassLoader并重写其loadClass方法来创建自定义类加载器。通过这种方式,可以打破双亲委派机制,实现类的隔离。例如,在Tomcat中,每个Web应用都有自己的类加载器,从而实现了应用之间的类隔离。当两个Web应用中有相同限定名的类时,如Servlet类,Tomcat通过自定义类加载器保证它们是不同的类。
参考回答:双亲委派模型是Java类加载机制的核心组成部分,它要求一个类加载器在尝试加载一个类之前,先将其加载请求委派给其父类加载器。通过这种方式,从顶层启动类加载器开始,逐级向下进行类加载,确保了核心类库的稳定性与安全性,同时避免了类的重复加载。要打破双亲委派机制,常见的做法是实现自定义类加载器,并重写defineClass方法。
3.如何判断堆上的对象没有被引用?
在Java中,判断堆上对象是否已被垃圾回收的过程是通过可达性分析算法来完成的。这个算法将对象分为两类:垃圾回收的根对象(GC Root)和普通对象。可达性分析算法的核心思想是:如果一个对象从GC Root开始,按照引用关系向下搜索,不可达(即不存在引用链)到该对象,那么该对象就被认为是不可达的,即可以被垃圾回收。最常见的是,GC Root对象会引用栈上的局部变量和静态变量,如果这些引用被断开,那么对应的对象就会变得不可达,从而可能被垃圾回收。与引用计数法相比,可达性分析算法能够更准确地判断对象的可回收性,因为它能够处理循环引用的情况。
引用计数法会为每个对象维护一个引用计数器,每当对象被引用时计数器加1,取消引用时减1。然而,当存在循环引用时,即使对象之间不再需要相互引用,引用计数器也不会归零,从而导致内存泄漏。因此,Java选择了可达性分析算法来管理内存,以避免这类问题。
参考回答:在Java中,通过可达性分析算法判断堆上对象是否可回收。该算法从GC Root开始,检查对象是否可达。若对象不可达,则可能被回收。与引用计数法不同,可达性分析能处理循环引用问题,避免了内存泄漏。因此,Java采用可达性分析算法来管理内存。
4.JVM 中都有哪些引用类型?
在JVM中,存在多种引用类型,每种类型都有其特定的用途和特性。
- 强引用:JVM中最常见的引用类型,它表示对象被局部变量、静态变量等GC Root所关联。只要强引用存在,垃圾回收器就不会回收该对象。
- 软引用:一种相对较弱的引用关系。当系统内存足够时,软引用对象不会被回收;但在内存不足时,软引用对象会被回收,以释放内存空间。软引用在缓存框架中特别有用,因为它们允许系统在需要时释放不常使用的数据。
- 弱引用:与软引用类似,但弱引用的对象在垃圾回收时,无论内存是否充足,都会被回收。弱引用主要在
ThreadLocal
中使用,以确保线程局部变量的及时清理。 - 虚引用(也被称为幽灵引用或幻影引用):不会直接关联到对象。它的唯一用途是能在对象被垃圾回收时接收到通知。虚引用主要用于跟踪对象被垃圾回收的活动,例如,在直接内存管理中,虚引用可以用来监控和回收不再使用的内存对象。
- 终结器引用:涉及对象的finalize方法。当对象被垃圾回收器标记为需要回收时,终结器引用会将其放入Finalizer类的引用队列中。稍后,由FinalizerThread线程从队列中取出对象,并执行其finalize方法。然而,需要注意的是,依赖finalize方法进行资源清理是不推荐的,因为它不是确定性的,并且可能导致性能问题。
参考回答:在JVM中,有四种主要引用类型:强引用、软引用、弱引用和虚引用。强引用是最常见的,它确保对象不被垃圾回收。软引用和弱引用用于内存管理,软引用在内存不足时回收对象,而弱引用则无论内存是否充足都会回收对象。虚引用主要用于对象被回收时的通知。此外,还有终结器引用,它关联对象并执行finalize方法,但现代Java开发中较少使用。
5.ThreadLocal中为什么要使用弱引用?
在ThreadLocal中使用弱引用的主要目的是为了解决内存泄漏问题。通常,ThreadLocal实例作为静态成员变量存在,其生命周期与应用程序的运行时长相同。如果ThreadLocal中存储的对象使用强引用,那么这些对象将一直存在,直到ThreadLocal实例被显式地回收,这可能导致内存泄漏。
通过使用弱引用,当没有其他强引用指向ThreadLocal中的对象时,这些对象可以被垃圾回收器回收,从而减少了内存泄漏的风险。然而,仅仅使用弱引用并不足以完全解决对象回收的问题。因为ThreadLocal内部使用Entry对象来存储值,这些Entry对象本身持有对值的强引用,这意味着即使值对象被弱引用,Entry对象仍然阻止其被回收。
因此,为了避免潜在的内存泄漏,最佳实践是在不再需要ThreadLocal变量时,手动调用其remove()方法来清除条目。这确保了Entry对象被移除,从而允许值对象(如果只有弱引用指向它)被垃圾回收。之后,当解除了对ThreadLocal实例的所有强引用时,该实例本身也可以被回收,从而彻底释放了与其关联的资源。
参考回答:在ThreadLocal中使用弱引用是为了避免内存泄漏。弱引用允许对象在没有其他强引用时被垃圾回收,减少泄漏风险。但仅使用弱引用不足以完全解决回收问题,因为ThreadLocal内部的Entry对象持有强引用。因此,最佳实践是手动调用remove()来清除条目,确保对象可回收。这样,当ThreadLocal不再使用时,相关资源可以被彻底释放。
6.有哪些常见的垃圾回收算法?
常见的垃圾回收算法包括:
- 标记-清除算法(Mark-Sweep GC)
- 优点:实现相对简单,能够处理任意对象的回收。
- 缺点:
- 碎片化问题:由于回收过程中对象的移动和删除,可能导致内存碎片化,影响内存分配效率。
- 分配速度慢:在清除阶段,需要遍历整个堆来寻找空闲内存,导致内存分配速度较慢。
- 复制算法(Copying GC)
- 优点:分配速度快,因为每次只使用一半的内存空间进行分配,没有内存碎片问题。
- 缺点:内存使用效率低,因为每次只能使用一半的内存空间,限制了可用内存的范围。
- 标记-整理算法(Mark-Compact GC)
- 优点:避免了内存碎片问题,通过移动对象来整理内存,使得内存空间连续。
- 缺点:整理阶段需要高效的算法来移动对象,否则可能导致效率不高。
- 分代GC(Generational GC)
- 优点:根据对象的生命周期将内存划分为不同的代,针对不同代使用不同的垃圾回收算法,提高了垃圾回收的效率和灵活性。
- 缺点:实现复杂度较高,需要针对年轻代和老年代分别设计合适的垃圾回收策略。
在实际应用中,不同的垃圾回收器可能会结合使用上述算法,以适应不同的应用场景和性能需求。例如,在Java的HotSpot虚拟机中,就采用了分代GC的策略,其中年轻代通常使用复制算法,而老年代则使用标记-清除或标记-整理算法。
垃圾回收算法包括标记-清除(简单但可能导致内存碎片和分配速度慢)、复制(分配速度快但内存效率低)、标记-整理(避免碎片但整理可能效率不高)以及分代GC(灵活结合不同算法以满足性能需求)。
总结
JVM是Java程序的运行环境,负责字节码解释、内存管理、安全保障、多线程支持、性能监控和跨平台运行。本文主要介绍了JVM常见面试题目等内容,希望对大家有所帮助。