Skip to content

Java 核心基础 — 泛型 / 反射 / JVM / GC / 类加载

更新: 5/15/2026 字数: 0 字 时长: 0 分钟

1. 泛型

1.1 类型擦除原理

Java 泛型是编译期特性,编译后泛型信息被擦除:

  • List<String>List<Integer> 编译后都是 List
  • 泛型类型参数被替换为上界(无界则为 Object

证明类型擦除

java
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 泛型边界

java
// 上界通配符:只能读,不能写(不知道具体是什么子类型)
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>
  • 如果既读又写,不用通配符
java
// 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 通过匿名内部类保留泛型信息:

java
// 匿名内部类的父类泛型信息保存在 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 可以在运行时获取泛型类型:

kotlin
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

java
// 获取 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 倍,原因:

  1. 安全检查:每次 invoke 都要检查访问权限
  2. 无法 JIT 优化:编译器无法内联反射调用
  3. 参数装箱:基本类型需要装箱为 Object[]
  4. 方法查找:需要遍历方法表

优化手段:

  • setAccessible(true) 跳过安全检查(提升约 4 倍)
  • 缓存 Method/Field 对象,避免重复查找
  • 使用 MethodHandle(JDK 7+)替代传统反射

2.3 注解保留策略

java
@Retention(RetentionPolicy.SOURCE)  // 编译后丢弃,如 @Override
@Retention(RetentionPolicy.CLASS)   // 保留在字节码,运行时不可见,如 @NonNull
@Retention(RetentionPolicy.RUNTIME) // 运行时可通过反射获取,如 @Deprecated

2.4 APT 编译期注解处理

APT(Annotation Processing Tool)在编译期扫描注解并生成代码:

java
@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 对象创建过程

  1. 类加载检查:检查 new 指令的参数能否在常量池中定位到类的符号引用,且该类已被加载
  2. 分配内存
    • 指针碰撞(Bump the Pointer):堆内存规整时,移动指针分配
    • 空闲列表(Free List):堆内存不规整时,从列表中找合适的块
    • 线程安全:TLAB(Thread Local Allocation Buffer)每个线程预分配一小块
  3. 初始化零值:所有字段设为默认值(0/null/false)
  4. 设置对象头:Mark Word + 类型指针
  5. 执行 <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)] = 11

3.4 四种引用类型

java
// 强引用:不会被 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(); // 永远返回 null

Android 中的应用

  • 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 GC

4.3 CMS 收集器

以最短停顿时间为目标的老年代收集器:

  1. 初始标记(STW):标记 GC Roots 直接关联的对象,速度快
  2. 并发标记:从 GC Roots 遍历整个对象图,与用户线程并发
  3. 重新标记(STW):修正并发标记期间变动的对象
  4. 并发清除:清除未标记对象,与用户线程并发

缺点:

  • 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)

自定义 ClassLoader
java
protected 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 类加载五阶段

  1. 加载:读取字节码,生成 Class 对象
  2. 验证:校验字节码格式、语义
  3. 准备:为静态变量分配内存并设零值(static int a = 10 此时 a=0)
  4. 解析:符号引用 → 直接引用
  5. 初始化:执行 <clinit>(静态变量赋值 + 静态代码块)

5.3 打破双亲委派

场景一:JDBC SPI

DriverManagerrt.jar 中(BootstrapClassLoader 加载),但具体驱动(如 MySQL Driver)在 classpath 中。Bootstrap 无法加载 classpath 的类,所以用线程上下文类加载器

java
// DriverManager 中
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// ServiceLoader.load 内部使用 Thread.currentThread().getContextClassLoader()

场景二:热修复

Tinker 等热修复框架通过修改 DexPathList 中 dex 文件的顺序,将补丁 dex 插入到原始 dex 之前,利用类加载的"先找到先使用"特性实现类替换。

5.4 Android 类加载器

java
// 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/字节码插桩 → 见工程化篇