文章目录
- 什么是类加载器?
- 说一说类加载机制
- 说说JAVA中的双亲委派机制过程
- 说说JVM内存结构
- JVM运行时数据区哪些是线程共享,哪些是线程私有
- JAVA怎么判断一个对象可回收?
- JAVA存在哪些引用类型
- 常见垃圾回收算法
- 什么情况发生FULLGC
什么是类加载器?
在JAVA中,类加载器(ClassLoader)是负责把.class字节码文件加载到JVM中,并生成Class对象的组件,是java中能够动态加载、运行代码的基础。
用一段代码说明类加载器作用
User user = new User();
背后JVM会做:
- JVM会检查User是否已经被加载?如果已经被加载过,直接复用加载过的Class< User >对象,否则执行类加载。下面说明类加载器加载过程
- 找到
User.class文件 - 读取字节码
- 生成也给
Java.lang.Class<User>对象 - 将这个Class对象交给JVM对象。
JAVA中内置类加载器大致分为三类:
- 启动类加载器(Bootstrap ClassLoader): 负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.* 开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。底层由C/C++实现。
- 扩展类加载器(Extension ClassLoader): 该加载器由
sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.* 开头的类),开发者可以直接使用扩展类加载器。 - 应用程序类加载器(Application ClassLoader): 该类加载器由
sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

说一说类加载机制
类加载机制是 JVM 在运行期间,将 .class 字节码加载到内存、生成 Class 对象,并完成验证、初始化等一系列过程的机制。一个类从出现到消失的声明周期为加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载,通常把前5个阶段合并为类加载过程。下面简述各个过程的作用:

- 加载: 查找并加载类的二进制数据。虚拟机完成完成以下三件事情
- 通过一个类的全限定名来获取其定义的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
- 验证:确保被加载类的正确性,为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范;元数据验证: 对字节码描述的信息进行语义分析以保证其描述的信息符合Java语言规范的要求。字节码验证: 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。符号引用验证: 确保解析动作能正确执行。
- 准备:正式为类变量分配内存并设置类变量初始值的阶段。
- 解析:虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
- 初始化:执行初始化方法
<clinit> ()方法的过程,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。类加载时机: 只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:- 创建类的实例,也就是new的方式
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如Class.forName("com.pdai.jvm.Test"))
- 初始化某个类的子类,则其父类也会被初始化
- Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
- 使用:类访问方法区内的数据结构的接口, 对象是Heap区的数据。
- 卸载:卸载类即该类的 Class 对象被 GC。卸载类需要满足以下要求
- 该类的所有实例对象都已被GC。
- 该类没有在任何地方被引用
- 该类的累加载器的实例已被GC。
说说JAVA中的双亲委派机制过程
双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派机制过程:
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
双亲委派机制的好处:
- 避免类的重复加载: 在 JVM 中,每个类都由一个唯一的全限定名和一个对应的类加载器确定,类加载器根据全限定名和类路径来确定类的位置。因此,在一个 JVM 实例中,如果有两个类加载器分别加载了同一个类,JVM 会认为这两个类是不同的,从而导致类型转换异常等问题。通过双亲委派机制,父类加载器在加载类之前会先委托给自己的父类加载器去加载,从而保证一个类在 JVM 中只会有一份,并且由其父类加载器所加载。
- 安全性考虑:JAVA核心类库(java.lang下的类)都是由启动类加载器加载,双亲委派模型限制自定义加载器加载核心类能力,有利于提高系统的安全型。
说说JVM内存结构
Java 程序在执行过程中需要内存区用于存储运行时产生的数据。运行时数据区为 Java 程序提供了内存,程序可以在其中分配内存以保存临时数据。JVM运行时数据区分为:程序计数器、本地方法栈、虚拟机栈、堆、方法区。以下介绍一下各个区域:
- 程序计数器: 记录当前线程 正在执行的字节码指令地址.保证线程切换后,线程能继续执行。
- 特点:
- 线程私有
- 占有内存小。
- 唯一一个不会产生OOM区域。
- 对于本地/java方法的区别
- 执行JAVA方法: 记录字节码地址。
- 执行本地(Native)方法: 值为空
- 特点:
- 虚拟机栈:记录JAVA方法的执行过程,保存栈帧。方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
- 特点:
- JVM 直接对虚拟机栈的操作只有两个:每个方法执行,伴随着入栈 (进栈/压栈),方法执行结束出栈
- 线程私有。
- 可能存在出现
StackOverflowError或者OutOfMemoryError异常。
- 栈帧中存储内容:
- 局部变量表
- 操作数栈
- 动态链接:指向运行时常量池的方法引用
- 方法返回地址:方法正常退出或异常退出的地址
- 一些附加信息
- 特点:
- 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在 HotSpot(主流JVM) 虚拟机中和 Java 虚拟机栈合二为一。
- 堆:JVM中最大的一块内存。存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
- 特点:
- 线程共享。
- GC管理的主要区域。
- 几乎所有对象都在堆中创建保存。
- 堆的结构:
- 新生代:新生代额外分为 Eden、Survivor S0、Survivor S1
- 老年代
- 特点:
- 方法区:主要存储已经被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。
- 特点:
- 线程共享。
- 方法区的大小决定了系统可以放多少个类,如果系统类太多,导致方法区溢出,虚拟机同样会抛出内存溢出错误。
- 存储内容
- 类的元数据: 包括类的完整结构,如类名、父类、实现的接口、访问修饰符,以及字段和方法的详细信息(名称、类型、修饰符等)。
- 方法的字节码:每个方法的原始指令序列
- 运行时常量池:每个类独有的,由 Class 文件中的常量池转换而来,用于存放编译期生成的各种字面量和对类型、字段、方法的符号引用。
- 特点:
JVM运行时数据区哪些是线程共享,哪些是线程私有
线程私有:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享: - 堆
- 方法区
JAVA怎么判断一个对象可回收?
JAVA使用可达性分析算法来判断一个对象是否可回收。可达性算法通过GC Roots(这里包含的对象一定存活)作为起始点进行搜索,能够到达的对象都是成活,不可达的对象认为可被回收。

