A公司一面:类加载的过程是怎么样的? 双亲委派的优点和缺点? 产生fullGC的情况有哪些? spring的动态代理有哪些?区别是什么? 如何排查CPU使用率过高?

摘要

A公司的面经

  • JVM的类加载的过程是怎么样的?
  • 双亲委派模型的优点和缺点?
  • 产生fullGC的情况有哪些?
  • spring的动态代理有哪些?区别是什么?
  • 如何排查CPU使用率过高?

JVM的类加载的过程是怎么样的?

这个问题有些抽象,是指要说出具体步骤,还是要深入每一步的细节?再次确认一下范围,给出的回答是,你自己了解多少就说多少。这就有意思,那我就凭自己的语言进行总结发挥了。

简述

类加载,是指JVM将.class文件的数据加载到内存中,并进行校验、解析以及初始化等一系列操作后,最终生成可被JVM直接使用的数据的过程。

解释

我们知道一个类在JVM的生命周期大致可以分为7个阶段:加载、验证、准备、解析、初始化、使用、卸载

类加载的过程,主要就是类生命周期的前5个阶段,所以类加载的主要步骤为:
加载、验证、准备、解析、初始化

因为【验证】、【准备】、【解析】有时候被统一称为链接阶段,因此有时候类加载也会被分三个步骤:加载、链接、初始化

加载(Loading)

第一步,加载,主要是通过类的全限定名(如:java.lang.String)获取类的二进制字节流,将字节流转换为JVM运行时的数据结构,在堆中生成一个 java.lang.Class 对象,作为该类的访问入口。

触发方式:

  • ClassLoader.getSystemClassLoader().loadClass("com.jimoer.Test")
  • Class.forName("com.jimoer.Test") // 加载并初始化
  • 创建实例(new Test())、调用静态方法或访问静态字段。

验证(Verification)

主要是校验,.class文件的正确性。
校验类的正确性(文件格式,元数据,字节码,二进制兼容性),保证类的结构符合JVM规范。

准备(Preparation)

为类的 静态变量(static 字段)分配内存并设置 默认值
这里只初始化类变量,即static变量,所以都是在方法区里面进行分配内存的。而实例变量是会在对象实例化的时候进行初始化的,并在Java堆里分配内存。

解析(Resolution)

将常量池中的 符号引用 转换为 直接引用。

把类的符号引用转为直接引用(类或接口、字段、类方法、接口方法、方法类型、方法句柄和访问控制修饰符7类符号引用)。

初始化(Initialization)

执行类的 初始化逻辑(即 <clinit>() 方法),完成静态变量赋值和静态代码块的执行。

java 复制代码
public class ClassInit {
    static int a = 10; // 准备阶段:a = 0;初始化阶段:a = 10
    static {
        a = 20; // 最终 a = 20
    }
}

双亲委派模型的优点和缺点?

Java应用是由 启动类加载器(Bootstrap Class Loader)扩展类加载器(Extension Class Loader)应用程序类加载器(Application Class Loader) ,这三类加载器互相配合来完成加载的,如果有自定义的类加载器,会先执行自定义的类加载器。

各种的类加载器之间的层次关系被称为类加载器的"双亲委派模型(Parents Delegation Model) "。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。

双亲委派模型的工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶端的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载才会尝试自己去完成加载。

双亲委派模型的优点

  1. 避免类的重复加载 。确保了不同类加载器加载的相同类是同一个实例,避免类型冲突(如java.lang.Object的唯一性)。
  2. 防止恶意代码篡改核心类,以及避免因类版本不一致导致的兼容性问题 。例如,攻击者无法通过自定义类加载器替换java.lang.String为恶意实现,从而保障JVM运行安全。
  3. 提高类加载效率。通过层级委托机制,减少重复搜索类路径(ClassPath)的次数。类加载器只需尝试一次父类加载器的加载,若失败再自行加载,避免了全盘扫描,提升性能。

