类加载过程? Java怎么保证核心API不被篡改? 打破双亲委派机制的案例

本文深入探讨Java类加载过程,包括加载、验证、准备、解析和初始化五个阶段,以及类加载器的工作原理和双亲委派机制。同时,讨论了Java如何确保核心API的安全性,防止篡改。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

类加载过程?

Java怎么保证核心API不被篡改?

打破双亲委派机制的案例


我们首先看几个面试题。

  • 我们能够通过一定的手段,覆盖 HashMap 类的实现么?
  • 有哪些地方打破了 Java 的类加载机制?
  • 如何加载一个远程的 .class 文件?怎样加密 .class 文件?
  • <cinit> 方法和 <init> 方法有什么区别?

类加载过程?

JVM 通过加载 .class 文件,能够将其中的字节码解析成操作系统机器码。那这些文件是怎么加载进来的呢?又有哪些约定?加载过程如图:

浅绿的两个部分表示类的生命周期,就是从类的加载到类实例的创建与使用,再到类对象不再被使用时可以被 GC 卸载回收。这里要注意一点,由 Java 虚拟机自带的三种类加载器加载的类在虚拟机的整个生命周期中是不会被卸载的,只有用户自定义的类加载器所加载的类才可以被卸载。

注:几种变量的关系:

preview

1、加载

将外部的 .class 文件,加载到 Java 的方法区内。将外部*.class加载到方法区,并创建一个Class对象。加载阶段主要是找到并加载类的二进制数据,比如从 jar 包里或者 war 包里找到它们。

2、验证

不符合规范的将抛出 java.lang.VerifyError 错误。像一些低版本的 JVM,是无法加载一些高版本的类库的,就是在这个阶段完成的。

3、准备

将为一些类变量(static修饰的变量,不包含用 final 修饰的静态变量)分配内存,并将其初始化为默认值。此时,实例对象还没有分配内存,所以这些动作是在方法区上进行的。

类变量有两次赋初始值的过程:

  • 准备阶段,赋予默认初始值;
  • 初始化阶段,赋予程序员定义的值。

而局部变量不存在准备阶段赋予初值。

范例:类变量与局部变量初值赋予

code-snippet 1:
     public class A {
         static int a ;
         public static void main(String[] args) {
             System.out.println(a);
         }
     }
 code-snippet 2:
 public class A {
     public static void main(String[] args) {
         int a ;
         System.out.println(a);
     }
 }

code-snippet 1 将会输出 0,而 code-snippet 2 将无法通过编译。

4、解析

解析字段、接口、方法,将符号引用替换为直接引用的过程:

  • 符号引用:一种定义,可以是任何字面上的含义;
  • 直接引用:直接指向目标的指针、相对偏移量。

具体可以分为:

  • 类或接口的解析
  • 类方法解析
  • 接口方法解析
  • 字段解析

我们来看几个经常发生的异常,就与这个阶段有关:

  • java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。
  • java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。
  • java.lang.NoSuchMethodError 找不到相关方法时的错误。

解析过程保证了相互引用的完整性

5、初始化

主要完成静态块执行与静态变量的赋值,这里有两个规则:

  • 规则一:static 语句块,只能访问到定义在 static 语句块之前的变量。
  • 规则二:JVM 会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕。

范例:规则一

static {
         b = b + 1;
 }
 static int b = 0;

无法通过编译的。

范例:规则一

public class A {
     static int a = 0 ;
     static {
         a = 1;
         b = 1;
     }
     static int b = 0;
 
     public static void main(String[] args) {
         System.out.println(a);
         System.out.println(b);
     }
 }

结果是 a = 1 、 b = 0;

变量的初始化过程:准备阶段->cinit->init


面试题:<cinit> 方法和 <init> 方法有什么区别?

 

详解类加载器

Java怎么保证核心API不被篡改?

如上图所示,Java 自带的三种类加载器分别是:BootStrap 启动类加载器、扩展类加载器和应用加载器(也叫系统加载器)。图右边的桔黄色文字表示各类加载器对应的加载目录。启动类加载器加载 java home 中 lib 目录下的类,扩展加载器负责加载 ext 目录下的类,应用加载器加载 classpath 指定目录下的类。除此之外,可以自定义类加载器。

Java 的类加载使用双亲委派模式,即一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器,如上图中蓝色向上的箭头。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。如图中的桔黄色向下的箭头。

这种双亲委派模式的好处,可以避免类的重复加载,另外也避免了 Java 的核心 API 被篡改。

打破双亲委派机制的案例

 

