"/>
侧边栏壁纸
博主头像
PySuper 博主等级

千里之行,始于足下

  • 累计撰写 286 篇文章
  • 累计创建 17 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

Java JVM-2

PySuper
2022-12-05 / 0 评论 / 0 点赞 / 9 阅读 / 0 字
温馨提示:
所有牛逼的人都有一段苦逼的岁月。 但是你只要像SB一样去坚持,终将牛逼!!! ✊✊✊

类初始化和加载

创建对象

Java 对象创建的核心流程:

  1. ​​类加载检查

  2. 内存分配

  3. 初始化零值

  4. 设置对象头

  5. 执行构造方法​​

内存分配策略和并发控制机制(如 TLAB(Thread-Local Allocation Buffer))直接影响性能

而对象内存布局的设计优化了 GC 效率与访问速度。

对象生命周期

Java 对象的生命周期可概括为:

  • 创建阶段

  • 应用阶段(被强引用持有)

  • 不可达阶段(GC Roots 无法访问)

  • 垃圾回收阶段(可能触发 finalize() 方法)

  • 内存回收阶段

类加载器

类加载器有哪些?

  • 启动类加载器:Bootstrap ClassLoader

    • 用来加载 Java 核心类库,无法被 Java 程序直接引用

    • 如加载存放在 JDK\jre\lib(JDK 代表 JDK 的安装目录,下同)下

    • 或被 -Xbootclasspath 参数指定的路径中的,并且能被虚拟机识别的类库

  • 扩展类加载器:Extension ClassLoader

    • 该加载器由 sun.misc.Launcher$ExtClassLoader 实现

    • 它负责加载 JDK\jre\lib\ext 目录中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如 javax.* 开头的类)

    • 开发者可以直接使用扩展类加载器

  • 应用程序类加载器:Application ClassLoader

    • 该类加载器由 sun.misc.Launcher$AppClassLoader 来实现

    • 它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器

  • 用户自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现

双亲委派模型

双亲委派模型的作用

工作过程

  1. 如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成

  2. 每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中

  3. 只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类

作用

  • 避免类的重复加载:通过父类加载器优先加载类,确保同一类在JVM中仅被加载一次,减少内存占用和版本冲突风险

  • 保证核心类库的安全性:防止用户自定义类覆盖JVM核心类(如java.lang.String),通过父类加载器优先加载核心类库实现安全隔离

  • 提高类加载效率:父类加载器已加载的类可直接复用,减少重复加载的开销

  • 隔离类加载器的命名空间:不同类加载器加载的类处于独立命名空间,避免类冲突(如Tomcat多Web应用场景)

  • 维护类的统一行为:确保核心类(如Object)在所有加载器中行为一致,避免因类版本不同导致的逻辑异常

类加载过程

  1. 类从被加载到虚拟机内存中开始,到卸载出内存为止

  2. 它的整个生命周期包括:

    1. 加载(Loading)

    2. 验证(Verification)

    3. 准备(Preparation)

    4. 解析(Resolution)

    5. 初始化(Initialization)

    6. 使用(Using)

    7. 卸载(Unloading)

其中验证、准备、解析 3 个部分统称为连接(Linking)

详见 类加载过程

类加载和双亲委派原则

就是把类加载过程和双亲委派机制加载步骤说明下就行了

垃圾回收

垃圾判断

3种方法

  • ​​引用计数法​:个对象维护一个引用计数器,记录当前被引用的次数

  • 可达性分析算法​​:从 ​​GC Roots​​ 出发,遍历所有可达对象,未被遍历到的对象视为不可达(即垃圾)

  • 引用类型判定法:根据引用类型的不同,决定对象回收的优先级和条件

详见 对象已死?

垃圾回收算法

  • 分代收集算法

  • 标记-清除算法

  • 标记-整理算法

  • 复制算法。部分也表述为标记-复制算法

 详见 垃圾收集算法

标记清除缺点