在 Java 中 GC Roots 一般包含以下内容:
- 虚拟机栈中 /本地方法栈引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
JAVA存在哪些引用类型
在 Java 中,对象的引用强度 被划分为 4 种,主要用于 内存管理和垃圾回收(GC)。这 4 种引用从强到弱依次是:
强引用
强引用:被强引用关联的对象不会被回收,这也是最常用的引用类型。
// user就是一个强引用
User user = new User()
特点:
- 只要强引用存在,对象就不会被GC。
- 即使发生 OOM,GC 也不会回收强引用对象。
- 是程序中最稳定、最安全的引用方式。
软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
SoftReference<User> ref = new SoftReference<>(new User());
特点:
- 内存不足时才会被GC回收。
弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
WeakReference<User> ref = new WeakReference<>(new User());
特点:
- 只要发生GC,就会被回收
- 生命周期极短
虚引用
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
ReferenceQueue<User> queue = new ReferenceQueue<>();
PhantomReference<User> ref =
new PhantomReference<>(new User(), queue);
特点:
- get()永远返回null。
- 不影响对象声明周期
- 主要用于对象被回收前得到通知。
常见垃圾回收算法
标记-清除算法

分为两步:
- 标记:可达性分析算法,标记所有可达对象
- 清除:回收所有未被标记的对象。
优点:
- 实现对象
- 不需要移动对象。
缺点: - 会产生大量内存碎片
- 大量内存碎片可能到达大对象无法分配。
- 清除效率低。
复制算法

思想:
- 将内存分为两块区域
- 每次只使用其中一块
- 每次GC前将使用区域中存活对象标记出来,把存活对象复制到另一块区域
- 清除原使用区域。
优点:
- 没有内存碎片
- 分配效率高
缺点:
- 空间利用率低
- 存活对象多,复制成本高。
标记 - 整理算法

算法思路:
- 标记:可达性分析算法,标记所有可达对象
- 整理:将存活对象 向一端移动
- 清除:按顺序排列,清理边界外内存
优点:
- 无内存碎片
- 空间利用率高
缺点: - 移动对象成本高
- 移动对象过程中需要暂停所有线程执行。
分代收集算法
算法思想:
- 基于对象生命周期的假设,绝大多数对象朝生夕死
- 对不同区域采取不同垃圾回收算法,新生代采用
复制算法,老年代使用标记-清除 / 标记整理算法。
优点:
- 符合对象存活规律
- GC效率高
什么情况发生FULLGC
Full GC(完全垃圾回收)是 Java 中的一个重要垃圾回收阶段,它会回收 整个堆内存 ,包括 新生代 和 老年代 。触发 Full GC 的条件通常比 Young GC 更为复杂且影响较大,因为它会导致 JVM 停顿时间较长。下面是一些常见的触发 Full GC 的情况
- 老年代空间不足:老年代空间不足的常见场景大对象直接进入老年代、长期存活的对象进入老年代等。
- 直接调用
System.gc(): JVM 只是建议,但大多数情况下会触发 Full GC - 空间分配担保失败:使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
- 方法区 / 元空间不足:当大量动态生成类(CGLIB、JDK Proxy)造成方法区/原空间内存不足,会尝试FULL GC回收无用类。
- 老年代碎片过多:在一些垃圾收集器(CMS),如果老年代中的碎片过多,导致大对象无法找到连续的内存空间分配,也可能会触发 Full GC 以进行内存压缩.