“类(class)是构造对象的模板或蓝图。我们可以将类想象成制作小甜饼的切割机,将对象想象为小甜饼。由类构造(construct)对象的过程称为创建类的实例(instance)。” 摘录来自 Java核心技术 卷Ⅰ 基础知识(原书第10版) 【美】凯 S.霍斯特曼
1、类
What(什么是类)
类是一个抽象的概念或模版,它定义了对象的属性和行为。
类通过关键字class来定义。一个类通常包括:
- 成员变量 属性(Fields):用于描述对象的状态或特征(数据)。
- 方法 (Methods):描述对象能做什么(行为/功能)。
- 构造器 (Constructors):负责在创建对象时进行初始化。
- 访问修饰符 :控制类、方法、变量等的访问权限。
访问修饰符
Java类的访问权限可以通过访问修饰符来控制,常见的有:
-
public:类可以被任何其他类访问。 -
private:类只能在当前类内部访问(对于内部类)。 -
default:没有指定访问修饰符时,类只能在同一个包内访问。 -
protected:不能单独使用,但可以用于类的成员,表示仅能在同包或子类中访问。
How (怎么写)
public class ClassName {
// 成员变量
private String name;
private int age;
// 构造方法
public ClassName(String name, int age) {
this.name = name;
this.age = age;
}
// 方法
public void sayHello() {
System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");
}
// Getter方法
public String getName() {
return name;
}
// Setter方法
public void setName(String name) {
this.name = name;
}
}
Why (原理)
类在JVM中的实现机制我们可以通过几个方面来理解。
类的生命周期
一个类从被编译的 .java 文件到最终在 JVM 中实例化对象,需要经历 加载、连接(验证、准备、解析)、初始化 三个阶段。
| 阶段 | 作用 | 关键机制 |
|---|---|---|
| 加载 (Loading) | 查找和导入 .class 文件的二进制数据。 | 类加载器 (ClassLoader):负责从文件系统、网络或 JAR 包中找到 Class 文件,将其二进制数据读入内存,并生成一个代表该类的 java.lang.Class 对象。 |
| 连接 (Linking) | 将类的二进制数据合并到 JVM 运行时状态中。 | 包括: 1. 验证 (Verification):确保 .class 文件符合 JVM 规范,没有安全问题。2. 准备 (Preparation):为类的静态变量分配内存,并设置默认初始值(例如 int 为 0,引用为 null)。3. 解析 (Resolution):将常量池中的符号引用(如方法名、字段名)替换为直接引用(内存地址)。 |
| 初始化 (Initialization) | 执行类中的 Java 代码。 | 执行 <clinit> 方法:这是 JVM 自动为类生成的构造器,用于执行所有静态代码块 {} 和为静态变量赋予我们在代码中设定的初始值。这个阶段是线程安全的。 |
类的加载
类加载器
Java中的类加载是由类加载器(ClassLoader)来完成的。类加载器负责将类的字节码加载到内存,并执行上述的类加载过程。类加载器的层次结构分为几种类型:
-
Bootstrap ClassLoader:负责加载JDK内置的类库,如
java.lang.*等。这些类通常存在于JDK的rt.jar文件中。 -
Extension ClassLoader:负责加载JDK扩展目录(
jre/lib/ext)中的类库。 -
Application ClassLoader:负责加载应用程序类路径(
classpath)中的类,通常是项目中的classes目录或jar包中的类。 -
自定义 ClassLoader:开发者可以自定义类加载器,用于特殊需求,比如动态加载类或从网络加载类。
双亲委派模型
双亲委派模型是Java类加载机制的核心设计模式,它定义了类加载器在加载类时的优先级顺序:当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,只有当父类加载器无法完成加载时,子加载器才会尝试自己加载。
⚙️模型工作流程
- 请求委派:当前 ClassLoader 收到加载请求。
- 向上委托:它将加载任务委托给其父加载器。这个过程层层向上,直到最顶层的启动类加载器 (Bootstrap ClassLoader)。
- 尝试加载:启动类加载器检查其路径(
<JAVA_HOME>/jre/lib)下能否找到并加载该类。 - 反馈下放:
- 成功:如果父类加载器成功加载,则直接返回
Class对象,子加载器不再介入。 - 失败:如果父类加载器无法加载(找不到
.class文件),则通知子加载器。
- 成功:如果父类加载器成功加载,则直接返回
- 自我加载:子加载器接到通知后,才会在自己的搜索路径中尝试加载该类。
好处
- 避免重复加载:当一个类已经被父加载器加载过一次,子加载器就不会再加载,确保了类的唯一性
- 保证核心 API 安全:防止核心 Java API(如
java.lang.Object、java.lang.String)被恶意代码或自定义代码篡改。
类信息存储
类加载完成后,其核心结构信息会被存储在 方法区 (Method Area),在 Java 8 及其以后,这部分被称为元空间 (Metaspace)。
| 存储区域 | 存储内容 | 作用 |
|---|---|---|
| 方法区 / 元空间 | 类模板 (Class Template) | 存储类的静态信息,是所有实例共享的“蓝图”。包括: 1. 类元数据 包括类的完整名称、父类名称、实现的接口、访问修饰符等。 2. 运行时常量池:类的所有常量、字面量、符号引用。 3. 字段信息:字段名、类型、修饰符。 4. 方法信息:方法名、参数类型、返回类型、方法字节码。 |
可以通过反射(Reflection)来访问这些信息。
例如,下面是通过反射来获取类信息的代码:
Class<?> clazz = Person.class; // 获取Person类的Class对象
System.out.println(clazz.getName()); // 打印类名
类的卸载
当一个类不再被任何地方引用,并且加载该类的类加载器也被垃圾回收时,JVM可以卸载该类,释放方法区中的内存空间。这通常发生在应用重新部署
类的实例化
当我们使用new关键字创建对象时,JVM会执行以下步骤:
- 类加载检查:JVM首先检查该类是否已经被加载,如果没有则JVM 执行加载、连接、初始化,将类模板放入元空间。
- 内存分配:在堆内存中为对象分配空间。
- 内存初始化:将分配到的内存空间初始化为零值(不包括对象头)。
- 对象头设置:设置对象头(Object Header),包含:
- Mark Word:存储对象的hashCode、GC分代年龄、锁状态标志等
- Class Pointer:指向类元数据的指针,JVM通过这个指针确定对象是哪个类的实例
- 执行构造方法:调用构造方法进行初始化。构造器是对象实例化的关键。
- 返回对象的引用:JVM返回一个指向新创建对象的引用,该引用通常存储在栈区的局部变量中。
通过这个类指针,JVM 才能在运行时知道这个对象(小甜饼)是由哪个类(切割机)构造的,从而能找到并执行它所属的方法字节码。
2、对象
What(什么是对象)
对象是类的具体实例,是面向对象编程的基本单元。每个对象都具有自己独立的状态(通过成员变量来描述)和行为(通过方法来定义)。在JVM中,对象是存储在堆内存中的实体。
对象的核心特性:
- 唯一性:每一个实例都有一个唯一的身份。通过
==运算符可验证身份唯一性。 - 状态性:对象保存实例变量的值,这些值定义了对象的当前状态。可通过get、set方法来验证。
- 生命周期:从
new创建到GC回收,经历创建→使用→不可达→回收的过程
💡 面向对象的本质:不是"面向类",而是面向具体的对象实例进行编程。我们通过对象的状态变化和行为交互来解决问题。
How (怎么写)
// 1. 对象创建(实例化)
ClassName obj1 = new ClassName("张三", 18);
ClassName obj2 = new ClassName("李四", 20);
// 2. 访问对象状态
String name = obj1.getName(); // 获取状态
obj2.setName("李四四"); // 修改状态
// 3. 调用对象行为
obj1.sayHello(); // 输出: Hello, my name is 张三 and I am 18 years old.
// 4. 对象身份比较(内存地址)
boolean isSameObject = (obj1 == obj2); // false - 不同对象
// 5. 对象内容比较(需要重写equals方法)
boolean isSameContent = obj1.equals(obj2); // 通常为false,取决于equals实现
// ⚠️ 重要提醒:
// 1. == 比较的是对象内存地址(身份),不是内容
// 2. equals()默认行为同==,需重写才能比较业务逻辑相等
// 3. 重写equals()必须同时重写hashCode()(契约要求)
// 6. 对象引用传递
void changeName(ClassName obj) {
obj.setName("修改后的名字"); // 修改原对象状态
}
changeName(obj1); // obj1的状态被改变
Why (原理)
对象在JVM中的内存布局
当执行new ClassName()时,JVM在堆内存中为对象分配空间,布局如下:
┌────────────────────────────────────---------------------─┐
│ 对象内存布局 (堆内存) │
├───────────────────┬─────────────────---------------------┤
│ 1.对象头 (Header)│ 2.实例数据 │ 3.对齐补充 |
│ (12-16 bytes) │ (Instance Data) │ (Padding) |
├─────────┬─────────┼─────────────────┤--------------------|
│ Mark Word │ Klass │ 字段1 │ 字段2 ...│ |
│ (8 bytes)│ Pointer│ │ │ |
│ │(4-8B) │ │ │ (0-7B) |
└─────────┴─────────┴─────────────────----------------------|
- 对象头(Header)
- Mark Word (标记字)
- 存储对象的hashCode(25位)
- GC分代年龄(4位)
- 锁状态标志(1-2位):无锁/偏向锁/轻量级锁/重量级锁
- 线程ID(偏向锁时)
- CMS GC标记(垃圾回收时)
- Klass Pointer(类指针):
- 指向方法区中类元数据的指针
- 通过此指针找到类的方法表、字段信息等
- 64位JVM默认开启指针压缩(4字节),否则8字节
- 实例数据
- 存储对象的实例字段(非静态字段)
- 字段排列遵循从大到小、同类型连续原则(JVM优化)
- 父类字段在前,子类字段在后
- 对齐补充
- 保证对象大小是8字节的整数倍(HotSpot要求)
- 纯粹为了内存对齐,无实际意义
对象的垃圾回收
- 可达性分析:从GC Roots(栈帧局部变量、静态变量等)出发,无法到达的对象被标记为垃圾
- finalize()机制:对象被回收前的最后机会(不推荐使用)
- 内存释放:JVM回收堆内存空间,供后续对象分配使用
3、总结
- 类是模板/蓝图,对象是具体实例
- 类存储在方法区/元空间,对象存储在堆内存
- 类定义了对象的结构和行为,对象承载具体的状态和交互
- 面向对象编程的本质是通过对象间的协作解决问题
🎯 核心思想:
"类描述世界,对象改变世界"
类定义了可能性,对象实现了具体行为。我们编程时操作的是对象,而非类本身。理解对象的内存布局、生命周期和访问机制,是写出高效Java代码的关键。