当前位置: 首页 > news >正文

关于 java:8. Java 内存模型与 JVM 基础

一、堆

Java 堆是 JVM 中所有线程共享的运行时内存区域,用于存放所有对象实例、数组以及类的实例字段值

在 Java 中:

String str = new String("abc");
  • new String("abc") 创建的对象就分配在中。

1.1 堆的特点

特性说明
共享区域所有线程共享堆
GC 管理垃圾回收器对堆管理最频繁
分代模型为提高 GC 性能,堆被划分为新生代/老年代等区域
空间大堆是 JVM 管理内存中最大的区域
慢速堆分配慢于栈,依赖 GC 清理

1.2 堆的内部结构:分代模型

为提升垃圾回收效率,JVM 把堆进一步划分为:

Java 堆
├── 新生代(Young Generation)
│   ├── Eden 区
│   ├── Survivor 0(S0)
│   └── Survivor 1(S1)
└── 老年代(Old Generation)

1)新生代(Young Generation)

  • 创建的对象默认分配在 Eden 区

  • 大多数对象生命周期很短,会迅速被 GC 回收。

  • Minor GC 专门清理新生代。

★ 分区说明:

区域用途
Eden对象初始分配区域
Survivor 0 / 1Eden 回收后活下来的对象会在 S0/S1 之间“拷贝轮换”

新生代采用 复制算法(Copying GC),避免内存碎片。

2)老年代(Old Generation)

  • 长期存活或大对象被晋升(Promote)到老年代。

  • 老年代 GC 称为 Major GCFull GC

  • 回收成本较高,需尽量避免频繁触发。

3)大对象直接进入老年代

  • 大于阈值(如 PretenureSizeThreshold)的对象跳过 Eden,直接进老年代。

1.3 对象生命周期在堆中的流转

new → Eden → Survivor → Old
  • 对象创建在 Eden;

  • 如果发生 Minor GC 且对象存活 → 移至 Survivor;

  • 对象在 Survivor 区多次存活后(如 15 次)→ 晋升到老年代;

  • 老年代中对象若仍不可达 → 被 Full GC 清除。

1.4 JVM 参数:堆大小设置

参数说明
-Xms初始堆大小(最小)
-Xmx最大堆大小
-Xmn新生代大小(包括 Eden 和 Survivor)
-XX:SurvivorRatio=8Eden:Survivor 比例(8:1:1)
-XX:PretenureSizeThreshold大对象直接进入老年代的阈值

示例:

java -Xms512m -Xmx1024m -Xmn256m -XX:+PrintGCDetails MyApp

1.5 垃圾回收(GC)与堆的关系

GC 主要在堆中进行回收:

类型清理区域特点
Minor GC新生代频繁且速度快
Major GC / Full GC老年代 + 方法区代价高,暂停时间长
G1 GC跨代混合划分 Region、低延迟、高性能

1.6 堆的内存溢出(OutOfMemoryError)

常见堆异常:

java.lang.OutOfMemoryError: Java heap space

原因可能有:

  • 对象持续创建,无法被回收(引用泄漏)

  • JVM 堆太小

  • 大对象过多

  • 死循环缓存引用