双亲委派模型的缺点

  1. 限制自定义类的动态更新 。一旦类被父类加载器加载(如BootStrapClassLoader),即使类文件被修改,子类加载器也无法重新加载该类。
    场景 :
    在热部署(Hot Deployment)或插件化系统中,需要动态更新类时,双亲委派机制会阻碍实现。
    解决方案
    打破双亲委派机制:通过自定义类加载器绕过父类加载器,直接加载新版本类(例如Tomcat的WebAppClassLoader)。
    使用模块化框架:如OSGi,通过隔离类加载器实现模块的独立更新。
  2. 子类加载器加载的类无法被父类加载器访问(单向依赖)。
    场景
    在分布式系统中,可能需要跨类加载器共享数据,但父类加载器无法直接调用子类加载器加载的类。
    解决方案
    通过接口或抽象类设计:将公共方法定义为接口,由父类加载器加载接口,子类加载器实现具体逻辑。
    使用共享类路径:将需要共享的类放在父类加载器的类路径中。
  3. 类的可见性受限
    子类加载器加载的类无法被父类加载器访问(单向依赖)。
    场景:
    在分布式系统中,可能需要跨类加载器共享数据,但父类加载器无法直接调用子类加载器加载的类。
    解决方案:
    通过接口或抽象类设计:将公共方法定义为接口,由父类加载器加载接口,子类加载器实现具体逻辑。
    使用共享类路径:将需要共享的类放在父类加载器的类路径中。

产生FullGC的情况有哪些?

JVM触发FullGC的情况比较复杂也比较多,这里只说一些常见的,不能保证包含了全部产生FullGC的情况。

老年代空间不足

老年代存储空间不足

当老年代不足以容纳新对象或新生代晋升的对象时,会触发 Full GC

大对象直接分配到老年代(通过 -XX:PretenureSizeThreshold 阈值)。

Survivor 区无法容纳所有存活对象(担保机制触发晋升到老年代)。

老年代连续空间不足

即使老年代总空间足够,但碎片化严重(如大量小对象释放后未合并),无法分配大对象时,也会触发 Full GC。

元空间内存不足

当元空间(Metaspace)存储类元数据的空间不足时,JVM 会尝试通过 Full GC 回收无用的类元数据(如卸载不再使用的类)。

若仍不足,则抛出 OutOfMemoryError: Metaspace

System.gc() 被显式调用

显式调用 System.gc() 会请求 JVM 执行 Full GC(可通过 -XX:+DisableExplicitGC 禁用)。

OOM 前的最后尝试

当内存不足错误(OOM)触发。

当 JVM 即将抛出 OutOfMemoryError(如堆内存不足 Java heap space 或元空间不足 Metaspace)时,会尝试通过 Full GC 回收垃圾,若仍失败则抛出 OOM

自适应内存管理策略

若 JVM 的自适应内存管理(如 -XX:+UseAdaptiveSizePolicy)动态调整堆内存时发现内存紧张,可能触发 Full GC 重新平衡内存布局。

分布式缓存框架主动触发

某些分布式缓存框架(如 EhcacheRedis Java 客户端)在检测到堆内存占用过高时,会主动触发 Full GC 回收缓存对象。

然而这种做法需谨慎使用,可能导致性能问题,可能引发性能抖动甚至 STW(Stop-The-World)时间过长。

其他特殊场景

内存泄漏

未被正确释放的对象(如未关闭数据库连接、线程池未关闭、ThreadLocal 未 remove())导致老年代持续增长,最终触发 Full GC。

晋升年龄阈值过低

若新生代对象晋升到老年代的年龄阈值(-XX:MaxTenuringThreshold)设置过小,对象过早进入老年代,可能加速老年代空间耗尽。

Spring使用的动态代理有哪些?区别是什么?

Spring框架中主要使用两种动态代理技术:JDK动态代理CGLIB动态代理

JDK动态代理

基于接口实现:通过Java自带的 java.lang.reflect.Proxy 类动态生成代理类,代理类会实现目标类所实现的所有接口。

通过反射调用目标方法(Method.invoke()),性能相对较低。

