类加载
- Java的类加载过程是将.class文件中的二进制数据读入内存,并最终形成可被虚拟机直接使用的Java类型。这一过程主要分为 加载、连接(验证、准备、解析)、初始化三大阶段。
加载(Loading):
- 这是类加载的第一步,由类加载器 (ClassLoader) 完成。
- 通过类的全限定名获取定义此类的二进制字节流(可以从磁盘、JAR 包、网络等获取)。
- 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存(堆)中生成一个代表该类的 java.lang.Class 对象,作为方法区这些数据的访问入口。
连接 (Linking):
- 连接阶段负责将类合并到JVM的运行时状态中,分为三个子步骤:
- 验证 (Verification):确保字节流信息符合当前虚拟机的要求,不会危害虚拟机安全(如检查文件格式、元数据、字节码语义等)。
- 准备 (Preparation):为类的静态变量 (static) 分配内存,并设置默认初始值(如int 设为 0,引用类型设为 null)。
注意:此时并未执行 Java 代码。如果是 final static 常量,则会在此阶段直接赋值。
- 解析 (Resolution):将常量池内的符号引用(如方法名、类名字符串)替换为直接引用(直接指向目标的指针或偏移量)。
初始化 (Initialization):
- 这是类加载的最后一步,也是真正开始执行Java 字节码的阶段。初始化阶段就是执⾏类构造器⽅法的过程。
- 核心逻辑:执行类构造器 () 方法。
- 主要任务:1. 执行静态变量的显式赋值语句(如 static int i = 10;)。 2. 执行静态代码块 (static { ... })。
- 触发时机:只有当类被“主动使用”时才会触发(如 new 实例化对象、访问静态字段、调用静态方法、反射等)。
双亲委派模型
什么是双亲委派模型?
- 如果⼀个类加载器收到了类加载的请求,它⾸先不会⾃⼰去尝试加载这个类,⽽是把这个请求委派给⽗类加载器去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当⽗加载器反馈⾃⼰⽆法完成这个加载请求(它的搜索范围中没有找到所需的类)时,⼦加载器才会尝试⾃⼰去完成加载。
- 类加载器的等级制度
- 站在Java虚拟机的⻆度来看,只存在两种不同的类加载器:⼀种是启动类加载器(BootstrapClassLoader),这个类加载器使⽤C++语⾔实现,是虚拟机⾃⾝的⼀部分;另外⼀种就是其他所有的类加载器,这些类加载器都由Java语⾔实现,独⽴存在于虚拟机外部,并且全都继承⾃抽象类java.lang.ClassLoader。
- 工作原理(三步走),当一个类加载器接收到加载请求时:
- 向上委托:它先看自己有没有加载过这个类。如果没有,它不会自己去读字节码,而是调用父加载器的loadClass方法。这个过程一直递归到顶层的Bootstrap ClassLoader。
- 顶层检查:顶层加载器尝试在自己的搜索范围(如核心类库)中寻找该类。
- 向下接力:如果父加载器没找到,它会告诉子加载器:“我搞不定,你自己试吧”。这时子加载器才会调用自己的findClass 方法尝试加载。如果全链条都找不到,最终抛出ClassNotFoundException。
双亲委派模型的优点:
- 避免重复加载类:⽐如A类和B类都有⼀个⽗类C类,那么当A启动时就会将C类加载起来,那么在B类进⾏加载时就不需要在重复加载C类了。
- 安全性:使⽤双亲委派模型也可以保证了Java的核⼼API不被篡改,如果没有使⽤双亲委派模型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为java.lang.Object类的话,那么程序运⾏的时候,系统就会出现多个不同的Object类,⽽有些Object类⼜是用户⾃⼰提供的因此安全性就不能得到保证了。
模型的不足:
- 虽然这个模型很棒,但在某些特殊场景下需要“逆向操作”:
- SPI (Service Provider Interface):比如 JDBC。核心类(由 Bootstrap 加载)需要调用第三方厂商实现的驱动代码(由 AppClassLoader 加载)。此时父加载器需要请求子加载器去加载类,这破坏了单向向上的委派。
- 热部署/热插拔:像OSGi或某些应用服务器(Tomcat),为了实现模块独立和动态更新,会自定义复杂的类加载机制。
垃圾回收机制
- 在Java中,所有的对象都是要存在内存中的(也可以说内存中存储的是⼀个个对象),因此我们将内存回收,也可以叫做死亡对象的回收。
死亡对象的判断算法:
- 可达性分析算法 (Reachability Analysis):Java并不使用“引用计数法”,因为它无法解决对象间循环引用的问题。Java使用的是可达性分析:从一系列被称为 GC Roots 的根对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连,则判定该对象为可回收。
- 常作为 GC Roots 的对象包括:
- 虚拟机栈中引用的对象(局部变量)。
- 方法区中类静态属性、常量引用的对象。
- 本地方法栈(Native Method)引用的对象。
回收算法
- 标记-清除 (Mark-Sweep):
- 过程:标记出所有需要回收的对象,然后统一回收。
- 问题:会产生大量不连续的内存碎片,导致大对象无法找到足够的连续空间。
- 标记-复制 (Copying):
- 过程:将内存分为容量相等的两块,每次只使用一块。垃圾回收时,将存活对象复制到另一块,然后清空当前块。
- 优点:效率高,无碎片。
- 缺点:内存利用率低(只有 50%)。
- 标记-整理 (Mark-Compact):
- 过程:标记后,让所有存活对象向内存一端移动,然后直接清理掉边界以外的内存。
- 优点:避免了碎片,也无需浪费一半空间。
- 分代算法:
- 当前JVM垃圾收集都采⽤的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为⼏块。⼀般是把Java堆分为新⽣代和⽼年代。在新⽣代中,每次垃圾回收都有⼤批对象死去,只有少量存活,因此我们采⽤复制算法;⽽⽼年代中对象存活率⾼、没有额外空间对它进⾏分配担保,就必须采⽤"标记-清理"或者"标记-整理"算法。
- 新生代 (Young Generation):绝大多数创建的新对象都在这里诞生。
- 结构:分为 Eden、Survivor From、Survivor To(比例通常是 8:1:1)。
- 算法:采用复制算法。因为存活对象少,复制成本极低。
- 老年代 (Old Generation):存放生命周期长的对象(比如大对象、经过多次 GC 依然存活的对象)。
- 请问了解Minor GC和Full GC么,这两种GC有什么不⼀样吗
- Minor GC⼜称为新⽣代GC:指的是发⽣在新⽣代的垃圾收集。因为Java对象⼤多都具备朝⽣夕灭的特性,因此MinorGC(采⽤复制算法)⾮常频繁,⼀般回收速度也⽐较快。
- Full GC⼜称为⽼年代GC或者Major GC:指发⽣在⽼年代的垃圾收集。出现了Major GC,经常会伴随⾄少⼀次的Minor GC(并⾮绝对,在Parallel Scavenge收集器中就有直接进⾏Full GC的策略选择过程)。Major GC的速度⼀般会⽐Minor GC慢10倍以上。
垃圾回收器
- 垃圾回收器是算法的具体实现。不同版本和场景下,选择也不同:
- CMS (Concurrent Mark Sweep):追求最短回收停顿时间。它在标记和清除时可以和用户线程并发执行,适合对响应速度有要求的互联网应用。
- G1 (Garbage First):JDK 9 以后的默认选择。它打破了物理隔离的新生代/老年代概念,将堆拆分为多个 Region。它会计算哪个区域回收收益最高,优先回收那块区域。
- ZGC:现代顶级回收器。通过“着色指针”和“读屏障”技术,无论堆内存有多大,停顿时间(STW)都能控制在 1ms 以内。
补充:什么是STW (Stop The World)?
- 这是GC过程中的一个痛点。在执行某些回收逻辑时,必须暂停所有的用户线程,以确保在标记或移动对象时,内存状态是静止的。优化GC的核心目标,就是尽可能减少STW的时长和频率。
posted @
2026-01-19 23:05
我会替风去
阅读(
6)
评论()
收藏
举报