解决方法:

  • 分析堆快照(工具:JVisualVM、MAT)

  • 优化对象生命周期

  • 增大堆(如:-Xmx2g

1.7 性能优化与调优策略

场景建议
Minor GC 频繁增加新生代大小 -Xmn
Full GC 频繁减少老年代对象生成,优化代码
OOM 崩溃提取堆快照分析泄漏源
延迟要求高使用 G1 GC、设置暂停时间 -XX:MaxGCPauseMillis

1.8 逆向与安全分析中堆的作用

应用分析方法
动态壳、脱壳在堆中查找动态解密后的类/对象
内存马植入某些 WebShell、Agent 注入的对象驻留在堆中
动态指针解密壳加载的关键结构常存放于堆
堆喷射攻击分析堆中构造恶意数据结构,触发漏洞利用
模拟器分析查看模拟环境中的堆分布是否与真机一致

1.9 实战示例(对象分配与 GC)

public class TestHeap {public static void main(String[] args) {byte[] arr1 = new byte[2 * 1024 * 1024]; // 2MBbyte[] arr2 = new byte[2 * 1024 * 1024];byte[] arr3 = new byte[2 * 1024 * 1024];byte[] arr4 = new byte[4 * 1024 * 1024]; // 4MB}
}
java -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails TestHeap

分析输出:

  • 新生代 10M,Eden 8M,S0/S1 各 1M;

  • 大于 4M 的对象直接进老年代;

  • 如果 Eden 装不下,就触发 Minor GC。

小结图

Java Heap
├── Young Generation(新生代)
│   ├── Eden(默认 8/10)
│   ├── Survivor0(1/10)
│   └── Survivor1(1/10)
└── Old Generation(老年代)
类型分配对象是否 GC 管理是否共享应用重点
new 的所有对象逆向脱壳、泄漏检测、优化

二、栈

Java 虚拟机栈(Java Stack) 是每个线程私有的内存区域,用于存储方法调用相关信息,包括 局部变量、操作数栈、动态链接、返回地址等

  • 每创建一个线程,就会创建一个栈;

  • 每调用一个方法,就会创建一个栈帧(Stack Frame)

  • 方法调用结束后,栈帧被销毁,返回上层方法继续执行。

2.1 栈的结构:栈帧(Stack Frame)

一个线程的栈是由一组栈帧组成的,每个栈帧代表一次方法调用过程,包含以下内容:

组成部分作用
局部变量表存储方法参数和局部变量(int、引用、float...)
操作数栈存储计算过程中的中间值
动态链接当前方法调用其他方法时的符号引用
返回地址方法返回后跳转到的字节码位置
异常处理表异常时用于跳转 catch 代码块

JVM 使用字节码解释器或 JIT 编译器操作这些栈帧。

2.2 举例说明:栈帧如何运作?

public class Test {public static void main(String[] args) {int a = 10;int b = sum(a, 20);}public static int sum(int x, int y) {int z = x + y;return z;}
}

执行顺序栈结构如下:

线程启动
└── main 栈帧└── 调用 sum()└── sum 栈帧(独立局部变量表、操作数栈)└── 执行完 → 返回结果 → main 栈帧继续

2.3 局部变量表(Local Variable Table)

  • 是一个线性表,用于存储基本类型变量和引用类型

  • 索引号从 0 开始,由字节码指令访问(如 iload_0)。

  • 64 位类型(longdouble)会占两个槽(slot)。

示例:

int x = 5;         // slot 0
String str = "hi"; // slot 1
long l = 123456L;  // slot 2 + 3

2.4 操作数栈(Operand Stack)

  • 每个方法的字节码指令执行时用操作数栈作为临时工作空间;

  • 栈式结构:先进后出;

  • 计算表达式时数据会先入栈,然后执行操作。

示例:

int a = 2 + 3;

JVM 字节码:

iconst_2        // 压入 2
iconst_3        // 压入 3
iadd            // 弹出两个操作数,加法后结果入栈
istore_1        // 将结果存入局部变量表 slot 1

2.5 方法返回地址 + 异常处理

  • 返回地址:用于指示方法执行完毕后,从哪里继续执行;

  • 异常表:用于当抛出异常时,查找是否有 catch 块处理。

2.6 JVM 参数控制栈大小

  • 每个线程的栈大小可以通过 -Xss 参数设置。

  • 栈过小 → StackOverflowError

  • 栈过大 → 启动线程数减少,可能 OOM(Out Of Memory,即内存溢出错误

java -Xss512k MyApp

2.7 异常:StackOverflowError

典型场景:递归无终止条件

public class StackTest {public static void recurse() {recurse();  // 无限递归}
}

执行后异常:

Exception in thread "main" java.lang.StackOverflowError

2.8 线程私有性与安全性

  • 每个线程都有独立的栈;

  • 栈不受其他线程干扰;

  • 所以 局部变量天然线程安全

2.9 栈与逆向工程的联系

场景分析点
方法调用追踪分析栈帧生成与销毁位置
动态调试时追踪参数查看局部变量表、操作数栈内容
SOF 崩溃排查找出调用链是否循环
异常劫持Hook 方法栈帧前后行为
虚拟机逃逸分析判断对象是否在栈上分配,可避免堆分配与 GC

2.10 栈与逃逸分析

JVM 可通过逃逸分析判断对象是否逃离当前方法:

  • 没有逃逸:可以在栈上分配,GC 不再管理

  • 优化方式:标量替换、栈上分配、锁消除

2.11 小结

名称特性内容与堆的区别
Java 栈线程私有栈帧(局部变量、操作数栈、返回地址)分配在栈上的是临时数据,堆存储持久对象
栈帧每次方法调用生成管理方法执行时的数据方法结束自动销毁

栈的核心优势与限制

优势限制
分配速度快容量小
生命周期清晰不适合大对象
不需要 GC 管理不适合共享数据

栈与堆对比

特性
所属线程线程私有所有线程共享
管理方式调用即分配,调用结束即销毁由 GC 管理
分配速度
存储内容方法调用上下文、局部变量new 出来的对象、数组
常见错误StackOverflowErrorOutOfMemoryError

三、方法区

方法区(Method Area) 是 Java 虚拟机中线程共享的一块内存区域,主要用于存储类的结构信息、静态变量、运行时常量池、JIT 编译代码等元数据。

方法区也被称为:

  • 非堆(Non-Heap)内存

  • 是 GC 管理的一部分。

3.1 方法区存储的内容

内容类型说明
类信息(Class Metadata)类名、父类、接口、字段、方法结构
字节码(Code)类中方法的字节码(用于解释执行或 JIT 编译)
静态变量所有 static 修饰的字段
运行时常量池类加载后从 class 文件中解析的常量
JIT 编译代码热点代码被 JIT 编译成机器码后缓存

3.2 JDK 版本变迁

JDK 7 及以前

  • 方法区实现为 永久代(PermGen)

  • -XX:PermSize -XX:MaxPermSize 控制

JDK 8 及以后

  • 永久代被移除,方法区改为 元空间(Metaspace)

  • 元空间位于 本地内存(Native Memory),不再在 JVM 堆中

JDK 11 起

  • 元空间优化:支持动态释放未使用的 class 元信息内存(降低 footprint)

3.3 元空间(Metaspace)结构

元空间组成描述
Class Metadata存储类的结构
Constant Pool存储类中的运行时常量
Intern String Pool字符串常量池,存放所有被 intern() 的字符串
Static Fields类的静态变量(包括 final 静态字段)

元空间的大小默认由操作系统限制,可用如下参数控制:

-XX:MetaspaceSize=128m            // 初始大小
-XX:MaxMetaspaceSize=512m         // 最大大小

3.4 方法区 vs 堆 vs 栈 对比

区域是否线程私有存储内容是否 GC 管理
方法区类结构、静态字段、常量池是(部分)
实例对象
方法调用、局部变量

3.5 方法区中的运行时常量池

  • 每个类有一个对应的常量池,记录编译时生成的各种常量,如:

    • 类名、方法名、字段名

    • 字符串、整型、浮点数字面量

    • 符号引用(Symbolic Reference)

示例(javap -v Hello.class 输出):

Constant pool:#1 = Methodref     #2.#3    // java/lang/Object."<init>":()V#2 = Class         #4       // java/lang/Object#3 = NameAndType   #5:#6    // "<init>":()V

逆向时可以通过常量池提取方法名、类名等重要信息,即使加了混淆也能找出关键结构。

3.6 方法区中的静态变量(static

  • 所有类的静态字段都存在方法区中;

  • 字段在类加载时初始化,仅一份;

  • 可被反射访问修改。

示例:

public class Demo {public static int counter = 100;  // 存放在方法区
}

3.7 GC 与方法区

  • JDK 8 之后,元空间部分内容(如 unused class metadata)也会被 GC 回收;

  • GC 主要针对:

    • 废弃的类加载器(ClassLoader)

    • Class 元信息

示例:

  • 如果频繁生成动态类(如:使用 cglib、Javassist 动态代理),未及时卸载,会导致:

java.lang.OutOfMemoryError: Metaspace

3.8 常见异常:OutOfMemoryError: Metaspace

原因:

  • 创建过多的动态类(如 Spring AOP、Groovy 脚本)

  • 类未卸载(类加载器泄漏)

  • 元空间大小过小

解决方式:

  • 使用 -XX:MaxMetaspaceSize=512m 增大元空间

  • 合理设计类加载器结构,避免泄漏

  • 使用 -XX:+ClassUnloading 配合 G1 实现类信息卸载

3.9 逆向分析与方法区

1)获取类结构信息

  • 方法区中保存了所有类的结构;

  • 可以用反射、Instrumentation、JVMTI 等 API 读取所有已加载类。

Class[] classes = instrumentation.getAllLoadedClasses();

2)动态类加载跟踪

  • 若壳或恶意代码使用自定义 ClassLoader 加载字节码,可以 Hook:

    • defineClass()loadClass() 方法

  • 加载后,这些类的元信息会注册进方法区(Metaspace)

3)常量池逆向分析

  • 可提取混淆后的类名、字段名、字符串,辅助代码还原

  • 特别适用于逆向 Android 中的 .dex.class 后分析结构还原

3.10 反编译 & 方法区调试建议

工具用途
javap -v查看字节码与常量池
JClassLib图形化查看 .class 文件结构(含方法区内容)
JVM TI / JVMTI Agent动态注入 agent,监控类加载与方法区变化
VisualVM查看类加载器、类实例、方法区使用
MAT分析内存快照中的 class metadata 占用情况

3.11 小结

项目方法区(JDK7前)元空间(JDK8+)
所在区域JVM 堆内本地内存
管理内容类信息、常量池、静态变量同上
可配置参数PermSize/MaxPermSizeMetaspaceSize/MaxMetaspaceSize
OOM 异常PermGen spaceMetaspace
调优方式增大永久代大小限制元空间最大值,控制类加载量

方法区是 类的“大脑”,保存了所有类的结构信息、静态变量与常量池,是动态加载、混淆壳分析、反射攻击、内存马植入的核心落点区域。


四、常量池

常量池是类加载后存储在 JVM 方法区中的一部分数据结构,主要包含:

  • 字面量(如整数、浮点数、字符串等)

  • 符号引用(如类名、字段名、方法名)

  • 用于运行时构造字段、方法的元信息

常量池包括两部分:

名称说明
编译期常量池(Constant Pool Table)编译后 .class 文件中存在的常量池结构
运行时常量池(Runtime Constant Pool)类加载到 JVM 后,解析 .class 中的常量池转为 JVM 可用结构

4.1 常量池在 JVM 内存中的位置

区域存储内容
方法区的一部分每个类或接口都有一个运行时常量池
与 class 文件一一对应每加载一个 class 文件,就有一个对应常量池

4.2 常量池存储的内容(分类)

1)字面量常量(Literal Constants)

  • 数值常量:int, long, float, double

  • 字符串常量:"hello"

  • 布尔值、null、字符等字面量

int a = 100;      // 常量池中记录 100
String s = "abc"; // 字符串常量池中记录 "abc"

2)符号引用(Symbolic References)

  • 类名引用(Class)

  • 字段引用(FieldRef)

  • 方法引用(MethodRef)

  • 接口方法引用(InterfaceMethodRef)