适用场景

目标类实现了至少一个接口(如Service层接口)。

适用于需要兼容接口扩展性的场景。

优点

代码简洁:无需引入额外依赖。

兼容性强:适合有接口的设计模式。

缺点

局限性:目标类必须实现接口,否则无法使用。

性能问题:基于反射调用,性能较低(尤其是高频调用时)

无法获取实现类方法上的注解:当目标类实现接口时,代理对象可能无法直接获取实现类方法上的注解

CGLIB动态代理

通过CGLIB库(Code Generation Library)动态生成目标类的子类,重写方法实现代理

直接调用父类方法(非反射),性能较高。

需要引入CGLIB库(如 cglib 或 spring-core)。

spring-boot现在默认是使用CGLIB动态代理。

适用场景

目标类未实现任何接口(如Controller层或第三方类)
需要代理final类或方法以外的普通类

优点

灵活性高:无需目标类实现接口

性能更高:直接调用方法,避免反射开销

支持更复杂的代理需求:如代理无接口类

缺点

依赖第三方库:需要引入CGLIB依赖

限制:无法代理final类或final方法(因为无法继承和重写)

生成代理类较慢:字节码生成过程比JDK动态代理稍慢

如何排查CPU使用率过高?

首先登录到服务器上,看一下具体情况。

定位进程

登录服务器,执行top命令,查看CPU占用情况。

powershell 复制代码
PID    COMMAND      %CPU  TIME     #TH   #WQ  #PORT MEM    PURG   CMPRS  PGRP  PPID  STATE
41846  java        130.4  04:36:58 14/1  5    1695+ 618M-  6356K  99M-   41846 1     running
18122  top          7.5   00:04.68 1/1   0    32    4872K  0B     0B     18122 18106 running

通过Top命令,可以看到,占用CPU最高的是PID为41846的这个java进程。

定位线程

由于 Java 程序是单进程多线程模型,因此需要进一步定位具体是哪个线程的CPU占用最高。

同样是使用top命令:top -Hp 41846

powershell 复制代码
PID    COMMAND   %CPU TIME     #TH  #WQ  #POR MEM   PURG CMPRS PGRP  PPID
19327  java       130  00:12.59 30   1    141  154M  0B   123M- 41846 41846

top -Hp 41846命令可以看到,当前进程下,线程ID为19327的占用CPU最高。

定位代码

首先将线程ID转成16进制

powershell 复制代码
printf '%x\n' 19327

4b7f

接下来就可以通过jstack来查看栈信息

powershell 复制代码
jstack 41846 |grep -A 200 4b7f
powershell 复制代码
"main" #1 prio=5 os_prio=0 tid=0x00007f8a8c000000 nid=0x3048 runnable [0x00007f8a9c000000]
   java.lang.Thread.State: RUNNABLE
	at com.jimoer.app.CPUSpikeDemo.simulation(CPUSpikeDemo.java:12)
	at com.jimoer.app.CPUSpikeDemo.main(CPUSpikeDemo.java:18)

通过输出的栈信息日志,可以看到,是CPUSpikeDemo这个类的第18行可能有问题。

相关推荐
JavaGuide2 小时前
JDK 25(长期支持版) 发布,新特性解读!
java·后端
用户3721574261352 小时前
Java 轻松批量替换 Word 文档文字内容
java
白鲸开源2 小时前
教你数分钟内创建并运行一个 DolphinScheduler Workflow!
java
Java中文社群3 小时前
有点意思!Java8后最有用新特性排行榜!
java·后端·面试
代码匠心3 小时前
从零开始学Flink:数据源
java·大数据·后端·flink
间彧3 小时前
Spring Boot项目中如何自定义线程池
java
间彧4 小时前
Java线程池详解与实战指南
java
用户298698530144 小时前
Java 使用 Spire.PDF 将PDF文档转换为Word格式
java·后端
渣哥4 小时前
ConcurrentHashMap 1.7 vs 1.8:分段锁到 CAS+红黑树的演进与性能差异
java