<think>嗯,用户问的是关于类加载器有哪些的问题。首先,我需要回忆一下JVM中类加载器的相关知识。类加载器Java虚拟机(JVM)用来动态加载Java类的重要组件。根据我的理解,类加载器主要分为几个层次,包括启动类加载器、扩展类加载器、系统类加载器,可能还有自定义类加载器过,我得确认这些名称是否正确,以及每个加载器的具体职责是什么。 用户之前的问题是关于程序计数器为什么属于JVM内存模型,现在转向类加载器,可能是因为在学习JVM结构的同部分。用户可能对类加载机制太清楚,或者想了解类加载器的区别和应用场景。需要确保回答结构清晰,分点说明,并且用例子帮助理解。 首先,启动类加载器(Bootstrap ClassLoader),这个加载器是用C++实现的,负责加载Java核心库,比如rt.jar。它是最顶层的类加载器,没有父加载器。然后是扩展类加载器(Extension ClassLoader),负责加载JRE的扩展目录中的类,比如lib/ext目录下的jar包。接下来是应用程序类加载器(Application ClassLoader),也称为系统类加载器,负责加载用户类路径(ClassPath)上的类库。这三个是JVM自带的类加载器,遵循双亲委派模型。 另外,还有自定义类加载器,用户可以根据需要继承ClassLoader类,实现自己的类加载逻辑。比如,Tomcat使用自定义类加载器来隔离同的Web应用,或者实现热部署功能。自定义类加载器打破了双亲委派模型,需要特别注意类冲突的问题。 需要检查是否还有其他类型的类加载器,比如线程上下文类加载器过这可能属于系统类加载器的变体。或者像Java 9引入的模块化系统对类加载器的影响,但可能用户的问题更关注传统结构,所以暂时提模块化系统。 可能用户会混淆类加载器的层次结构,所以在回答时需要明确它们的层次关系和加载范围。双亲委派模型的工作机制也很重要,每个类加载器在加载类时先委派给父加载器,只有当父加载器无法完成时才自己加载。这种机制保证Java核心库的类型安全,防止用户自定义类覆盖核心类。 同时,要举例子说明各个类加载器的具体应用场景,比如启动类加载器加载java.lang.Object,扩展类加载器加载javax包中的类,系统类加载器加载用户自己编写的类。自定义类加载器的例子如OSGi框架、Tomcat等。 需要注意术语的准确性,比如Bootstrap ClassLoader是否真的由C++实现,而其他类加载器Java实现。此外,类加载器的父子关系是否是继承关系,还是组合关系。在双亲委派模型中,父加载器并是通过继承实现,而是通过组合的方式,每个类加载器持有父加载器的引用。 用户可能还想知道类加载器在实际开发中的应用,比如如何实现热部署,或者解决类冲突。这时候可以简要说明自定义类加载器的作用,比如每个Web应用使用独立的类加载器,避免同应用之间的类冲突。 总结一下,回答的结构应该是:首先介绍类加载器的作用,然后分点说明各个内置类加载器(启动、扩展、系统),再讲自定义类加载器,最后可以提一下双亲委派模型和实际应用例子。确保用中文,避免使用Markdown格式,语言口语化,符合思考过程。</think>类加载器Java虚拟机(JVM)用来动态加载类的核心组件,它们像“搬运工”一样把.class文件搬运到内存中。常见的类加载器主要分为四类,各司其职: --- ### 一、**启动类加载器(Bootstrap ClassLoader)** - **地位**:类加载器家族的老祖宗 - **职责**:加载`JAVA_HOME/lib`目录下的核心库(如rt.jar、charsets.jar) - **特点**: - 由C++实现(你看到它的Java代码) - 没有父类加载器(它是顶层的存在) - 加载`java.lang.Object`、`java.lang.String`等核心类 *示例*: 当你写`new String()`时,实际由它加载`java.lang.String`类 --- ### 二、**扩展类加载器(Extension ClassLoader)** - **地位**:二当家 - **职责**:加载`JAVA_HOME/lib/ext`目录下的扩展包 - **特点**: - 由Java实现(对应`sun.misc.Launcher$ExtClassLoader`) - 父加载器是启动类加载器 - 加载像`javax.servlet`这种扩展API *示例*: 使用XML解析器时,可能由它加载相关扩展类 --- ### 三、**应用程序类加载器(Application ClassLoader)** - **别名**:系统类加载器(System ClassLoader) - **职责**:加载用户类路径(ClassPath)下的类 - **特点**: - 由Java实现(对应`sun.misc.Launcher$AppClassLoader`) - 父加载器是扩展类加载器 - 你写的`Main.java`编译后的类,默认由它加载 *示例*: 执行`java -jar myapp.jar`时,它负责加载你写的所有业务类 --- ### 四、**自定义类加载器** - **作用**:打破双亲委派机制,实现特殊需求 - **常见场景**: 1. 热部署(如Tomcat修改代码用重启) 2. 类隔离(防止同模块的类冲突) 3. 加密解密(加载加密的.class文件) - **实现方式**:继承`ClassLoader`类,重写`findClass()`方法 *实际应用案例*: - Tomcat为每个Web应用创建独立的类加载器 - OSGi框架通过自定义加载器实现模块化 --- ### 五、**双亲委派模型(关键机制)** 所有类加载器都遵循这个“先请示上级”的工作流程: 1. 收到类加载请求时,先自己处理 2. 逐级向上委托给父加载器 3. 父加载器无法完成时,才自己加载 *举个栗子*: 当你尝试加载`java.lang.Object`时,最终会由启动类加载器完成加载,避免用户自定义的Object类覆盖核心库。 --- ### 六、**为什么需要这么多类加载器?** 1. **安全防护**:防止核心类被篡改(比如你无法自定义`java.lang.String`) 2. **职责分离**:同层级加载同重要程度的类 3. **灵活扩展**:通过自定义加载器突破限制(比如实现热更新) --- ### 总结 - **内置三大加载器**:启动(搬砖核心库)→ 扩展(搬砖扩展包)→ 应用(搬砖你的代码) - **自定义加载器**:解决特殊场景需求(相当于定制化搬运工) - **工作原则**:双亲委派机制像“家族企业”,小事让底层处理,大事必须请示上级 理解类加载器层级,就像搞懂快递公司的配送体系——总仓发核心货物(启动类加载器),区域仓发扩展件(扩展类加载器),最后快递员(应用类加载器)把包裹送到你家门口!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值