String s = obj.toString();

这里的 toString 会作为符号引用放进常量池,运行时解析为 obj 的具体实现。

4.3 class 文件中常量池结构(字节码视角)

使用 javap -v Hello.class 查看字节码,可以看到常量池:

javap -v Hello.class

示例输出:

Constant pool:#1 = Methodref      #6.#15      // java/lang/Object."<init>":()V#2 = Class          #16         // Hello#3 = Utf8           Hello#4 = Utf8           java/lang/Object#5 = Utf8           main#6 = Utf8           ([Ljava/lang/String;)V

常见常量项类型表:

tag名称说明
1Utf8字符串或符号名
3Integerint 类型
4Floatfloat 类型
5Longlong 类型
6Doubledouble 类型
7Class类或接口引用
8String字符串引用
9Fieldref字段引用
10Methodref方法引用
12NameAndType字段/方法名+类型
15MethodHandleLambda 表达式/动态调用
18InvokeDynamic动态调用指令(Lambda、反射)

4.4 字符串常量池(String Constant Pool)

字符串是特殊的:

String a = "hello";
String b = "hello";
System.out.println(a == b); // true,因为指向相同常量池对象
  • 字符串常量池在 JVM 启动时初始化;

  • 所有 "字符串" 直接量存放于此;

  • new String("hello") 会创建两个对象:一个在常量池,一个在堆上;

  • 可使用 str.intern() 强制把字符串加入常量池或返回已有常量池引用。

4.5 运行时常量池与类加载器

  • 每个 class 文件有独立的常量池;

  • 加载时常量池会被解析并转化为 JVM 内部引用;

  • 可通过 Class.getConstantPool()Instrumentation 获取。

4.6 与反射、动态调用关系密切

反射、Lambda 表达式、动态代理等功能,其方法名、类名、字段名都存在常量池中:

Class<?> clazz = Class.forName("com.example.Hello");  // "com.example.Hello" 来自常量池

逆向时,可从常量池中提取这些敏感字符串,帮助快速定位逻辑结构。

4.7 常量池逆向分析实战应用