标记清除算法的缺点是什么?

  1. 内存碎片化问题​

  2. 执行效率低​:标记和清除阶段均需遍历堆中的所有对象,时间复杂度为 O(n)

  3. 无法处理循环引用​

  4. 不可预测的回收时机​:被动触发,标记清除算法通常在全堆内存不足时触发回收,导致回收时机不可预测,可能在高负载时引发性能抖动

  5. 内存访问冲突风险:多线程问题​​,在标记和清除过程中,若与程序线程并发访问内存,可能引发数据不一致或崩溃

垃圾回收器

分类

  • 经典垃圾回收器

    • Serial 收集器

      • 特点:单线程串行回收,采用 "Stop-The-World" 机制;新生代使用复制算法,老年代(Serial Old)使用标记-整理算法

      • 适用场景:单核 CPU 或小内存客户端应用(如早期桌面程序)

      • 参数-XX:+UseSerialGC

    • ParNew 收集器

      • 特点:Serial 的多线程版本,新生代并行回收(复制算法),需与 CMS 搭配使用

      • 适用场景:JDK8 及之前版本的服务器端低延迟场景

      • 参数-XX:+UseParNewGC

    • Parallel Scavenge/Old(PS+PO)

      • 特点:多线程并行回收,以吞吐量优先(JDK8 默认组合),支持自适应调节堆大小

      • 适用场景:后台批处理、大数据计算等高吞吐场景

      • 参数-XX:+UseParallelGC

    • CMS(Concurrent Mark Sweep)

      • 特点:并发标记清除(减少停顿时间),老年代使用标记-清除算法;存在内存碎片和浮动垃圾问题

      • 适用场景:Web 服务、订单系统等低延迟场景(JDK14 后已移除)

      • 参数-XX:+UseConcMarkSweepGC

  • 现代垃圾回收器

    • G1(Garbage-First)

      • 特点:分区式(Region)、并行与并发结合;采用复制算法和预测性停顿模型,支持大堆内存(6GB+)

      • 优势:平衡吞吐量与延迟,JDK9 后成为默认回收器

      • 参数-XX:+UseG1GC

    • ZGC(Z Garbage Collector)

      • 特点:超低延迟(停顿 <10ms),支持 TB 级堆内存;通过读屏障和并发压缩实现

      • 适用场景:金融交易、实时系统等对延迟敏感的场景(JDK11+ 支持)

      • 参数-XX:+UseZGC

    • Shenandoah

      • 特点:通过“颜色指针”和并发整理减少停顿时间,适合大堆且低延迟需求

      • 适用场景:大堆内存、低延迟场景(需手动启用)

      • 参数-XX:+UseShenandoahGC

    • Epsilon GC

      • 特点:无操作的回收器,仅分配内存不回收;用于性能测试或内存管理完全可控的场景

      • 适用场景:调试、短期任务或内存泄漏检测

      • 参数-XX:+UseEpsilonGC

 详见 经典的垃圾收集器

G1回收器

G1 回收器的特色是什么?

  1. 基于分区的内存管理机制

    • 将堆内存划分为多个等大小的 Region(默认约 2048 个),每个 Region 可动态切换为 Eden、Survivor、Old 或 Humongous 区域

    • 支持动态分代调整:年轻代占比通过 -XX:G1NewSizePercent-XX:G1MaxNewSizePercent 在 5%~60% 间自动伸缩

    • 大对象优化:超过单个 Region 50% 大小的对象分配至 Humongous 区域,避免内存碎片

  2. 可预测的停顿时间模型

    • 通过 -XX:MaxGCPauseMillis 设定目标停顿时间(如 200ms),实现软实时回收

    • 优先级回收策略:按 Region 垃圾价值(回收空间/耗时)排序,优先处理高收益 Region(Garbage-First 设计理念)

    • 衰减标准差算法:根据历史 GC 数据动态预测回收能力,确保目标时间内完成回收

  3. 并行与并发混合执行

    • 并行回收:利用多核 CPU 并行执行 Young GC 和 Mixed GC,缩短 STW 时间

    • 并发标记:后台线程与用户线程并发运行,仅初始标记/最终标记需短暂 STW

    • 增量式回收:将老年代回收拆分为多次 Mixed GC,避免单次长时间停顿

  4. 高效空间整合与低碎片

    • 全局使用标记-整理算法(Mark-Compact),局部使用标记-复制算法(Mark-Copy)

    • 零内存碎片:支持长期运行服务稳定分配大对象

    • 连续内存分配:通过指针碰撞(Bump-the-Pointer)直接分配,无需空闲链表管理

  5. 智能化数据结构支持

    • 记忆集(RSet):每个 Region 维护跨 Region 引用记录,避免全堆扫描

    • 全局卡片表(Card Table):标记 512B 卡片单元,精准追踪对象引用变化

    • 并发标记快照(SATB):记录标记开始时的对象引用关系,防止漏标

  6. 适用场景对比

    • 最大堆内存

      • CMS:≤4GB

      • G1:≥8GB(支持 TB 级堆)

    • 停顿时间控制

      • CMS:无明确预测模型

      • G1:可设定目标停顿时间

    • 内存碎片问题

      • CMS:需定期 Full GC 整理

      • G1:自动整合(零碎片)

    • JDK 版本兼容性

      • CMS:JDK8 及以下主流

      • G1:JDK7u4+ 可用,JDK9+ 默认

  7. 调优建议

    • 关键参数:

      • -XX:+UseG1GC

      • -XX:MaxGCPauseMillis=200

      • -XX:G1HeapRegionSize=4M

      • -XX:InitiatingHeapOccupancyPercent=45

    • 监控工具:使用 JMX 或 jstat -gcutil 观察 Region 分布与回收效率

