在Java中,类和类加载器是密切相关的两个概念,理解它们有助于我们更好地掌握Java的运行机制。
什么是Java类?
Java类就像是一个模板或蓝图,它定义了对象的属性和行为。比如"汽车"可以看作一个类,它有颜色、品牌等属性,有行驶、刹车等行为。我们根据这个类可以创建具体的汽车对象(如一辆红色的特斯拉)。
// 汽车类(模板)
class Car {String color; // 属性String brand; // 属性// 行为void drive() {System.out.println(brand + "汽车正在行驶");}
}// 创建对象(根据模板造具体的车)
Car myCar = new Car();
myCar.color = "红色";
myCar.brand = "特斯拉";
myCar.drive(); // 输出:特斯拉汽车正在行驶
什么是类加载器?
类加载器(ClassLoader)就像是"类的搬运工",它的作用是把.class文件(编译后的类)加载到Java虚拟机(JVM)中,让JVM能够认识这个类并使用它。
想象一下:JVM就像一个大型工厂,类就像是生产所需的图纸。类加载器的工作就是把这些图纸从文件系统、网络或其他地方运送到工厂里,供工厂使用。
咱们可以把Java虚拟机(JVM)想象成一个“大工厂”,类加载器就是负责给这个工厂“运图纸(类)”的工人。
从虚拟机角度看(两种类加载器)
- 启动类加载器(Bootstrap ClassLoader):它是工厂里“最核心的元老级搬运工”,用C++写的,是虚拟机本身的一部分。专门搬最基础、最核心的“工厂自带图纸”,像
java.lang.String
这类Java最根本的类,就靠它搬。 - 其他类加载器:这些是“后来的搬运工”,用Java写的,和虚拟机是分开的。它们都得继承
java.lang.ClassLoader
这个抽象类,负责搬一些额外的图纸。
从开发者角度看(更细致的划分)
- 启动类加载器:还是那个“核心元老搬运工”,负责搬
<JRE_HOME>\lib
目录里(或者-Xbootclasspath
参数指定路径里)、虚拟机能认出来的“核心图纸”。比如java.util
(工具类)、java.io
(输入输出类)、java.lang
(语言基础类)这些常用基础类库。而且它很“傲娇”,只认文件名,像rt.jar
这类符合名字的才搬,名字不对的,哪怕在lib
目录里也不管。另外,Java程序没法直接调用它。
- 扩展类加载器(Extension ClassLoader):它是“扩展搬运工”,负责搬
<JRE_HOME>/lib/ext
目录或者java.ext.dir
系统变量指定路径里的“扩展图纸”。像swing
(界面相关)、内置js
引擎、xml
解析器这些以javax
开头的扩展类库,都由它来搬。开发者是可以直接用这个加载器的。
- 应用程序类加载器(Application ClassLoader):也叫“系统类加载器”,是“用户搬运工”。负责搬用户自己指定的类路径(ClassPath)里的“图纸”,比如我们自己写的类,或者第三方的
jar
包。如果我们没自己定义类加载器,程序默认就用它来搬图纸。而且它可以通过ClassLoader
的getSystemClassLoader()
方法获取到,开发者能直接使用。
类与类加载器的关系
- 每个类被加载到JVM后,都会记录是被哪个类加载器加载的
- 类加载器之间存在"父子关系":Application的父是Extension,Extension的父是Bootstrap
- 判断两个类是否相同,不仅要看类名是否一样,还要看它们是否被同一个类加载器加载
打个比方:两个名字相同的"汽车"图纸,如果一个是由"中国搬运工"搬来的,一个是由"美国搬运工"搬来的,JVM会认为它们是不同的类。
为什么需要类加载器?
- 实现了类的动态加载:需要用某个类时才加载,不用时不加载,节省资源
- 实现了隔离性:不同的类加载器可以加载同名类而不冲突,这对容器(如Tomcat)很重要
- 安全性:可以通过自定义类加载器来控制哪些类能被加载,防止恶意代码
理解类和类加载器的关系,有助于我们解决类冲突、类找不到等问题,也是学习Java反射、动态代理等高级特性的基础。
在Java类加载机制中,双亲委派模型可以类比成一个公司的文件审批流程,这样能更通俗易懂地理解。
什么是双亲委派模型
在Java里,类加载器之间存在层次关系,形成了一个类似树形的结构。除了最顶层的启动类加载器 ,其他类加载器都有自己的“父加载器”,比如扩展类加载器的父加载器是启动类加载器,应用程序类加载器的父加载器是扩展类加载器。双亲委派模型规定,当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把请求委派给父加载器去完成,只有当父加载器无法完成加载任务时,子加载器才会尝试自己去加载。
用公司审批流程举例说明
假设你在公司里写了一份重要的报告,需要经过审批才能正式发布。这就类似于类加载器收到加载类的请求。
- 部门经理(应用程序类加载器):你作为普通员工,写好报告后会先交给部门经理,对应应用程序类加载器收到类加载请求,它不会自己先去处理,而是把请求向上提交。
- 部门总监(扩展类加载器):部门经理拿到你的报告后,不会直接审批,而是交给部门总监,这就好比应用程序类加载器把加载请求交给扩展类加载器。部门总监拿到报告后,同样也不会直接审批,而是继续往上提交。
- 公司老板(启动类加载器):部门总监把报告交给公司老板,启动类加载器作为最顶层,先检查这个报告(类)是不是属于它负责的核心内容,比如公司的基本规章制度、财务审批流程等相关文件(Java核心类库) 。如果是,老板就直接审批(加载类);如果不是,比如是关于某个项目的具体报告,老板就会把报告打回给部门总监,说“这事儿不归我管,你处理一下”。
- 部门总监处理:部门总监接到老板打回的报告后,查看是否属于自己职责范围内,比如一些技术规范、行业标准相关的报告,如果是就审批(加载类);如果不是,再打回给部门经理。
- 部门经理处理:部门经理接到报告,一看是关于自己部门项目执行情况的报告,就自己审批了(应用程序类加载器自己加载类)。
双亲委派模型的好处
- 避免重复加载:如果父加载器已经加载过这个类,子加载器就不需要再加载了,节省了资源。就像在公司审批流程中,如果高层已经审批过类似的文件,底层就不用重复审批。
- 保证安全性:它确保了Java核心类库的一致性和安全性。因为核心类库都是由最顶层的启动类加载器加载的,防止了用户自定义的类去替换核心类。比如不会出现有人写一个假的
java.lang.String
类混入系统,因为启动类加载器只会加载真正的核心String
类 。
对象的创建过程
咱们把Java中对象的创建过程,比作"盖房子"的过程,这样就很好理解了:
- 确定图纸(类加载检查)
1.当你说new 房子()
时,JVM首先会检查"房子"这个类的图纸(.class文件)有没有被加载到内存里。如果没加载,就会让类加载器去把图纸搬过来(类加载过程)。
就像盖房子前,必须先有建筑图纸,而且图纸得先拿到工地才行。
- 圈地(分配内存)
图纸确认后,JVM会在内存里找一块合适的地方(堆内存),专门用来盖这个房子(存放对象)。
相当于开发商在空地上圈出一块地,大小刚好够盖这栋房子。
- 地基处理(初始化零值)
圈好地后,JVM会先把这块地"打扫干净",给里面的各种属性(比如房子的面积、房间数)设置默认值(数字0、布尔false、引用null等)。
就像盖房子前,先把地皮整平,打好地基,确保基础是合格的。
- 挂门牌(设置对象头)
JVM会给这个对象加一个"身份证"(对象头),里面记录着:这个对象属于哪个类(对应哪个图纸)、哈希码、GC信息等。
相当于给房子挂上门牌,写明"这是XX小区3号楼",方便后续查找和管理。
- 内部装修(执行初始化)
最后一步是真正按照图纸装修:给属性赋值(比如面积120平米、3个房间)、执行构造方法里的逻辑(比如安装门窗、铺地板)。
到这一步,房子才算真正盖好,能住人(使用对象)了。
总结一下:从确认图纸→分配空间→基础处理→标记身份→详细装修,一步不差,一个对象就创建出来了。就像盖房子一样,按流程来才能保证最终的"房子"能用、好用。