应用场景分析方式
代码混淆分析从常量池中提取字段/方法/类名(如:e.a.b.a() → 原名)
类名/方法名还原提取 ClassMethodref 字段对照映射表
字符串解密入口字符串加密器的加密内容往往来自常量池(如 "encrypted_abc"
静态调用跟踪常量池记录了调用链(MethodRef → NameAndType)
反射调用恢复Class.forNameMethod.invoke 参数常藏在常量池中

4.8 查看常量池的工具

工具用途
javap -v官方命令行工具,查看 class 常量池
JClassLib图形化查看 class 文件结构与常量池
CFR / Procyon / fernflower反编译并展示结构
ASM / Javassist程序化修改字节码中的常量池

4.9 混淆分析中的常量池提取示例

# 通过 javap 反编一个混淆类,查看方法名、字段名
Class: a.b.c
Methodref: a.b.c.e(III)Ljava/lang/String;
Utf8: "密钥" → 说明解密逻辑在这里

对逆向安全来说,常量池是“解密的金矿”,尤其在反编译被混淆的壳时,常量池未加密往往成为突破口

4.10 JVM 限制与 OOM 风险

  • 常量池大小有限(65535 项)

  • 极端情况下可以构造 OutOfMemoryError: constant pool full

  • 常见于恶意 class 文件攻击、Fuzzing 工具测试

4.11 小结

内容是否 GC 管理是否共享用于什么
常量池整体是(每类一个)存储常量、符号引用
字符串常量池管理字符串字面量
与方法区关系是其一部分类结构核心
与逆向关系高度相关提取混淆/解密入口

常量池是 class 文件的“元数据仓库”,是逆向定位结构、还原逻辑、解密混淆、理解类加载机制的基础入口。


五、直接内存

直接内存是指 JVM 堆外的一块内存区域,由操作系统分配,不属于 Java 堆,也不在方法区,但受 JVM 管理和限制

它主要通过:

  • java.nio.ByteBuffer.allocateDirect(...)

  • Unsafe.allocateMemory(...)

  • MappedByteBuffer(内存映射文件)

进行分配和访问。

5.1 直接内存 vs JVM 堆内存

对比点JVM 堆内存直接内存
所在位置JVM 管理(堆内)系统内存(堆外)
GC 是否管否(手动或隐式回收)
分配方式newByteBuffer.allocateDirect()Unsafe
性能有 GC 影响高速、适合大数据传输
用途存储普通对象存储大量数据、高性能 I/O 缓冲区

5.2 为什么使用直接内存?

高性能 I/O 的关键:避免 JVM 中对象 → 本地内存的来回复制

在使用传统堆内内存时:

  • 数据先从磁盘 → native buffer → JVM 堆中对象

而使用直接内存:

  • 数据从磁盘直接读入堆外内存,省去中间拷贝

优点:

  • 零拷贝(Zero-Copy)能力

  • 避免 GC 影响

  • 文件内存映射(提升读写效率)

5.3 直接内存的使用方式

1)使用 NIO ByteBuffer 分配

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
  • 分配 1KB 的堆外内存

  • 由 JVM 封装底层 malloc,内存交由操作系统管理

  • 不属于 Java 堆,不会被 GC 自动清理,由 JVM 内部回收器(Cleaner)管理

2)使用 Unsafe 分配(更底层)

Unsafe unsafe = getUnsafe();  // 获取方式略麻烦
long address = unsafe.allocateMemory(1024); // 分配 1024 字节
unsafe.putByte(address, (byte) 1);
unsafe.freeMemory(address); // 记得释放!
  • 更接近操作系统底层

  • 若未释放会造成堆外内存泄漏

3)使用 MappedByteBuffer 进行文件内存映射

FileChannel channel = new RandomAccessFile("data.txt", "rw").getChannel();
MappedByteBuffer mapped = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());
mapped.put(0, (byte) 65);  // 修改文件内容,A 对应 65
  • OS 将文件内容映射到内存地址空间

  • 修改 mapped 相当于直接修改磁盘文件

  • 广泛用于数据库、高频交易系统、搜索引擎等

5.4 直接内存与 JVM 参数控制

虽然不在堆中,但 JVM 会尝试限制其最大使用量

-XX:MaxDirectMemorySize=512m

