Java面试黄金宝典12
1. 什么是 Java 类加载机制
- 定义
Java 类加载机制是 Java 程序运行时的关键环节,其作用是把类的字节码文件(.class
文件)加载到 Java 虚拟机(JVM)中,并且将字节码文件转化为 JVM 能够识别的类对象。整个类加载过程主要包含加载、连接(验证、准备、解析)和初始化三个阶段。
-
原理
- 加载阶段:
- 此阶段会通过类的全限定名来获取定义该类的二进制字节流。获取途径较为多样,既可以从本地文件系统读取,也能从网络下载,还能通过动态代理生成等方式得到。
- 接着将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 最后在内存中生成一个代表这个类的
java.lang.Class
对象,后续程序就可以通过这个对象来访问该类的相关信息。
- 连接阶段:
- 验证:该步骤是为了确保被加载的类的字节码符合 JVM 的规范,不会对 JVM 的安全造成危害。验证内容涵盖文件格式验证(如检查字节码文件的魔数、版本号等是否正确)、元数据验证(检查类的继承关系、方法签名等是否合法)、字节码验证(检查字节码指令的语义是否正确)以及符号引用验证(检查符号引用所指向的类、方法等是否存在)。
- 准备:这一阶段会为类的静态变量分配内存,并将其初始化为默认值。例如,对于
int
类型的静态变量,会初始化为 0;对于引用类型的静态变量,会初始化为null
。 - 解析:将常量池中的符号引用替换为直接引用。符号引用是一种以符号形式表示的引用,它并不直接指向具体的内存地址;而直接引用则是直接指向目标的内存地址。例如,在类的方法中调用另一个类的方法时,最初使用的是符号引用,解析阶段会将其转换为实际的内存地址引用。
- 初始化阶段:为类的静态变量赋予正确的初始值,执行类的静态代码块。在这个阶段,会按照代码中定义的顺序依次执行静态变量的赋值语句和静态代码块。
- 要点
- 类加载过程是按序逐步推进的,前一个阶段完成之后才会进入下一个阶段。不过,在实际的 JVM 实现中,部分阶段可能会存在交叉进行的情况。
- 类加载是按需进行的,只有当程序需要使用某个类时,才会触发该类的加载过程。
- 应用
Java 类加载机制赋予了 Java 程序良好的动态扩展性。例如,在 Java Web 开发中,可以在运行时动态加载新的 Servlet 类,实现系统功能的动态更新。同时,不同的类加载器可以实现不同的加载策略,从而满足不同的应用场景需求,像在模块化开发中,每个模块可以使用独立的类加载器来加载自身的类。
2. 什么是双亲委派
- 定义
双亲委派是 Java 类加载机制中的一种重要设计模式。当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每一个层次的类加载器都是如此,所以所有的加载请求最终都会传送到顶层的启动类加载器中。只有当父类加载器反馈自己无法完成这个加载请求(即在它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
- 原理
这种机制的核心目的在于保证 Java 类的加载具有层次性和安全性。通过将类加载请求向上委派,避免了类的重复加载。例如,如果多个类加载器都可以加载同一个类,采用双亲委派机制可以确保该类只被加载一次。同时,它也保证了 Java 核心类库的安全性,因为核心类库总是由启动类加载器加载,不会被用户自定义的类加载器所干扰。比如,用户无法通过自定义类加载器来加载一个与 Java 核心类库同名的类,从而防止恶意代码替换核心类。
- 要点
- 每个类加载器都有一个父类加载器(除了启动类加载器,它没有父类加载器)。类加载器之间形成了一种父子层次关系。
- 类加载请求从下往上传递,加载过程从上往下进行。即子加载器先将请求传递给父加载器,父加载器尝试加载,若失败则子加载器再尝试。
- 应用
双亲委派模型是 Java 类加载机制的默认模型,但并不是强制要求。在某些特殊场景下,可以通过自定义类加载器来打破双亲委派模型。例如,在 Java Web 容器(如 Tomcat)中,为了实现不同 Web 应用之间的类隔离,会自定义类加载器并打破双亲委派模型,使得每个 Web 应用可以有自己独立的类加载空间。
3. 什么是破坏双亲委派模型
- 定义
破坏双亲委派模型指的是不遵循双亲委派的规则来进行类的加载。在某些情况下,由于业务需求或特殊的设计,需要让类加载器不先将类加载请求委派给父类加载器,而是自己先尝试加载类。
- 原理
破坏双亲委派模型通常是为了满足一些特殊的需求,例如实现热部署、加载不同版本的类库等。通过自定义类加载器,重写 loadClass
方法,改变类加载的顺序。比如,在热部署场景中,当类的代码发生修改后,需要重新加载该类,此时可以自定义类加载器,直接加载新的类字节码,而不经过父类加载器。
- 要点
- 破坏双亲委派模型需要自定义类加载器。自定义类加载器需要继承
ClassLoader
类,并根据需求重写loadClass
方法。 - 重写
loadClass
方法时需要谨慎,避免破坏 Java 类加载的安全性和稳定性。例如,如果重写不当,可能会导致类的重复加载,或者出现类冲突的问题。
- 应用
常见的破坏双亲委派模型的场景包括 OSGi 框架,它通过自定义类加载器实现了模块的动态加载和卸载,允许不同模块使用不同版本的类库。在 OSGi 中,每个模块都有自己独立的类加载器,模块之间的类加载是相互隔离的,从而实现了更灵活的类加载机制。
4. 什么是内存泄露
- 定义
内存泄露指的是程序在运行过程中,由于某些原因导致一些不再使用的对象无法被垃圾回收器回收,从而占用了大量的内存空间,最终可能导致内存溢出(OutOfMemoryError)。
- 原理
内存泄露通常是由于对象的引用没有被正确释放造成的。例如,在集合中添加了对象,但在对象不再使用时没有将其从集合中移除,导致集合一直持有对象的引用,垃圾回收器无法回收这些对象。另外,静态变量持有对象引用也可能导致内存泄露,因为静态变量的生命周期与类的生命周期相同,只要类不被卸载,静态变量所引用的对象就不会被回收。
- 要点
- 内存泄露会导致内存占用不断增加,最终影响程序的性能和稳定性。随着内存的不断消耗,程序可能会出现卡顿、响应缓慢等问题,甚至会因为内存不足而崩溃。
- 常见的内存泄露场景包括静态集合持有对象引用、未关闭的资源(如文件、数据库连接、网络连接等)、内部类持有外部类的引用等。
- 应用
为了避免内存泄露,需要养成良好的编程习惯,及时释放不再使用的对象引用,关闭不再使用的资源。同时,可以使用一些工具(如 VisualVM、MAT 等)来检测和分析内存泄露问题。例如,VisualVM 可以实时监控 JVM 的内存使用情况,通过查看堆内存的变化趋势,发现是否存在内存泄露的迹象;MAT(Memory Analyzer Tool)可以对堆转储文件进行深入分析,找出导致内存泄露的对象和引用关系。
5. 如何进行 JVM 调优
- 定义
JVM 调优是指通过调整 JVM 的参数和配置,来优化 Java 程序的性能。主要包括以下几个方面:
- 调整堆内存大小:合理设置堆内存的初始大小和最大大小,避免频繁的垃圾回收。如果堆内存设置过小,会导致频繁的垃圾回收,影响程序的性能;如果设置过大,会占用过多的系统资源,并且可能会导致垃圾回收时间过长。
- 选择合适的垃圾回收器:根据应用程序的特点和性能需求,选择合适的垃圾回收器,如 Serial、Parallel、CMS、G1 等。不同的垃圾回收器适用于不同的场景,例如 Serial 垃圾回收器适用于单线程环境和小型应用;Parallel 垃圾回收器适用于多 CPU 环境下对吞吐量要求较高的应用;CMS 垃圾回收器适用于对响应时间要求较高的应用;G1 垃圾回收器适用于大内存、多 CPU 环境下的应用。
- 调整垃圾回收参数:根据垃圾回收器的不同,调整相关的参数,如新生代和老年代的比例、垃圾回收的触发阈值等。例如,对于 G1 垃圾回收器,可以调整
-XX:MaxGCPauseMillis
参数来控制最大垃圾回收停顿时间。
- 原理
JVM 调优的核心是根据应用程序的内存使用情况和性能需求,合理分配资源,减少垃圾回收的频率和时间,提高程序的响应速度和吞吐量。通过调整堆内存大小,可以避免因内存不足导致的频繁垃圾回收;选择合适的垃圾回收器可以根据应用的特点优化垃圾回收的效率;调整垃圾回收参数可以进一步优化垃圾回收的过程。
- 要点
- JVM 调优需要根据具体的应用场景进行,没有通用的调优方案。不同的应用程序在内存使用模式、并发程度等方面存在差异,因此需要根据实际情况进行调优。
- 调优过程中需要不断地进行测试和监控,根据测试结果进行调整。可以使用一些工具(如 VisualVM、JConsole、JProfiler 等)来监控 JVM 的性能指标,如堆内存使用情况、垃圾回收频率和时间等,以便更好地进行调优。
- 应用
在进行 JVM 调优时,可以采用逐步调优的方法,每次只调整一个参数,然后进行测试和监控,观察性能的变化。同时,要注意参数之间的相互影响,避免出现冲突。例如,调整堆内存大小可能会影响垃圾回收的频率和时间,进而影响程序的性能。另外,还可以结合代码优化来提高程序的性能,如减少对象的创建、优化算法等。
6. 一个服务系统,经常出现卡顿,分析原因,发现 Full GC 时间太长,新生代和老生代大小之比为 1:9,如何进行调优,举例说明
- 定义
Full GC 时间太长可能是由于老年代空间过大,导致垃圾回收时需要处理的对象过多。可以尝试以下调优方法:
- 调整新生代和老生代的比例:增大新生代的比例,例如将新生代和老生代的比例调整为 1:4 或 1:3,这样可以让更多的对象在新生代被回收,减少对象进入老年代的概率。因为新生代的垃圾回收速度相对较快,让更多对象在新生代被回收可以减少 Full GC 的频率和时间。
- 选择合适的垃圾回收器:如果当前使用的垃圾回收器不适合应用程序的特点,可以考虑更换垃圾回收器。例如,如果应用程序对响应时间要求较高,可以选择 CMS 或 G1 垃圾回收器。CMS 垃圾回收器以获取最短回收停顿时间为目标,G1 垃圾回收器可以更好地控制垃圾回收的停顿时间。
- 优化对象的生命周期:尽量减少大对象的创建,及时释放不再使用的对象,避免对象过早进入老年代。例如,在处理大量数据时,可以采用分批处理的方式,避免一次性创建过大的对象。
- 原理
通过调整新生代和老生代的比例,可以让垃圾回收更加高效。增大新生代的比例可以让更多的对象在新生代被回收,减少老年代的垃圾回收压力。选择合适的垃圾回收器可以根据应用程序的特点和性能需求,提高垃圾回收的效率。优化对象的生命周期可以减少对象进入老年代的数量,从而减少 Full GC 的频率和时间。
- 要点
- 调优过程需要逐步进行,每次只调整一个参数,然后进行测试和监控,观察性能的变化。这样可以准确地判断每个参数调整对性能的影响。
- 不同的应用程序可能需要不同的调优方案,需要根据实际情况进行调整。例如,对于内存使用较为稳定的应用程序,可能只需要调整新生代和老生代的比例即可;而对于对响应时间要求极高的应用程序,可能需要更换垃圾回收器。
- 应用
例如,对于一个 Web 应用程序,可以通过以下 JVM 参数进行调优:
plaintext
java -Xms512m -Xmx512m -XX:NewRatio=3 -XX:+UseConcMarkSweepGC YourMainClass
其中,-Xms
和 -Xmx
分别设置堆内存的初始大小和最大大小为 512MB,这样可以避免堆内存的动态扩展,减少垃圾回收的开销。-XX:NewRatio=3
表示新生代和老生代的比例为 1:3,增大了新生代的比例。-XX:+UseConcMarkSweepGC
表示使用 CMS 垃圾回收器,该垃圾回收器可以在较短的时间内完成垃圾回收,减少对应用程序的影响。
7. 什么是 jstat
、jmap
、jps
、jinfo
、jconsole
- 定义
jstat
:是一个用于监控 JVM 统计信息的工具。它可以实时显示堆内存各区域(如 Eden 区、Survivor 区、老年代)的使用情况、垃圾回收的统计信息(如垃圾回收次数、垃圾回收时间)等。通过jstat
可以了解 JVM 的内存使用和垃圾回收情况,为 JVM 调优提供依据。jmap
:用于生成堆转储快照(Heap Dump),可以查看堆内存中对象的分布情况,帮助分析内存泄露和内存溢出问题。堆转储快照是一个二进制文件,包含了某一时刻堆内存中所有对象的信息。可以使用一些工具(如 MAT)对堆转储快照进行分析,找出占用大量内存的对象和可能存在的内存泄露问题。jps
:是一个简单的进程查看工具,用于列出当前系统中所有正在运行的 Java 进程,并显示进程的 ID 和主类名。通过jps
可以快速找到要监控的 Java 进程的 ID,以便后续使用其他工具进行监控和分析。jinfo
:可以查看和修改 JVM 的运行时参数,帮助了解 JVM 的配置信息。例如,可以使用jinfo
查看当前 JVM 使用的垃圾回收器、堆内存大小等参数,也可以在运行时动态修改一些参数。jconsole
:是一个图形化的监控工具,提供了一个直观的界面来监控 JVM 的性能指标,如堆内存使用情况、线程状态、类加载情况等。通过jconsole
可以实时观察 JVM 的运行状态,发现潜在的性能问题。
- 原理
这些工具都是基于 JVM 的管理接口(JMX)实现的,通过与 JVM 进行通信,获取 JVM 的运行时信息。JMX 是 Java 平台提供的一种管理和监控 Java 应用程序的标准接口,它允许开发者通过远程或本地的方式对 JVM 进行管理和监控。
- 要点
- 这些工具可以帮助开发者监控和分析 JVM 的性能问题。不同的工具适用于不同的场景,需要根据具体需求选择合适的工具。
- 在使用这些工具时,需要确保目标 Java 进程已经开启了 JMX 服务,否则可能无法获取到相关信息。
- 应用
可以结合使用这些工具来进行全面的 JVM 性能分析。例如,使用 jps
找到要监控的 Java 进程 ID,然后使用 jstat
实时监控该进程的垃圾回收情况,使用 jmap
生成堆转储快照,使用 jinfo
查看和修改 JVM 参数,最后使用 jconsole
进行图形化的监控。同时,还可以使用一些第三方工具(如 VisualVM、JProfiler 等)来提供更丰富的监控和分析功能。
8. 如何进行 JVM 参数设置,推荐优化配置
- 定义
JVM 参数可以通过命令行或配置文件进行设置。在命令行中,可以使用 -XX
或 -X
开头的参数来设置 JVM 的各种选项。以下是一些推荐的优化配置:
- 堆内存设置:
-Xms
:设置堆内存的初始大小。例如,-Xms512m
表示堆内存的初始大小为 512MB。-Xmx
:设置堆内存的最大大小。通常将-Xms
和-Xmx
设置为相同的值,避免堆内存的动态扩展,减少垃圾回收的开销。例如,-Xmx512m
表示堆内存的最大大小为 512MB。
- 新生代和老年代设置:
-XX:NewRatio
:设置新生代和老年代的比例。例如,-XX:NewRatio=3
表示新生代和老年代的比例为 1:3。-XX:SurvivorRatio
:设置新生代中 Eden 区和 Survivor 区的比例。例如,-XX:SurvivorRatio=8
表示 Eden 区和 Survivor 区的比例为 8:1。
- 垃圾回收器设置:
-XX:+UseSerialGC
:使用 Serial 垃圾回收器,适用于单线程环境和小型应用。-XX:+UseParallelGC
:使用 Parallel 垃圾回收器,适用于多 CPU 环境下对吞吐量要求较高的应用。-XX:+UseConcMarkSweepGC
:使用 CMS 垃圾回收器,适用于对响应时间要求较高的应用。-XX:+UseG1GC
:使用 G1 垃圾回收器,适用于大内存、多 CPU 环境下的应用。
- 原理
通过合理设置 JVM 参数,可以根据应用程序的特点和性能需求,优化内存分配和垃圾回收的策略,提高程序的性能。例如,合理设置堆内存大小可以避免因内存不足导致的频繁垃圾回收;选择合适的垃圾回收器可以根据应用的特点优化垃圾回收的效率。
- 要点
- JVM 参数的设置需要根据具体的应用场景进行调整,不同的应用程序可能需要不同的参数配置。例如,对于对响应时间要求极高的 Web 应用程序,可能需要选择 CMS 或 G1 垃圾回收器;而对于对吞吐量要求较高的批处理应用程序,可能选择 Parallel 垃圾回收器更合适。
- 在设置参数时,需要注意参数之间的相互影响,避免出现冲突。例如,调整堆内存大小可能会影响垃圾回收的频率和时间,进而影响程序的性能。
- 应用
例如,对于一个对响应时间要求较高的 Web 应用程序,可以使用以下 JVM 参数配置:
plaintext
java -Xms2048m -Xmx2048m -XX:NewRatio=3 -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled YourMainClass
其中,堆内存的初始大小和最大大小都设置为 2GB,新生代和老年代的比例为 1:3,Eden 区和 Survivor 区的比例为 8:1,使用 CMS 垃圾回收器,并启用并行标记,以进一步减少垃圾回收的停顿时间。
9. 什么是内存分配与回收策略
- 定义
内存分配与回收策略是指 JVM 如何为对象分配内存以及如何回收不再使用的内存。主要包括以下几个方面:
- 对象优先在 Eden 区分配:大多数情况下,新创建的对象会优先在新生代的 Eden 区分配内存。因为 Eden 区是新生代中最大的一块内存区域,新对象通常生命周期较短,在 Eden 区分配内存可以提高内存分配的效率。
- 大对象直接进入老年代:如果对象的大小超过了一定的阈值,会直接在老年代分配内存。大对象的创建和销毁会对内存分配和垃圾回收产生较大的影响,将其直接分配到老年代可以避免在新生代频繁进行垃圾回收时对大对象的复制操作。
- 长期存活的对象将进入老年代:对象在 Survivor 区经过多次垃圾回收后,如果仍然存活,会被晋升到老年代。JVM 会为每个对象维护一个年龄计数器,对象每经过一次垃圾回收,年龄就会加 1,当年龄达到一定阈值时,就会被晋升到老年代。
- 动态对象年龄判定:如果 Survivor 区中相同年龄的所有对象大小的总和大于 Survivor 区空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。这是一种动态调整对象晋升到老年代的策略,可以根据实际的内存使用情况灵活调整对象的晋升规则。
- 原理
这些策略的目的是根据对象的大小和存活时间,合理地分配内存,减少垃圾回收的开销,提高内存的使用效率。通过将不同类型的对象分配到不同的内存区域,并根据对象的存活情况进行晋升和回收,可以使垃圾回收更加高效。
- 要点
- 不同的对象根据其特点会被分配到不同的内存区域。例如,新创建的小对象优先在 Eden 区分配,大对象直接进入老年代。
- 垃圾回收器会根据对象的年龄和内存使用情况,决定对象是否需要晋升到老年代。动态对象年龄判定策略可以根据实际情况灵活调整对象的晋升规则。
- 应用
了解内存分配与回收策略可以帮助开发者优化对象的创建和使用,避免创建过多的大对象,减少对象过早进入老年代的概率,从而提高程序的性能。例如,在编写代码时,可以尽量避免创建过大的数组或集合,以减少大对象的产生。同时,及时释放不再使用的对象,避免对象在内存中长时间占用空间。
10. 一般 Java 堆是如何实现的
- 定义
Java 堆是 JVM 中用于存储对象实例的内存区域,一般由以下几个部分组成:
- 新生代:用于存储新创建的对象,通常分为 Eden 区和两个 Survivor 区。大多数对象在新生代创建和销毁,因此新生代的垃圾回收比较频繁。Eden 区是新对象分配内存的主要区域,当 Eden 区内存不足时,会触发 Minor GC(新生代垃圾回收)。在 Minor GC 过程中,存活的对象会被复制到其中一个 Survivor 区,当这个 Survivor 区满时,存活的对象会被晋升到老年代或者另一个 Survivor 区。
- 老年代:用于存储长期存活的对象,当对象在新生代经过多次垃圾回收后仍然存活,会被晋升到老年代。老年代的垃圾回收频率相对较低,但回收时间较长。因为老年代中的对象通常生命周期较长,垃圾回收时需要处理的对象数量较多。
- 永久代 / 元空间:在 Java 8 之前是永久代,用于存储类的元数据信息,如类的结构、方法、字段、常量池等;从 Java 8 开始,永久代被元空间取代,元空间使用本地内存。元空间的大小不再受 JVM 堆大小的限制,减少了因永久代空间不足导致的
OutOfMemoryError: PermGen space
错误。
- 原理
Java 堆的实现是基于分代收集的思想,根据对象的存活时间将内存划分为不同的区域,不同的区域采用不同的垃圾回收算法,以提高垃圾回收的效率。新生代中的对象生命周期较短,采用复制算法进行垃圾回收,效率较高;老年代中的对象生命周期较长,采用标记 - 清除或标记 - 整理算法进行垃圾回收。
- 要点
- 新生代和老年代的大小和比例可以通过 JVM 参数进行调整。例如,通过
-XX:NewRatio
参数可以设置新生代和老年代的比例。 - 永久代 / 元空间的使用在 Java 8 前后有所变化,需要注意版本的差异。在 Java 8 及以后的版本中,应使用元空间相关的参数进行配置。
- 应用
不同的 JVM 实现可能会对 Java 堆的结构和实现方式有所不同,但总体上都遵循分代收集的原则。了解 Java 堆的实现方式可以帮助开发者更好地理解 JVM 的内存管理机制,进行有效的内存优化和调优。例如,根据应用程序的特点合理调整新生代和老年代的比例,选择合适的垃圾回收器等。
友情提示:本文已经整理成文档,可以到如下链接免积分下载阅读
https://download.csdn.net/download/ylfhpy/90523744