CMS vs G1

垃圾回收器 CMS 和 G1 的比较?

  1. 分代与分区设计

    • CMS:物理分代,严格划分新生代和老年代,需配合其他新生代收集器(如 ParNew)使用

    • G1:逻辑分代 + 物理分区,堆内存划分为多个等大小 Region(默认约 2048 个),支持动态调整分代

  2. 回收算法与内存碎片

    • CMS:基于标记-清除算法,老年代回收后产生内存碎片,可能触发 Full GC

    • G1:基于标记-整理算法,通过 Region 间对象移动避免内存碎片

  3. 回收阶段差异

    • CMS 四阶段

      • 初始标记(STW 短暂) → 并发标记 → 重新标记(STW) → 并发清除

    • G1 四阶段

      • 初始标记(STW) → 并发标记 → 最终标记(STW) → 筛选回收(按 Region 价值排序回收)

  4. 停顿时间控制

    • CMS:以最小停顿时间为目标,但无法预测具体停顿时间

    • G1:支持可预测停顿模型(如 -XX:MaxGCPauseMillis=200ms),适合大堆场景

  5. 内存碎片与浮动垃圾

    • CMS:存在内存碎片和浮动垃圾,可能因并发模式失败触发 Full GC

    • G1:无内存碎片,通过 SATB 机制避免浮动垃圾导致漏标

  6. 大对象处理

    • CMS:大对象直接进入老年代,加剧碎片问题

    • G1:大对象分配至 Humongous 区域(跨多个 Region),避免过早晋升

  7. 辅助数据结构

    • CMS:依赖 Card Table 记录跨代引用

    • G1:额外使用 Remembered Set(RSet)记录跨 Region 引用,占用约 20% Region 内存

  8. 执行负载与读写屏障

    • CMS

      • 仅需写后屏障维护卡表(Card Table),记录跨代引用,同步操作开销低

      • 并发标记阶段使用增量更新策略,重新标记阶段计算量较小

    • G1

      • 需写前屏障(支持 SATB 快照)和写后屏障(维护 RSet),异步队列处理开销高

      • 维护 Remembered Set(RSet)占用约 20% Region 内存,CPU 资源竞争更激烈

  9. 适用场景

    • CMS:中小型堆(≤4GB)、低延迟敏感场景(如 Web 服务),JDK8 及以下版本

    • G1:大型堆(≥8GB)、需平衡吞吐量与延迟的场景(如实时系统),JDK9+ 默认回收器