默认行为:

  • 如果未设置该参数,JVM 会自动设置为 堆最大值(如 -Xmx大小

5.5 直接内存泄漏与异常

若未及时释放:

java.lang.OutOfMemoryError: Direct buffer memory

常见原因:

原因说明
使用 allocateDirect 分配后未释放Cleaner 没来得及清理
使用 Unsafe 分配未调用 freeMemory纯手动内存管理失误
内存映射文件使用后未 unmap某些 JVM 无法及时 GC unmap

5.6 直接内存与 GC 的关系

  • 堆外内存不会被 GC 直接管理

  • 但直接内存对象(如 DirectByteBuffer)在 Java 对象中持有 Cleaner 引用;

  • 当 DirectByteBuffer 被 GC 回收后,Cleaner 会释放对应 native 内存

若对象长时间引用未释放,则直接内存无法回收,导致内存泄漏。

5.7 实际应用场景

应用场景用途
Netty 网络框架使用 DirectBuffer 提高 I/O 性能
Kafka 消息系统零拷贝传输使用直接内存
数据库(如 H2)使用 MappedByteBuffer 映射文件
文件传输/高频交易内存映射文件、堆外缓存池
游戏引擎快速纹理加载与显存交互

5.8 逆向与安全相关用途

应用场景技术点
内存反作弊检测游戏或程序会在直接内存中存放加密逻辑、状态标志位
壳加载数据一些混淆壳使用 DirectBuffer 存储解密后的 class 字节码
动态类注入使用 defineClass 加载从直接内存中读取的 class 数据
规避 GC 跟踪恶意代码将关键信息存放在堆外,绕过 JVM 常规监控
JNI native 方法调用native 方法操作的内存区域常来自直接内存地址(address 参数)

5.9 工具查看与调试

工具用途
jcmd <pid> VM.native_memory summary查看堆外内存使用情况
jmap可查看堆内,但无法查看直接内存
NMT(Native Memory Tracking)原生内存追踪,可查 DirectMemory 占用
VisualVM + NMT plugin图形界面查看直接内存使用
perf / valgrindC 层堆外内存使用分析

5.10 小结

项目说明
存储位置堆外(native memory)
分配方式allocateDirect、Unsafe.allocateMemory
GC 影响不受 GC 管理,需手动或通过 Cleaner 回收
使用场景高性能 IO、内存映射、Zero-Copy
主要风险内存泄漏、OOM、GC 无法清理
监控手段jcmd、NMT、VisualVM 插件
逆向重点堆外存放关键结构、加密逻辑、绕过 JVM 安全检测机制

直接内存是 JVM 高性能 IO 的利器,也是逆向与安全分析中规避 JVM 监管的常用技术手段。


六、类的加载机制(双亲委派模型)

类加载机制负责 将 .class 文件加载到 JVM 并转化为 Class 对象,是 Java 程序运行前的第一步。

Java 类加载过程分为:

【加载】→【验证】→【准备】→【解析】→【初始化】

其中,“加载”这一步中使用了“类加载器”机制(ClassLoader),而其中的核心就是双亲委派模型(Parent Delegation Model)

6.1 类加载的五个阶段

阶段说明
加载(Loading)读取 .class 文件为字节流并转化为 Class 对象
验证(Verification)检查字节码合法性、安全性(防止恶意代码)
准备(Preparation)为静态变量分配内存,并赋初始值(非初始化值)
解析(Resolution)将常量池中的符号引用转为直接引用(如方法、类)
初始化(Initialization)执行类的 <clinit> 静态初始化代码块和静态字段赋值

6.2 什么是类加载器(ClassLoader)?

类加载器负责加载类的字节码,并生成 Class 对象。JVM 中类由类加载器标识,类名 + 加载器 才能唯一确定一个类。

所以:两个类名相同但由不同 ClassLoader 加载,它们是两个不同的类

6.3 双亲委派模型(Parent Delegation Model)

核心思想:先让父类加载器尝试加载,如果父类加载失败,才由当前加载器尝试加载。

即:

当前类加载器 → 委托父加载器 → 逐级向上 → 直到 Bootstrap ClassLoader → 没找到才返回尝试自己加载

好处:

  • 防止重复加载

  • 防止核心类库被恶意篡改(如替换 java.lang.String)

6.4 类加载器体系结构

BootstrapClassLoader
↑
ExtClassLoader(Extension)
↑
AppClassLoader(System)
↑
自定义 ClassLoader
加载器说明
Bootstrap ClassLoaderC++ 实现,加载 rt.jar(JDK核心类)
ExtClassLoader加载 jre/lib/ext/*.jar
AppClassLoader加载 classpath 中的类
用户自定义加载器自定义逻辑(插件系统、热部署、加壳)

6.5 双亲委派流程图

      请求加载类 String↓
自定义类加载器(找父)↓AppClassLoader → ExtClassLoader → Bootstrap(找到 String.class)↑ 加载成功,返回

只有当父类找不到时,才会由子加载器亲自尝试。

6.6 实战示例:自定义类加载器

public class MyClassLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] data = loadClassFromDisk(name);  // 你自定义的加载逻辑return defineClass(name, data, 0, data.length);}
}

可用于加密类解密、插件隔离、JVM内存马注入等。

6.7 打破双亲委派:沙箱逃逸 & 热部署核心

  • 正常 ClassLoader 遵守双亲委派;

  • 某些框架(如 OSGi、Tomcat、JSP 热加载)会打破双亲委派模型,实现类隔离与版本控制。

如何打破?

  • 不调用 super.loadClass(),直接使用 findClass() 加载自己的类;

  • 自定义类加载器加载的类不能访问父加载器的类,避免污染。

6.8 典型应用场景

场景类加载行为
插件系统(IDEA 插件)每个插件使用独立的 ClassLoader
热部署每次热加载都创建新的 ClassLoader(旧的等 GC 回收)
加密壳加载使用自定义加载器解密字节流再 defineClass()
沙箱安全机制利用加载器隔离类、控制访问权限
JDBC DriverSPI 加载 JDBC 驱动时,使用线程上下文类加载器加载
Servlet/Tomcat每个 WebApp 使用独立加载器,防止类污染

6.9 逆向与安全实战中的应用

应用点示例说明
脱壳 & 加密类被自定义加载器动态加载(无法在磁盘找到 class)
字节码分析需要手动 trace defineClass 的调用栈
动态注入使用 defineClass 将字节码注入 JVM(如内存马)
沙箱逃逸检测是否加载非 core class 中的类(绕过双亲委派)
类隔离逃逸判断当前类是否由 AppClassLoader 加载

6.10 小结

项目说明
双亲委派模型先委托父加载器加载,父找不到再自己加载
优点安全性高、防止重复加载
加载器层次结构Bootstrap → Ext → App → 自定义
自定义加载器用途插件隔离、解密壳、热部署、内存马注入
逆向常识ClassLoader 是壳的重要点,Class + Loader 决定类唯一性

双亲委派模型是 JVM 保证类加载安全的第一道防线,而破坏双亲委派机制,正是加壳、反射攻击、热部署的关键入口。


七、字节码结构(.class 文件分析)

.class Java 编译器生成的中间产物,它不是源代码,也不是机器码,而是 JVM 能识别的一种 平台无关的二进制格式,其核心是字节码(bytecode)+ 元信息

JVM 规范对 .class 文件的结构做了非常严格的定义,使得 逆向工程、编译器开发、AST 替换、反混淆处理成为可能。

7.1 .class 文件结构总览

按顺序排列如下结构:

| 魔数 Magic Number          → 固定 0xCAFEBABE
| 次版本号 Minor Version
| 主版本号 Major Version
| 常量池 Constant Pool
| 访问标志 Access Flags
| 类索引 This Class
| 父类索引 Super Class
| 接口索引表 Interfaces
| 字段表 Fields
| 方法表 Methods
| 属性表 Attributes(包括 Code、LineNumber、SourceFile 等)

7.2 结构字段详细说明

1)魔数(Magic Number)

  • 固定值:0xCAFEBABE

  • 作用:识别是否为合法的 Java class 文件

2)版本号(Minor + Major)

表示 Java 版本:

  • Java 8 → 52

  • Java 11 → 55

  • Java 17 → 61

Minor Version: 0
Major Version: 52 (JDK 1.8)

3)常量池(Constant Pool)

  • 存储字面量、类名、字段、方法、符号引用等

  • 使用索引访问

  • 表项格式:tag + data(长度不固定)

#1 = Methodref     #2.#3 // java/lang/Object."<init>":()V
#2 = Class         #4    // java/lang/Object
#3 = NameAndType   #5:#6 // "<init>":()V

在混淆逆向中,常量池可以还原方法名、类名、调用关系。

4)访问标志(Access Flags)

  • 表示类的修饰属性,如 public、final、interface 等
标志
0x0001public
0x0020super(特殊调用方式)
0x0200interface
0x0400abstract
Access flags: 0x0021 (public, super)

5)类索引(This Class)

  • 常量池中的一个 Class 类型项

  • 表示当前类的类名

6)父类索引(Super Class)

  • 指向常量池中父类名称

  • 若是 java.lang.Object 则为 0

7)接口表(Interfaces)

  • 当前类实现的接口集合,索引到常量池中 Class 类型
Interfaces count: 1
#25 = Interface java/io/Serializable

8)字段表(Fields)

  • 所有成员变量(不包括局部变量)

  • 包括访问标志、名称索引、描述符索引、属性(如 ConstantValue)

private static final int counter = 100;

可映射为:

Field:name: counterdescriptor: Iaccess: 0x001A (private, static, final)ConstantValue: 100

9)方法表(Methods)

  • 所有方法定义,包括构造器 <init>、静态块 <clinit>

  • 每个方法包括:

    • 访问标志

    • 名称

    • 描述符(如 (I)V 表示参数为 int、返回 void)

    • 属性(最关键:Code 字节码)

Method:name: maindescriptor: ([Ljava/lang/String;)VCode:0: getstatic3: ldc "hello"5: invokevirtual println

10)属性表(Attributes)

  • 存储附加信息,如:

    • Code方法字节码

    • LineNumberTable调试信息,源代码行号

    • LocalVariableTable局部变量名表

    • SourceFile源代码文件名

    • Signature泛型签名

举例(Code 部分):

Code:stack=2, locals=1, args_size=10: getstatic     #23: ldc           #35: invokevirtual #4

每个 Code 中包含:max_stackmax_locals字节码数组、异常表、Code Attributes

7.3 方法描述符(Method Descriptor)

描述符含义
Iint
Jlong
Zboolean
Ljava/lang/String;String
(I)V参数为 int,返回 void
()Ljava/lang/String;无参返回 String

7.4 字节码指令集(Opcode)

指令含义
getstatic获取静态字段
ldc加载常量池字面量
invokevirtual调用实例方法
invokestatic调用静态方法
returnvoid 返回
ireturn返回 int

可以使用 javap -cASMifier 观察字节码。

7.5 用工具分析 .class 文件结构

工具功能
javap -v Xxx.class官方命令行工具,输出完整结构
JClassLib图形化查看 .class 文件结构与常量池
ASM / BCEL / Javassist字节码读取 + 修改
CFR / Fernflower / Procyon反编译为源码
HexEditor + class 格式规范手动查看二进制内容

7.6 逆向分析与安全用途

用途描述
混淆还原通过常量池、描述符还原真实类名方法名
恶意字节码检测检查字节码结构异常(符号异常、访问标志伪造)
热加载 & class 注入动态生成 class 结构并用 defineClass() 注入
内存马识别识别恶意 class 中的隐藏行为(如反射调用、native)
壳还原加壳后的 class 解密后结构分析与恢复

7.7 完整流程小示例

源代码:

public class Hello {public static void main(String[] args) {System.out.println("Hello World");}
}

运行:

javac Hello.java
javap -v Hello.class

输出节选:

Magic: 0xCAFEBABE
Minor version: 0
Major version: 52
Constant pool:#1 = Methodref     #5.#17   // java/lang/Object."<init>":()V#2 = Fieldref      #18.#19  // java/lang/System.out:Ljava/io/PrintStream;#3 = String        #20      // Hello World#4 = Methodref     #21.#22  // java/io/PrintStream.println:(Ljava/lang/String;)V

7.8 小结

.class 文件是 JVM 的“汇编语言”,理解它就掌握了 Java 字节码编程、逆向、加壳解壳、安全防护的根本钥匙。


八、Java 对象生命周期

Java 对象生命周期的 6 个阶段

new → 初始化 → 使用 → 不再引用 → 等待回收 → 被 GC 清理

或者:

new 创建对象
→ 对象进入 Eden 区
→ 经 Minor GC 后进入 Survivor 区
→ 经多次 Minor GC 后晋升到 Old 区
→ 最终在 Full GC 中被清除

8.1 对象的创建过程

使用 new 关键字创建对象

User u = new User();

JVM 创建对象的底层流程如下:

步骤描述
类是否已加载没有则触发类加载、验证、准备、初始化
分配内存空间在堆中为对象分配一块连续内存(Eden 区)
对象内存初始化自动将实例字段设置为默认值
执行构造函数执行 <init> 方法,设置字段初始值

8.2 对象在内存中的存储结构

Java 对象在 JVM 中有如下三部分组成:

区块含义
对象头包含类元数据指针、哈希码、GC 状态、锁信息等
实例数据实际存储的字段值(按继承层级顺序排列)
对齐填充使对象大小为 8 字节的整数倍

8.3 对象的使用与逃逸

  • 对象创建后可以在堆中被多个线程访问;

  • 编译器可能通过逃逸分析优化对象分配位置:

逃逸分析三种情况:

类型说明优化方向
不逃逸只在当前线程和方法中使用可栈上分配
方法逃逸传出方法需要堆上分配
线程逃逸被多线程共享禁止同步消除
public void test() {User u = new User(); // 无逃逸,可栈分配System.out.println(u.name); // 也可能标量替换
}

8.4 对象的生命周期与 GC 管理

Java 的 GC 管理采用 可达性分析算法(Reachability Analysis)

对象是否“存活”依赖于是否可从 GC Root 可达。

GC Root 来源:

  • 当前线程栈中的局部变量

  • 静态字段引用

  • JNI 引用

8.5 对象存活时间与内存区域(分代)

年轻代(Young Generation)

  • 包含 Eden、S0、S1(两个 Survivor 区)

  • 新创建的对象首先进入 Eden 区

  • Minor GC:年轻代的垃圾回收

Eden → Survivor0 → Survivor1 → 晋升 Old 区
  • 一般经过 15 次 Survivor 区复制 后,进入老年代

老年代(Old Generation)

  • 存放生命周期长的对象,或大对象

  • 回收频率低但代价高

  • 被 Full GC 处理

永久代 / 元空间(PermGen / Metaspace)

  • 存放类元信息(class结构、方法、常量池)

  • Java 8 后永久代被移除,改为 本地内存的 Metaspace

8.6 对象何时被回收(GC 阶段)

条件是否回收
GC Root 不可达有可能回收(进入 Finalization)
实现 finalize()JVM 可能再次保留引用(仅一次机会)
被引用计数Java 不采用引用计数,采用可达性算法

示例:

public class User {protected void finalize() throws Throwable {System.out.println("finalize called");}
}

该方法会在对象第一次被判定为不可达时被调用一次,但并不保证何时调用或是否调用。

8.7 回收阶段示意图

new → Eden
→ S0 → S1(每次 Minor GC)
→ 多次后晋升 Old 区
→ 老年代 GC(Full GC)
→ 不可达 → Finalize
→ 回收 or Rescue(自救)失败 → 被清除

8.8 对象生命周期优化相关

技术说明
栈上分配小对象可在栈上分配,避免 GC
标量替换将对象拆为多个值
同步消除移除无需加锁的同步代码块
TLAB 分配每个线程分配一块私有内存空间(Thread Local Allocation Buffer)

8.9 逆向 / 调试应用场景

应用点描述
内存马分析恶意对象存在堆中但 GC Root 可达(永不回收)
动态类注入注入对象控制生命周期,如 defineClass → 不可达 → 垃圾回收
泄漏分析分析对象引用链是否断裂,是否被缓存错误持有
JVM Hook监控对象构造 / finalize / 回收过程
GCLog 分析追踪对象是否在 Minor/Full GC 中被回收

8.10 命令与工具支持

工具 / 命令作用
jmap -histo查看堆中对象分布
jmap -dump导出堆快照
MATEclipse Memory Analyzer,用于分析泄漏、对象生命周期
VisualVM图形化内存监控、GC 日志分析
jvisualgc观察各代内存回收活动
-XX:+PrintGCDetails输出 GC 详细日志
-XX:+UseTLAB启用线程本地内存池

8.11 小结

阶段描述
创建(new)堆上分配、TLAB
初始化构造函数执行
使用中被引用、在 GC Root 可达路径中
不再引用引用断裂,进入回收流程
Finalizationfinalize()(可能)执行一次
回收被 GC 移除,堆内存释放

Java 对象从创建到销毁遵循“分代收集 + 可达性分析”原则,其生命周期管理是性能调优、GC 调试、JVM 攻击防御的核心。


九、GC 算法

GC 总览:为什么需要垃圾回收?

  • Java 使用自动内存管理,对象一旦**不可达(GC Root 不可达)**就应被清除。

  • GC 目的:自动释放无用内存,避免泄漏与崩溃。

  • Java 使用“分代回收模型”,不同区域采用不同 GC 算法。

9.1 JVM 分代模型

分代特点默认回收算法
年轻代(Young)创建新对象,生命周期短,回收频繁复制算法
老年代(Old)生命周期长,对象大,回收代价高标记-清除 / G1
元空间(Metaspace)存储类信息(替代永久代)N/A

9.2 常见 GC 算法汇总一览表

算法原理是否压缩应用代适用场景
标记-清除(Mark-Sweep)标记可达 → 清除不可达 ✘不压缩老年代老式垃圾回收器
复制算法(Copying)将存活对象复制到新区域 ✔有压缩年轻代对象生命周期短
标记-整理(Mark-Compact)标记 → 移动对象 → 压缩老年代避免碎片
G1(Garbage First)按区域增量收集,高并发低停顿 ✔分区压缩所有代大堆 + 多核系统
ZGC低延迟压缩 GC(<10ms 停顿)所有代极低延迟场景
Shenandoah并发压缩所有代响应时间极低系统

9.3 标记-清除算法(Mark-Sweep)

原理:

  • 标记所有 GC Root 可达的对象;

  • 清除所有未被标记的对象,释放内存;

  • 内存空间不整理,可能产生碎片。

[Live][Dead][Live][Dead] → GC → [Live][  ][Live][  ]

优点:

  • 实现简单,适合老年代对象。

缺点:

  • 碎片化严重 → 堆空间碎片会影响后续分配;

  • 回收时间长,GC 停顿长。

9.4 复制算法(Copying)

原理:

  • 将内存划为两个区域(如 Eden 和 Survivor);

  • 每次 GC 只在一个区域中标记活对象,并复制到另一块;

  • 所有未复制的对象视为垃圾。

[Eden: A, B] → GC → [Survivor: A, B],Eden 清空

优点:

  • 无碎片,因为复制是连续的;

  • 效率高,只处理活对象。

缺点:

  • 空间浪费严重(50% 内存浪费);

  • 复制成本高;

  • 只适合对象存活率低的年轻代。

9.5 标记-整理算法(Mark-Compact)

原理:

  • 标记所有存活对象;

  • 将所有存活对象向一端压缩;

  • 清除未使用内存。

[Live][Dead][Live] → [Live][Live][  ]

优点:

  • 解决了碎片问题

  • 适合老年代。

缺点:

  • 需要对象移动 → 会更新所有引用 → 成本高;

  • 停顿时间仍然较长。

9.6 G1 GC(Garbage First)

G1 是 Java 9+ 默认 GC(Java 11 更成熟)

原理:

  • 将堆划分为多个 小区域(Region)

  • 每个 Region 可是 Eden、Survivor 或 Old;

  • 使用增量并发标记 + 区域优先清理策略:

    • 优先回收垃圾最多的 Region(Garbage First);

  • GC 时同时处理年轻代和老年代;

  • 停顿时间可配置:-XX:MaxGCPauseMillis=200

优点:

  • 并发标记 → 低停顿

  • 避免碎片;

  • 大堆管理能力强(>4GB);

缺点:

  • 调优复杂;

  • 边界场景下性能不稳定;

  • 不如 ZGC 延迟低。

9.7 ZGC(低延迟 GC)

Java 11+ 支持,JDK 17 成熟,可实现 GC 停顿 < 1ms

原理:

  • 全部使用 并发标记、并发压缩

  • 使用颜色指针 + 读屏障(Read Barrier)实现并发移动对象

特点:

特性
停顿时间< 1ms(超低)
最大堆大小TB 级别
回收方式并发标记、并发复制、并发压缩
指令开销比 G1 稍高,但适合低延迟服务

缺点:

  • 不支持 32 位 JVM;

  • 只在特定业务适用,如交易系统、实时游戏;

9.8 Shenandoah GC(另一种低延迟 GC)

  • RedHat 提出,Java 12 后加入正式版本;

  • 类似 ZGC,但使用写屏障(Write Barrier)+ 并发压缩;

  • 停顿时间 ≈ ZGC,性能可能略优。

9.9 选择哪种 GC?

场景推荐 GC
小应用 / 默认服务G1(JDK 9+ 默认)
大内存、响应要求高ZGC / Shenandoah
对象存活短、分配频繁Serial / Parallel(复制算法)
老年代大、碎片多CMS(旧)、G1(新)
高并发服务G1 / ZGC

9.10 GC 参数调优示例

# G1 + 限制停顿时间
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200# ZGC 启用
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC# Shenandoah 启用
-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

9.11 如何观察 GC 行为

工具用途
-Xlog:gc*(JDK 9+)输出 GC 详情
jstat -gc <pid>查看 JVM 各区内存
VisualVM图形监控 GC 情况
GCEasy.io分析 GC 日志
JFRJava Flight Recorder,实时采样 GC 停顿

9.12 逆向与安全用途

用途描述
对象保活使恶意对象不被 GC 清除(通过 GC Root 引用)
内存马利用 GC 漏洞隐藏 payload 对象
GC Hook利用 finalize() 或 Cleaner 执行恶意代码
shellcode loaderGC Root 悬挂对象持有 native shellcode
壳类生成加壳后 class 加载与释放配合 GC 操作

9.13 小结

从标记清除到 ZGC,Java GC 算法从简单效率走向并发压缩、低延迟高并发,了解这些算法是 JVM 调优、逆向攻防、稳定性保障的基础能力。


十、反编译工具分析

反编译工具的作用是:

  • .class .dex 文件还原为近似的 Java 源码;

  • 还原类结构、方法名、字段、控制流等;

  • 用于逆向分析、调试、加固绕过、混淆还原、审计等工作。

10.1 工具对比一览表

工具名支持格式输出格式优势劣势
jadx.dex, .apkJava + Smali安卓反编译一把手对混淆还原较弱
javap.class字节码(JVM 指令)官方权威,字节码分析利器不输出源码
fernflower.class, .jarJava 源码支持结构还原、被 IDEA 集成不支持 DEX

10.2 jadx – Android DEX 反编译神器

支持输入

  • .dex, .apk, .jar

输出格式

  • Java 源码

  • Smali 汇编(类 Dalvik 字节码)

  • 控制流图(可视化)

安装方式

# 安装 jadx
git clone https://github.com/skylot/jadx.git
cd jadx
./gradlew dist# 或使用 GUI
jadx-gui your.apk

使用示例

jadx -d out your.apk

目录结构:

out/└── com/example/MainActivity.javautils/Encrypt.java

也可使用 jadx-gui 图形化分析,支持:

  • 查找字符串/类/方法

  • 查看 smali 与 Java 双视图

  • 高亮调用链

逆向应用:

  • APK 破解、加密函数还原、查找 Web API、定位壳结构;

  • 搭配 Frida 定位要 Hook 的 Java 类和方法;

  • .so 中 JNI 函数调用也能反推调用点。

10.3 javap – 官方字节码查看器(低级分析利器)

输入

  • .class 文件

输出格式

  • 字节码(JVM 指令)

  • 常量池、方法签名、字段、属性等元信息

常用参数

javap -c Hello        # 输出字节码指令
javap -v Hello        # 全部详细信息(版本、常量池、方法表、属性等)
javap -p Hello        # 显示 private 方法

示例输出(-c)

public static void main(String[] args);Code:0: getstatic     #2   // System.out3: ldc           #3   // "Hello World"5: invokevirtual #4   // println8: return

可查看的内容包括:

  • JVM opcode(如 getstatic、ldc、invokevirtual)

  • 方法描述符 (Ljava/lang/String;)V

  • 局部变量表、行号表

  • Code 属性区

逆向用途:

  • 对比反编译源码与真实指令(识别插桩/壳);

  • 分析混淆后的真实调用路径;

  • 拆解 class 加壳、插码逻辑;

  • 自定义 class 构造(配合 ASM);

10.4 fernflower – 通用 Java class 反编译器(IDEA 默认用它)

是 IntelliJ IDEA / JD-GUI 默认反编译引擎,功能强大。

支持输入:

  • .class, .jar, .zip

使用方式:

1)图形工具:JD-GUI

下载 JD-GUI:
https://github.com/java-decompiler/jd-gui/releases
  • 拖入 .jar,可浏览 Java 源码

  • 支持结构树

  • 支持保存全部源码 .zip

2)命令行方式

git clone https://github.com/fesh0r/fernflower
cd fernflower
java -jar fernflower.jar <input.class> <output_dir>

输出结构:

  • 源码风格接近真实 Java

  • 支持泛型、匿名内部类、try/catch 等还原

限制:

  • 对高强度混淆(如 ProGuard、Allatori)处理有限

  • 不支持 dex

逆向用途:

  • 快速还原 Java 源码;

  • 审计第三方库;

  • 查找加密解密逻辑;

  • 定位反射类加载结构;

  • 脱壳或加壳还原分析基础。

10.5 混合使用建议

任务推荐工具组合
APK 反编译jadx + jadx-gui + Android Studio
Class 分析javap -v + JD-GUI/fernflower
代码插桩javap + ASM + fernflower 还原检查
JVM 壳类分析javap + JClassLib + hex editor
Frida Hook 类找点jadx 查类路径 + 方法名 + 参数签名

10.6 小结

工具类型优势适合用在
jadxAPK/Dex 反编译器Android 逆向必备,图形好用APP 逆向、加固分析
javap官方字节码分析可读 opcode、结构信息最权威插码/脱壳/字节码修改
fernflower通用 class 反编译器输出源码完整,结构清晰JAR 审计、逆向、学习源码

jadx 是 Android DEX 的眼睛,javap 是 JVM 字节码的放大镜,fernflower 是 Java 源码的还原器,三者结合可还原几乎所有 Java 字节码行为与结构。

http://www.dtcms.com/a/262747.html

相关文章:

  • 汇编基础介绍——ARMv8指令集(四)
  • 【c/c++1】数据类型/指针/结构体,static/extern/makefile/文件
  • 【c/c++3】类和对象,vector容器,类继承和多态,systemd,stdboost
  • Ragflow本地部署和基于知识库的智能问答测试
  • 机器学习在智能电网中的应用:负荷预测与能源管理
  • 【鸿蒙中级】
  • 面试复盘6.0
  • 「Java案例」输出24个希腊字母
  • 深入理解 Dubbo 负载均衡:原理、源码与实践
  • Redis Cluster Gossip 协议
  • 指针篇(6)- sizeof和strlen,数组和指针笔试题
  • 免费SSL证书一键申请与自动续期
  • MySQL-复合查询
  • 暴力风扇方案介绍
  • AlpineLinux安装部署MariaDB
  • 微信小程序接入腾讯云短信验证码流程
  • 用户行为序列建模(篇十)-【加州大学圣地亚哥分校】SASRec
  • 在Linux系统中部署Java项目
  • Unity Catalog 三大升级:Data+AI 时代的统一治理再进化
  • Re:从0开始的 空闲磁盘块管理(考研向)
  • HybridCLR热更新实例项目及改造流程
  • 人工智能之数学基础:如何判断正定矩阵和负定矩阵?
  • JVM基础--JVM的组成
  • Transformer超详细全解!含代码实战
  • Java面试宝典:基础三
  • 新生代潜力股刘小北:演艺路上的璀璨新星
  • 用户行为序列建模(篇七)-【阿里】DIN
  • Linux下基于C++11的socket网络编程(基础)个人总结版
  • 学习日志02 ETF 基础数据可视化分析与简易管理系统
  • BERT 模型详解:结构、原理解析