Java 核心基础 — 泛型 / 反射 / JVM / GC / 类加载
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
1. 泛型
1.1 类型擦除原理
Java 泛型是编译期特性,编译后泛型信息被擦除:
List<String>和List<Integer>编译后都是List- 泛型类型参数被替换为上界(无界则为
Object)
证明类型擦除:
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 运行时 class 相同
System.out.println(strList.getClass() == intList.getClass()); // true
// 通过反射绕过泛型检查
List<Integer> list = new ArrayList<>();
list.getClass().getMethod("add", Object.class).invoke(list, "hello");
System.out.println(list.get(0)); // "hello" —— Integer 列表里放了 String!1.2 泛型边界
// 上界通配符:只能读,不能写(不知道具体是什么子类型)
List<? extends Number> producer = new ArrayList<Integer>();
Number n = producer.get(0); // ✅ 可以读
// producer.add(1); // ❌ 编译错误
// 下界通配符:可以写,读只能得到 Object
List<? super Integer> consumer = new ArrayList<Number>();
consumer.add(1); // ✅ 可以写 Integer
Object obj = consumer.get(0); // 只能当 Object 读1.3 PECS 原则
Producer Extends, Consumer Super:
- 如果你只从集合中读取数据(生产者),用
<? extends T> - 如果你只向集合中写入数据(消费者),用
<? super T> - 如果既读又写,不用通配符
// Collections.copy 的签名完美体现 PECS
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++)
dest.set(i, src.get(i)); // src 是生产者(extends),dest 是消费者(super)
}1.4 TypeToken 与 Gson
由于类型擦除,运行时无法获取 List<User> 的泛型参数。Gson 通过匿名内部类保留泛型信息:
// 匿名内部类的父类泛型信息保存在 Class 文件的 Signature 属性中
Type type = new TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);原理:匿名内部类 new TypeToken<List<User>>(){} 编译后会在字节码中保留 List<User> 这个泛型签名(因为它是类定义的一部分,不是方法调用的一部分)。
1.5 Kotlin reified
Kotlin 内联函数配合 reified 可以在运行时获取泛型类型:
inline fun <reified T> Gson.fromJson(json: String): T {
return fromJson(json, T::class.java) // 可以直接用 T::class
}
// 使用
val user = gson.fromJson<User>(jsonString)原理:inline 函数在调用处内联展开,编译器直接将 T 替换为实际类型,所以不存在擦除问题。
2. 反射与注解
2.1 反射 API
// 获取 Class 对象的三种方式
Class<?> clazz1 = Class.forName("com.example.User");
Class<?> clazz2 = User.class;
Class<?> clazz3 = user.getClass();
// 创建实例
Object obj = clazz1.getDeclaredConstructor().newInstance();
// 访问私有字段
Field field = clazz1.getDeclaredField("name");
field.setAccessible(true); // 突破 private 限制
field.set(obj, "张三");
// 调用方法
Method method = clazz1.getDeclaredMethod("getName");
method.setAccessible(true);
String name = (String) method.invoke(obj);2.2 反射性能开销
反射比直接调用慢 5-50 倍,原因:
- 安全检查:每次
invoke都要检查访问权限 - 无法 JIT 优化:编译器无法内联反射调用
- 参数装箱:基本类型需要装箱为 Object[]
- 方法查找:需要遍历方法表
优化手段:
setAccessible(true)跳过安全检查(提升约 4 倍)- 缓存 Method/Field 对象,避免重复查找
- 使用 MethodHandle(JDK 7+)替代传统反射
2.3 注解保留策略
@Retention(RetentionPolicy.SOURCE) // 编译后丢弃,如 @Override
@Retention(RetentionPolicy.CLASS) // 保留在字节码,运行时不可见,如 @NonNull
@Retention(RetentionPolicy.RUNTIME) // 运行时可通过反射获取,如 @Deprecated2.4 APT 编译期注解处理
APT(Annotation Processing Tool)在编译期扫描注解并生成代码:
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
// 生成代码
JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
javaFile.writeTo(processingEnv.getFiler());
}
return true;
}
}Android 中的应用:
- ButterKnife:
@BindView→ 生成_ViewBinding类 - Dagger/Hilt:
@Inject→ 生成依赖注入工厂 - Room:
@Dao→ 生成 SQL 实现类 - Glide:
@GlideModule→ 生成GeneratedAppGlideModule
3. JVM 内存模型
3.1 运行时数据区
┌─────────────────────────────────────────┐
│ JVM 运行时数据区 │
├──────────────┬──────────────────────────┤
│ 线程私有 │ 线程共享 │
├──────────────┼──────────────────────────┤
│ 程序计数器 │ 堆(Heap) │
│ 虚拟机栈 │ - 新生代(Eden+S0+S1) │
│ 本地方法栈 │ - 老年代 │
│ │ 方法区(元空间) │
│ │ - 类信息、常量池、静态变量 │
└──────────────┴──────────────────────────┘各区域详解:
| 区域 | 存储内容 | 异常 |
|---|---|---|
| 程序计数器 | 当前线程执行的字节码行号 | 唯一不会 OOM 的区域 |
| 虚拟机栈 | 栈帧(局部变量表、操作数栈、动态链接、返回地址) | StackOverflowError / OOM |
| 本地方法栈 | Native 方法调用 | StackOverflowError / OOM |
| 堆 | 对象实例、数组 | OutOfMemoryError |
| 方法区 | 类信息、常量池、静态变量 | OutOfMemoryError |
3.2 对象创建过程
- 类加载检查:检查 new 指令的参数能否在常量池中定位到类的符号引用,且该类已被加载
- 分配内存:
- 指针碰撞(Bump the Pointer):堆内存规整时,移动指针分配
- 空闲列表(Free List):堆内存不规整时,从列表中找合适的块
- 线程安全:TLAB(Thread Local Allocation Buffer)每个线程预分配一小块
- 初始化零值:所有字段设为默认值(0/null/false)
- 设置对象头:Mark Word + 类型指针
- 执行
<init>:调用构造方法
3.3 对象头 Mark Word
在 64 位 JVM 中,Mark Word 占 8 字节:
无锁状态: [hashCode(31) | age(4) | biased(1) | lock(2)] = 01
偏向锁: [threadId(54) | epoch(2) | age(4) | biased(1) | lock(2)] = 01
轻量级锁: [指向栈中锁记录的指针(62) | lock(2)] = 00
重量级锁: [指向 Monitor 的指针(62) | lock(2)] = 10
GC 标记: [空(62) | lock(2)] = 113.4 四种引用类型
// 强引用:不会被 GC 回收,宁可 OOM
Object obj = new Object();
// 软引用:内存不足时回收,适合做缓存
SoftReference<Bitmap> softRef = new SoftReference<>(bitmap);
Bitmap b = softRef.get(); // 可能为 null
// 弱引用:下次 GC 就回收,不论内存是否充足
WeakReference<Activity> weakRef = new WeakReference<>(activity);
// 常用于避免内存泄漏,如 Handler 持有 Activity
// 虚引用:无法通过虚引用获取对象,仅用于回收通知
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);
phantomRef.get(); // 永远返回 nullAndroid 中的应用:
WeakReference:Handler 防泄漏、WeakHashMap 缓存SoftReference:图片内存缓存(但现在更推荐 LruCache)
4. GC 算法与收集器
4.1 三种基础算法
标记-清除(Mark-Sweep):
- 标记所有存活对象,清除未标记的
- 缺点:内存碎片
标记-复制(Mark-Copy):
- 将存活对象复制到另一块区域
- 新生代使用:Eden:S0:S1 = 8:1:1
- 优点:无碎片;缺点:浪费一半空间(实际只浪费 10%,因为 8:1:1)
标记-整理(Mark-Compact):
- 标记后将存活对象向一端移动
- 老年代使用
- 优点:无碎片;缺点:移动对象开销大
4.2 新生代 GC 流程
1. 新对象分配到 Eden
2. Eden 满 → Minor GC
3. 存活对象复制到 S0,年龄+1
4. 下次 Minor GC:Eden + S0 存活对象 → S1,年龄+1
5. S0 和 S1 交替使用
6. 年龄达到阈值(默认15)→ 晋升老年代
7. 老年代满 → Major GC / Full GC4.3 CMS 收集器
以最短停顿时间为目标的老年代收集器:
- 初始标记(STW):标记 GC Roots 直接关联的对象,速度快
- 并发标记:从 GC Roots 遍历整个对象图,与用户线程并发
- 重新标记(STW):修正并发标记期间变动的对象
- 并发清除:清除未标记对象,与用户线程并发
缺点:
- CPU 敏感(并发阶段占用线程)
- 浮动垃圾(并发清除阶段产生的新垃圾要下次才能清理)
- 内存碎片(标记-清除算法)
4.4 G1 收集器
面向服务端的收集器,可预测停顿:
- 将堆划分为多个大小相等的 Region(1-32MB)
- 每个 Region 可以是 Eden/Survivor/Old/Humongous
- 维护每个 Region 的回收价值(垃圾占比),优先回收价值高的(Garbage First)
- Mixed GC:同时回收新生代和部分老年代 Region
4.5 Android ART GC
Android Runtime 使用 CC(Concurrent Copying)收集器:
- 并发复制:GC 线程与应用线程并发执行
- 读屏障(Read Barrier):确保应用线程读到的是最新的对象引用
- 分代收集:年轻代用 Sticky CMS,老年代用 CC
- 压缩:复制算法天然无碎片
- 相比 Dalvik 的 CMS,ART CC 的停顿时间更短(通常 < 1ms)
5. 类加载机制
5.1 双亲委派模型
BootstrapClassLoader(C++ 实现,加载 rt.jar)
↑
ExtClassLoader(加载 ext 目录)
↑
AppClassLoader(加载 classpath)
↑
自定义 ClassLoaderprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 委托父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法加载
}
if (c == null) {
// 3. 自己加载
c = findClass(name);
}
}
return c;
}为什么要双亲委派?
- 避免类的重复加载
- 保证核心类库安全(用户无法自定义 java.lang.String 替换系统类)
5.2 类加载五阶段
- 加载:读取字节码,生成 Class 对象
- 验证:校验字节码格式、语义
- 准备:为静态变量分配内存并设零值(
static int a = 10此时 a=0) - 解析:符号引用 → 直接引用
- 初始化:执行
<clinit>(静态变量赋值 + 静态代码块)
5.3 打破双亲委派
场景一:JDBC SPI
DriverManager 在 rt.jar 中(BootstrapClassLoader 加载),但具体驱动(如 MySQL Driver)在 classpath 中。Bootstrap 无法加载 classpath 的类,所以用线程上下文类加载器:
// DriverManager 中
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// ServiceLoader.load 内部使用 Thread.currentThread().getContextClassLoader()场景二:热修复
Tinker 等热修复框架通过修改 DexPathList 中 dex 文件的顺序,将补丁 dex 插入到原始 dex 之前,利用类加载的"先找到先使用"特性实现类替换。
5.4 Android 类加载器
// PathClassLoader:加载已安装的 APK(data/app 目录)
// 系统默认使用,不能加载未安装的 dex
PathClassLoader pathCL = new PathClassLoader(apkPath, parent);
// DexClassLoader:可加载任意路径的 dex/jar/apk
// 插件化、热修复的基础
DexClassLoader dexCL = new DexClassLoader(dexPath, optimizedDir, libraryPath, parent);两者都继承自 BaseDexClassLoader,核心区别在于 optimizedDirectory 参数(API 26+ 已废弃该参数,两者基本等价)。
关联知识
- HashMap 的线程安全问题 → 引出 ConcurrentHashMap → 引出 CAS/synchronized → 见并发篇
- 类加载机制 → 引出热修复/插件化 → 见架构篇
- GC 与引用类型 → 引出内存泄漏检测 → 见性能优化篇
- 反射与注解 → 引出 APT/字节码插桩 → 见工程化篇