什么情况下使用 CMS,什么情况使用 G1?

  1. CMS 适用场景

    • 低延迟需求:适用于对响应时间敏感的应用(如 Web 服务、实时交易系统),需最小化 STW 停顿

    • 中小型堆内存(≤4GB):内存碎片风险可控,需预留 20% 空间存放浮动垃圾

    • 老年代为主的回收:适合对象晋升缓慢、老年代占用率高的场景(需配合 ParNew 收集器)

    • JDK 版本限制:推荐在 JDK 8 及以下版本使用

  2. G1 适用场景

    • 大堆内存(≥8GB):高效管理数十 GB 至数百 GB 堆内存,避免传统分代模型碎片问题

    • 可预测停顿时间:通过 -XX:MaxGCPauseMillis 设置目标停顿(如 200ms),适合金融交易等实时系统

    • 内存碎片敏感场景:长期运行服务(如云原生应用),依赖标记-整理算法避免 Full GC

    • 混合代际回收:支持同时回收新生代和老年代,适合对象生命周期复杂的应用

    • JDK 版本兼容性:JDK 7u4+ 可用,JDK 9+ 默认推荐

Java 垃圾回收

什么是 Java 里的垃圾回收?

为什么要 GC ?

如何触发垃圾回收?

定义

Java 垃圾回收(Garbage Collection, GC)是 JVM 提供的自动内存管理机制,用于回收程序中不再使用的对象所占用的内存空间

目标

其核心目标是如下 3 点:

  • 内存泄漏​​:防止因对象长期未被释放导致的内存耗尽(如未关闭的文件句柄或数据库连接)

  • ​手动管理复杂性​​:避免开发者手动分配/释放内存时可能出现的错误(如野指针、双重释放)

  • ​内存碎片化​​:通过特定策略减少内存碎片,提升内存分配效率

触发条件

  1. 内存分配失败

    • Minor GC(新生代回收):当 Eden 区满时触发,通过复制算法快速回收存活率低的对象

    • Major GC(老年代回收):老年代空间不足时触发,通常伴随 Full GC

  2. 显式调用

    • 通过 System.gc()Runtime.getRuntime().gc() 建议 JVM 执行垃圾回收,但 JVM 可能忽略该请求

  3. 阈值触发
    a. 堆内存使用率:当堆内存使用超过预设阈值(如 70%)时触发 GC
    b. 永久代/元空间满:Java 8 之前 PermGen 区满触发;Java 8+ Metaspace 区满触发

  4. 时间间隔

    • 部分 JVM 实现基于固定时间间隔(如每小时)触发 GC,无论内存使用情况

  5. 并发标记清除(CMS/G1)

    • 低延迟回收器(如 CMS、G1)可能在并发标记阶段因内存碎片等问题触发 Full GC

stop the world

垃圾回收算法哪些阶段会 stop the world?

简述

STW 的触发与算法设计强相关

  • 传统算法​​(如标记-清除、复制)的标记和转移阶段需全局暂停

  • ​并发算法​​(如 CMS、G1)通过并发标记减少 STW,但关键阶段仍无法避免

  • ​现代算法​​(如 ZGC)通过硬件优化(指针染色、读屏障)将 STW 压缩至极限

实际应用中,需根据业务需求(低延迟或高吞吐)选择合适的回收器,并通过调整堆大小、分代策略等参数优化 STW 时间

详细介绍

  1. 标记阶段

    • 初始标记(Initial Mark)

      • 触发原因:需暂停应用线程以确保根对象(GC Roots)的一致性,标记直接引用的存活对象

      • 适用算法:CMS、G1(混合回收的初始阶段)

      • 耗时:毫秒级(仅处理根对象)

    • 重新标记(Remark)

      • 触发原因:修正并发标记期间因应用线程修改引用导致的标记错误(漏标/误标)

      • 适用算法:CMS、G1(混合回收的最终标记阶段)

      • 耗时:毫秒级(通过写屏障优化缩短)

  2. 清除与复制阶段

    • 复制/转移阶段(Evacuation)

      • 触发原因:复制存活对象时需暂停线程,防止引用被修改

      • 适用算法:复制算法(年轻代回收)、G1(混合回收的复制阶段)

      • 耗时:与存活对象数量成正比(STW 主要瓶颈)

    • 清理阶段(Sweep)

      • 触发原因:统计分区存活情况时需短暂暂停线程

      • 适用算法:G1。

      • 耗时:微秒级(仅处理分区元数据)

  3. 分代回收的 STW 场景

    • 年轻代回收(Minor GC)

      • 触发原因:年轻代(如 Eden 区)空间不足时复制存活对象

      • 适用算法:所有分代算法(Serial、Parallel、G1 等)

      • 耗时:与年轻代存活对象数量相关(通常较短)

    • 老年代回收(Full GC)

      • 触发原因:老年代空间不足时触发全堆回收

      • 适用算法:Serial Old、Parallel Old、CMS(并发模式失败时退化)

      • 耗时:秒级(遍历全堆对象)

  4. 现代算法的优化与例外

    • 并发标记与增量回收

      • CMS/G1:并发标记阶段无 STW,但初始/最终标记仍需暂停

      • ZGC/Shenandoah:通过读屏障和并发压缩,仅根扫描阶段需亚毫秒级 STW

    • Epsilon GC

      • 特点:无回收操作,完全避免 STW

      • 适用场景:特殊用途(如性能测试)

minorGC & majorGC & fullGC

minorGC、majorGC、fullGC 的区别,什么场景触发 full GC

  1. 核心区别

类型

作用区域

触发条件

算法特点

耗时与影响

Minor GC

新生代(Eden + Survivor 区)

Eden 区空间不足时触发。

复制算法(存活对象复制到 Survivor 或老年代)

耗时短(毫秒级),但频繁触发可能影响吞吐量。

Major GC

老年代

老年代空间不足,或 Minor GC 后存活对象无法晋升至老年代时触发。

标记-清除或标记-整理(CMS、Parallel Old)

耗时长(秒级),通常伴随 Minor GC,暂停时间长。

Full GC

全堆(新生代+老年代)+ 方法区/元空间

老年代/元空间不足、显式调用 System.gc()、晋升失败、空间分配担保失败等。

全局标记-整理(如 Serial Old)

耗时最长(秒级甚至分钟级),完全暂停应用线程(STW),对性能影响最大。

  1. Full GC 的触发场景

    • 老年代空间不足

      • 对象晋升或大对象直接分配至老年代失败(通过 -XX:PretenureSizeThreshold 控制大对象阈值)

    • 显式调用 System.gc()

      • 开发者主动触发(需未禁用 -XX:+DisableExplicitGC

    • 元空间/永久代不足

      • 类/方法元数据加载过多(需合理设置 -XX:MaxMetaspaceSize

    • 晋升失败(Promotion Failure)

      • Survivor 区空间不足,且老年代无法提供担保空间

    • CMS 并发模式失败

      • CMS 回收器无法及时回收足够空间,退化为 Serial Old 触发 Full GC

    • 分配大对象失败

      • Eden 区无法分配大对象,且老年代空间不足

    • 空间分配担保失败

      • 老年代连续空间 < 新生代存活对象总大小(未开启 HandlePromotionFailure 时触发)

    • 堆转储或诊断操作

      • 使用 jmap -dump 等工具强制触发

 Minor GC 和 Major GC:详见 分代收集理论

回收方法区

主要回收两部分内容

  • 废弃的常量

  • 不再使用的类型

参考:回收方法区

分代垃圾回收器

分代垃圾回收器是怎么工作的?

以 CMS 收集器为例说明
分代回收器有两个分区

  • 新生代:默认的空间占比总空间的 1/3

  • 老生代:默认占比是 2/3

新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

  • 把 Eden + From Survivor 存活的对象放入 To Survivor 区

  • 清空 Eden 和 From Survivor 分区

  • From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor

每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代

大对象也会直接进入老生代。老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法

以上这些循环往复就构成了整个分代垃圾回收的整体执行流程

GC流程

JVM 中一次完整的 GC 流程是怎样的,对象如何晋升到老年代?

  1. GC 流程全解析

    • 标记阶段

      • 可达性分析:从 GC Roots(虚拟机栈局部变量、静态属性、常量等)出发标记存活对象

      • 两次标记机制:首次标记不可达对象,二次筛选未覆盖 finalize() 或已执行过的方法的对象直接回收

    • 垃圾回收

      • 新生代回收(Minor GC)

        • 触发条件:Eden 区空间不足时自动触发

        • 执行步骤:

          • 存活对象从 Eden 和 Survivor From 区复制到 Survivor To 区(年龄 +1)

          • 清空 Eden 和 Survivor From 区,交换 From/To 区角色

          • Survivor 空间不足时,存活对象通过分配担保机制直接进入老年代

      • 老年代回收(Full GC/Major GC)

        • 触发条件:老年代空间不足、大对象分配失败、显式调用 System.gc()

        • 执行步骤:

          • 采用标记-整理算法清理老年代内存碎片

          • 回收后仍内存不足则抛出 OutOfMemoryError

    • 内存整理与分配

      • 碎片整理:将存活对象紧凑排列(复制或移动)确保连续内存空间

      • 指针碰撞分配:直接分配连续内存,避免空闲链表管理开销

  2. 对象晋升到老年代的条件

    • 年龄阈值机制

      • 对象每经历一次 Minor GC 且存活,年龄 +1,达到阈值(默认 15)时晋升

      • 调整参数:-XX:MaxTenuringThreshold

    • 动态年龄判定

      • Survivor 区中相同年龄对象总大小 > Survivor 空间的 50% 时,年龄 ≥ 该值的对象直接晋升

    • 大对象直存老年代

      • 对象大小超过 -XX:PretenureSizeThreshold 设定值(如 30MB)时直接分配至老年代

    • Survivor 区空间不足

      • Minor GC 后存活对象无法全部放入 Survivor 区时提前晋升

    • 分配担保策略

      • Minor GC 前检查老年代剩余空间:

        • 剩余空间 < 历次晋升对象的平均大小时触发 Full GC

        • Full GC 后仍不足则部分存活对象强制晋升

面试题

垃圾回收器的基本原理是什么?

  • 对于 GC 来说,当程序员创建对象时,GC 就开始监控这个对象的地址、大小以及使用情况

  • 通常,GC 采用有向图的方式记录和管理堆(heap)中的所有对象

  • 通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"

  • 当 GC 确定一些对象为"不可达"时,GC 就有责任回收这些内存空间

垃圾回收器可以马上回收内存吗?

  • 可以

有什么办法主动通知虚拟机进行垃圾回收?

  • 程序员可以手动执行 System.gc(),通知 GC 运行,但是 Java 语言规范并不保证 GC 一定会执行

JVM 的元空间(永久代)中会发生垃圾回收么?

注意下 Java8 中已经移除了永久代,新加了一个叫做元空间的 native 内存区

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)

如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的

这就是为什么正确的永久代大小对避免 Full GC 是非常重要的原因

新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?

  • 新生代回收器:Serial、ParNew、Parallel Scavenge

  • 老年代回收器:Serial Old、Parallel Old、CMS

  • 整堆回收器:G1

新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低

老年代回收器一般采用的是标记-整理的算法进行垃圾回收

虚拟机执行子系统

类的生命周期

类的生命周期是 Java 虚拟机(JVM)管理类从加载到卸载的全过程,共分为五个阶段:

​​加载(Loading)→ 连接(Linking)→ 初始化(Initialization)→ 使用(Using)→ 卸载(Unloading)​​
详见 类加载的过程

类装载的执行过程

类装载是 Java 虚拟机(JVM)将类的字节码文件(.class)加载到内存并转化为可执行代码的过程

其执行流程可概括为​​加载、链接、初始化​​三个阶段,其中​链接阶段​​进一步分为​验证、准备、解析​​ 三个子步
详见 类加载的过程

JVM 加载 class 文件的原理机制

  • 类的加载流程

    • 将其放在运行时数据区的方法区内

    • 然后在堆区创建一个 java.lang.Class 对象

    • 用来封装类在方法区内的数据结构

  • 类的加载的最终产品

    • 是位于堆区中的 Class 对象,Class 对象封装了类在方法区内的数据结构

    • 并且向 Java 程序员提供了访问方法区内的数据结构的接口

    • 通常是创建一个字节数组读入 .class 文件,然后产生与所加载类对应的 Class 对象

  • 加载完成后,Class 对象还不完整,所以此时的类还不可用

  • 当类被加载后就进入连接阶段,这一阶段包括:

    • 验证

    • 准备(为静态变量分配内存并设置默认的初始值)

    • 解析(将符号引用替换为直接引用)三个步骤

  • 最后 JVM 对类进行初始化,包括:

    • 如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类

    • 如果类中存在初始化语句,就依次执行这些初始化语句

类加载器

什么是类加载器,类加载器有哪些?

定义

通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到 Java 虚拟机外部去实现

以便让应用程序自己决定如何去获取所需的类

实现这个动作的代码被称为“类加载器”

分类

  • 启动类加载器(Bootstrap Class Loader)

    • 由 JVM 自身用 C/C++ 实现,不继承 java.lang.ClassLoader,无法在 Java 代码中直接访问

    • 功能:加载核心 Java 类库(如 rt.jarresources.jar),路径为 <JAVA_HOME>/jre/lib-Xbootclasspath 指定的目录

  • 扩展类加载器(Extension Class Loader)

    • sun.misc.Launcher$ExtClassLoader 实现,父加载器为启动类加载器

    • 功能:加载扩展目录(<JAVA_HOME>/jre/lib/extjava.ext.dirs 指定路径)中的 JAR 文件

  • 应用程序类加载器(Application Class Loader/System Class Loader)

    • sun.misc.Launcher$AppClassLoader 实现,父加载器为扩展类加载器

    • 功能:默认加载用户类路径(ClassPath)下的类,是程序中默认的类加载器

  • 自定义类加载器(User-Defined Class Loader)

    • 开发者继承 ClassLoader 类实现,需重写 findClass() 方法

    • 用途:动态加载网络资源、加密字节码、模块化隔离(如 Tomcat 热部署)等

详见 双亲委派模型

双亲委派模型

双亲委派模型​​是 Java 类加载器的层级委托机制

​​子加载器在加载类时会优先委派给父加载器处理,依次递归至最顶层的启动类加载器(Bootstrap ClassLoader)

若父加载器无法加载,子加载器才会自行加载​​,以此保证核心类库的安全性(如防止用户篡改 java.lang.String)并避免重复加载
详见 双亲委派模型

Tomcat 类加载器为什么要破坏双亲委派机制?open in new window

Java 程序运行机制

  • 首先利用 IDE 集成开发工具编写 Java 源代码,源文件的后缀为 .java

  • 再利用编译器(javac 命令)将源代码编译成字节码文件,字节码文件的后缀名为 .class

  • 运行字节码的工作是由解释器(java 命令)来完成的

从上图可以看,java 文件通过编译器变成了 .class 文件,接下来类加载器又将这些 .class 文件加载到 JVM 中
其实可以一句话来解释:类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构

Java 对象创建过程

 首先让我们看看 Java 中提供的几种对象创建方式:

创建方式

描述

使用 new 关键字

调用了构造函数

使用 Class 的 newInstance 方法

调用了构造函数

使用 Constructor 类的 newInstance 方法

调用了构造函数

使用 clone 方法

没有调用构造函数

使用反序列化

没有调用构造函数

下面是对象创建的主要流程

  1. 虚拟机遇到一条 new 指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载

  2. 类加载通过后,接下来分配内存

    1. 若 Java 堆中内存是绝对规整的,使用“指针碰撞“方式分配内存

    2. 如果不是规整的,就从空闲列表中分配,叫做”空闲列表“方式

    3. 划分内存时还需要考虑一个问题-并发,也有两种方式

      1. CAS 同步处理

      2. 本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)

  3. 然后内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码…),最后执行 init 方法

Java 对象结构

Java 对象由三个部分组成:对象头、实例数据、对齐填充

  • 对象头由两部分组成

    • 第一部分存储对象自身的运行时数据:对象的哈希码、GC 分代年龄、锁标识状态、线程持有的锁、偏向线程 ID(一般占 32/64 bit)

    • 第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)

  • 如果是数组对象,则对象头中还有一部分用来记录数组长度

  • 实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)

  • 对齐填充:JVM 要求对象起始地址必须是 8 字节的整数倍(8 字节对齐)

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区