轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

    • 核心

    • 并发

    • 经验

    • JVM

      • JVM体系
      • 深入理解JIT编译器
      • JVMTI介绍
        • 一、JVMTI概述
          • 1、JVMTI的发展历史
          • 2、JVMTI的架构特点
        • 二、JVMTI功能
          • 1、线程管理
          • 2、堆内存分析
          • 3、类加载器跟踪
          • 4、字节码插装
          • 5、事件通知
          • 6、性能监控
          • 7、异常跟踪
          • 8、垃圾回收管理
        • 三、Agent加载方式
          • 1、启动时加载(Command-Line)
          • 2、运行时加载(Attach)
        • 四、使用JVMTI的示例
          • 1、编写一个简单的JVMTI Agent
          • 2、使用JVMTI获取线程信息
          • 3、内存分配监控Agent
          • 4、方法执行时间监控
        • 五、常见的JVMTI工具
          • 1、VisualVM
          • 2、YourKit Java Profiler
          • 3、JProfiler
          • 4、Java Flight Recorder (JFR)
          • 5、Async-profiler
          • 6、BTrace
        • 六、与Instrument API的关系
          • 1、vs Instrumentation API对比
          • 2、两者的关系
        • 七、JVMTI最佳实践
          • 1、性能优化建议
          • 2、内存管理
          • 3、线程安全
          • 4、错误处理
          • 5、调试技巧
        • 八、总结
          • 1、核心价值
          • 2、适用场景
          • 3、学习建议
      • 从Hotspot到GraalVM
    • 企业应用

  • Spring

  • 其他语言

  • 工具

  • 后端
  • Java
  • JVM
轩辕李
2023-05-03
目录

JVMTI介绍

JVMTI(Java Virtual Machine Tool Interface)是Java虚拟机(JVM)提供的一组用于构建Java虚拟机工具的接口。

它允许开发人员实现诸如调试器、分析器和监视工具等功能。

JVMTI为JVM与工具之间提供了一个标准化的通信接口。

# 一、JVMTI概述

JVMTI是JVM提供的本地编程接口,它定义了JVM与开发工具之间交互的标准方式。作为Java平台调试体系(Java Platform Debugger Architecture,JPDA)的重要组成部分,JVMTI提供了对JVM内部状态的访问和控制能力。

# 1、JVMTI的发展历史

  • JDK 1.1 - JDK 1.4:使用JVMPI(Java Virtual Machine Profiler Interface)和JVMDI(Java Virtual Machine Debug Interface)
  • JDK 5.0:引入JVMTI,统一了JVMPI和JVMDI的功能
  • JDK 6.0+:不断增强JVMTI功能,添加了更多的事件类型和能力

# 2、JVMTI的架构特点

  1. 基于事件驱动:JVMTI采用事件驱动模型,Agent可以注册感兴趣的事件并提供回调函数
  2. 原生接口:JVMTI是C/C++接口,需要通过JNI(Java Native Interface)与Java代码交互
  3. Agent模式:JVMTI功能通过Agent(代理)实现,Agent可以在JVM启动时或运行时加载
  4. 线程安全:JVMTI的大部分函数都是线程安全的,可以在多线程环境下使用

# 二、JVMTI功能

# 1、线程管理

JVMTI提供了全面的线程管理能力:

  • 线程信息获取:获取线程名、线程组、线程状态、线程优先级等信息
  • 线程控制:暂停(SuspendThread)、恢复(ResumeThread)、中断线程
  • 线程监控:监控线程创建、销毁、状态变化等事件
  • 死锁检测:通过GetOwnedMonitorInfo和GetCurrentContendedMonitor检测死锁

# 2、堆内存分析

JVMTI提供强大的堆内存分析功能:

  • 堆遍历:通过IterateOverHeap遍历堆中的所有对象
  • 对象标记:使用SetTag和GetTag为对象添加标记
  • 引用跟踪:通过FollowReferences跟踪对象引用链
  • 内存分配监控:监控对象分配事件,帮助发现内存泄漏
  • 堆快照:生成堆转储文件用于离线分析

# 3、类加载器跟踪

JVMTI可以全面监控类加载过程:

  • 类加载事件:监控类的加载、准备、解析和初始化
  • 类卸载事件:跟踪类的卸载过程
  • 类加载器信息:获取类加载器的层次结构和加载的类列表
  • 类重定义:支持运行时重新定义已加载的类(热部署)

# 4、字节码插装

JVMTI的字节码插装功能非常强大:

  • 类文件转换:在类加载前修改字节码
  • 方法重定义:运行时替换方法实现
  • 动态代码注入:插入监控代码、性能计数器
  • AOP支持:实现面向切面编程
  • 代码覆盖率:插入探针以统计代码执行情况

# 5、事件通知

JVMTI提供丰富的事件通知机制:

  • 生命周期事件:VMInit、VMDeath、VMStart
  • 线程事件:ThreadStart、ThreadEnd、MonitorWait、MonitorWaited
  • 类事件:ClassFileLoadHook、ClassLoad、ClassPrepare
  • 方法事件:MethodEntry、MethodExit、FramePop
  • 异常事件:Exception、ExceptionCatch
  • 字段访问事件:FieldAccess、FieldModification
  • 断点事件:Breakpoint、SingleStep

# 6、性能监控

JVMTI提供详细的性能监控接口:

  • CPU分析:通过方法进入/退出事件进行CPU采样
  • 内存分析:监控对象分配、垃圾回收活动
  • 锁竞争分析:监控监视器等待和竞争情况
  • 方法执行时间:精确测量方法执行时间
  • 线程CPU时间:获取线程级别的CPU使用时间

# 7、异常跟踪

JVMTI可以全面追踪异常:

  • 异常抛出监控:捕获所有异常抛出事件
  • 异常捕获监控:跟踪异常被捕获的位置
  • 异常链分析:完整的异常传播链路
  • 未捕获异常:特别关注未被捕获的异常
  • 异常统计:统计异常类型和频率

# 8、垃圾回收管理

JVMTI支持深入的GC监控和管理:

  • GC事件通知:GarbageCollectionStart、GarbageCollectionFinish
  • 强制GC:通过ForceGarbageCollection触发垃圾回收
  • 对象存活监控:跟踪对象在GC后的存活情况
  • GC统计信息:收集GC次数、耗时、回收内存量等
  • 内存池监控:监控各个内存池的使用情况

# 三、Agent加载方式

JVMTI Agent有两种加载方式:

# 1、启动时加载(Command-Line)

在JVM启动时通过命令行参数加载Agent:

# 使用 -agentpath 加载本地库
java -agentpath:/path/to/agent.so[=options] MainClass

# 使用 -agentlib 加载标准路径下的库
java -agentlib:agent[=options] MainClass

# 使用 -javaagent 加载Java Agent(基于Instrumentation API)
java -javaagent:agent.jar[=options] MainClass

# 2、运行时加载(Attach)

通过Attach API在JVM运行时动态加载Agent:

import com.sun.tools.attach.VirtualMachine;

String pid = "12345"; // 目标JVM进程ID
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgentPath("/path/to/agent.so", "options");
vm.detach();

# 四、使用JVMTI的示例

# 1、编写一个简单的JVMTI Agent

我们将创建一个简单的JVMTI Agent,用于打印JVM加载的所有类名。首先,我们需要创建一个名为jvmti_agent.c的C文件:

#include <jvmti.h>
#include <stdio.h>

// JVM加载类时的回调函数
static void JNICALL
ClassFileLoadHook(jvmtiEnv *jvmti, JNIEnv *jni_env, jclass class_being_redefined,
                  jobject loader, const char *name, jobject protection_domain,
                  jint class_data_len, const unsigned char *class_data,
                  jint *new_class_data_len, unsigned char **new_class_data) {
    printf("Loaded class: %s\n", name);
}

JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) {
    jvmtiEnv *jvmti;
    jvmtiCapabilities capabilities;
    jvmtiEventCallbacks callbacks;

    // 获取JVMTI环境
    (*jvm)->GetEnv(jvm, (void **)&jvmti, JVMTI_VERSION_1_0);

    // 初始化capabilities并启用ClassFileLoadHook事件
    memset(&capabilities, 0, sizeof(capabilities));
    capabilities.can_generate_all_class_hook_events = 1;
    (*jvmti)->AddCapabilities(jvmti, &capabilities);

    // 注册回调函数
    memset(&callbacks, 0, sizeof(callbacks));
    callbacks.ClassFileLoadHook = &ClassFileLoadHook;
    (*jvmti)->SetEventCallbacks(jvmti, &callbacks, (jint)sizeof(callbacks));

    // 启用ClassFileLoadHook事件
    (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL);

    return JNI_OK;
}

接下来,我们需要使用gcc或其他C编译器将此代码编译为共享库(例如,在Linux上为.so文件,在Windows上为.dll文件)。

gcc -shared -o libjvmti_agent.so jvmti_agent.c -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -fPIC

现在,我们可以使用-agentpath参数运行Java应用程序,并加载我们的JVMTI Agent:

java -agentpath:/path/to/libjvmti_agent.so MyJavaApp

运行此命令后,JVM将加载我们的JVMTI Agent,并在加载每个类时打印类名。这只是一个简单的示例,实际上JVMTI Agent可以执行许多高级任务,如性能分析、调试、监控等。

# 2、使用JVMTI获取线程信息

接下来,我们将创建一个JVMTI Agent,用于获取当前运行的Java线程信息。首先,我们需要创建一个名为jvmti_threads.c的C文件:

#include <jvmti.h>
#include <stdio.h>

// 获取并打印线程信息的函数
static void JNICALL
list_threads(jvmtiEnv *jvmti) {
    jthread *threads;
    jint thread_count;
    jvmtiError err;

    // 获取所有线程
    err = (*jvmti)->GetAllThreads(jvmti, &thread_count, &threads);
    if (err != JVMTI_ERROR_NONE) {
        printf("ERROR: Unable to get all threads\n");
        return;
    }

    printf("Total threads: %d\n", thread_count);

    // 遍历所有线程并打印信息
    for (int i = 0; i < thread_count; i++) {
        jvmtiThreadInfo thread_info;
        err = (*jvmti)->GetThreadInfo(jvmti, threads[i], &thread_info);
        if (err == JVMTI_ERROR_NONE) {
            printf("Thread %d: %s\n", i, thread_info.name);
            (*jvmti)->Deallocate(jvmti, (unsigned char *)thread_info.name);
        } else {
            printf("ERROR: Unable to get thread info\n");
        }
    }

    // 释放线程数组
    (*jvmti)->Deallocate(jvmti, (unsigned char *)threads);
}

JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) {
    jvmtiEnv *jvmti;

    // 获取JVMTI环境
    (*jvm)->GetEnv(jvm, (void **)&jvmti, JVMTI_VERSION_1_0);

    // 获取并打印线程信息
    list_threads(jvmti);

    return JNI_OK;
}

与上一个示例类似,我们需要使用C编译器将此代码编译为共享库:

gcc -shared -o libjvmti_threads.so jvmti_threads.c -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -fPIC

现在,我们可以使用-agentpath参数运行Java应用程序,并加载我们的JVMTI Agent:

java -agentpath:/path/to/libjvmti_threads.so MyJavaApp

运行此命令后,JVM将加载我们的JVMTI Agent,并在启动时打印所有当前运行的Java线程信息。

# 3、内存分配监控Agent

这个示例展示如何监控对象分配:

#include <jvmti.h>
#include <stdio.h>
#include <string.h>

static jlong total_allocated_bytes = 0;
static jint allocation_count = 0;

// 对象分配回调
static void JNICALL
VMObjectAlloc(jvmtiEnv *jvmti, JNIEnv *jni_env, jthread thread,
              jobject object, jclass object_klass, jlong size) {
    char *class_name;
    jvmtiError err;
    
    // 获取类名
    err = (*jvmti)->GetClassSignature(jvmti, object_klass, &class_name, NULL);
    if (err == JVMTI_ERROR_NONE) {
        total_allocated_bytes += size;
        allocation_count++;
        
        // 只打印大于1KB的对象分配
        if (size > 1024) {
            printf("Allocated: %s, size: %lld bytes\n", class_name, size);
        }
        
        (*jvmti)->Deallocate(jvmti, (unsigned char *)class_name);
    }
}

// Agent卸载时打印统计信息
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm) {
    printf("\n=== Memory Allocation Statistics ===\n");
    printf("Total allocations: %d\n", allocation_count);
    printf("Total allocated bytes: %lld\n", total_allocated_bytes);
}

JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) {
    jvmtiEnv *jvmti;
    jvmtiCapabilities capabilities;
    jvmtiEventCallbacks callbacks;
    
    // 获取JVMTI环境
    (*jvm)->GetEnv(jvm, (void **)&jvmti, JVMTI_VERSION_1_0);
    
    // 设置所需能力
    memset(&capabilities, 0, sizeof(capabilities));
    capabilities.can_generate_vm_object_alloc_events = 1;
    (*jvmti)->AddCapabilities(jvmti, &capabilities);
    
    // 注册回调
    memset(&callbacks, 0, sizeof(callbacks));
    callbacks.VMObjectAlloc = &VMObjectAlloc;
    (*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks));
    
    // 启用对象分配事件
    (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, 
                                      JVMTI_EVENT_VM_OBJECT_ALLOC, NULL);
    
    return JNI_OK;
}

# 4、方法执行时间监控

监控方法的执行时间:

#include <jvmti.h>
#include <stdio.h>
#include <time.h>

typedef struct {
    char *method_name;
    char *class_name;
    clock_t start_time;
} MethodInfo;

// 方法进入回调
static void JNICALL
MethodEntry(jvmtiEnv *jvmti, JNIEnv *jni_env, jthread thread, jmethodID method) {
    jclass declaring_class;
    char *method_name, *class_name;
    MethodInfo *info;
    
    // 获取方法信息
    (*jvmti)->GetMethodDeclaringClass(jvmti, method, &declaring_class);
    (*jvmti)->GetMethodName(jvmti, method, &method_name, NULL, NULL);
    (*jvmti)->GetClassSignature(jvmti, declaring_class, &class_name, NULL);
    
    // 记录开始时间
    info = (MethodInfo *)malloc(sizeof(MethodInfo));
    info->method_name = method_name;
    info->class_name = class_name;
    info->start_time = clock();
    
    // 将信息存储在线程本地存储中
    (*jvmti)->SetThreadLocalStorage(jvmti, thread, info);
}

// 方法退出回调
static void JNICALL
MethodExit(jvmtiEnv *jvmti, JNIEnv *jni_env, jthread thread, jmethodID method,
           jboolean was_popped_by_exception, jvalue return_value) {
    MethodInfo *info;
    clock_t end_time;
    double elapsed_time;
    
    // 获取存储的方法信息
    (*jvmti)->GetThreadLocalStorage(jvmti, thread, (void **)&info);
    if (info != NULL) {
        end_time = clock();
        elapsed_time = ((double)(end_time - info->start_time)) / CLOCKS_PER_SEC * 1000;
        
        // 只打印执行时间超过10ms的方法
        if (elapsed_time > 10) {
            printf("Method %s.%s took %.2f ms\n", 
                   info->class_name, info->method_name, elapsed_time);
        }
        
        // 清理内存
        (*jvmti)->Deallocate(jvmti, (unsigned char *)info->method_name);
        (*jvmti)->Deallocate(jvmti, (unsigned char *)info->class_name);
        free(info);
        (*jvmti)->SetThreadLocalStorage(jvmti, thread, NULL);
    }
}

# 五、常见的JVMTI工具

许多常见的Java性能和调试工具都使用了JVMTI接口。以下是一些广泛使用的JVMTI工具:

# 1、VisualVM

VisualVM是一个用于监视、分析和调试Java应用程序的工具。它提供了对运行中的Java虚拟机的实时信息,包括线程、内存、类加载器、垃圾回收等。VisualVM还包含了一些用于性能分析的功能,如CPU和内存分析器,以及用于调试的功能,如线程和监视器跟踪。VisualVM使用JVMTI接口与JVM进行通信。

主要功能:

  • 实时监控CPU、内存、线程
  • 堆转储分析
  • 线程转储分析
  • 性能剖析(CPU和内存)
  • 远程监控支持

# 2、YourKit Java Profiler

YourKit Java Profiler是一个强大的性能分析和调试工具,用于分析Java应用程序的CPU、内存、线程等方面的性能。YourKit Java Profiler使用JVMTI接口与JVM进行通信,以收集详细的性能数据。它还提供了一个易于使用的图形界面,用于查看和分析收集到的数据。

主要功能:

  • CPU性能分析(采样和跟踪)
  • 内存分析和泄漏检测
  • 线程分析和死锁检测
  • 异常分析
  • 数据库查询分析

# 3、JProfiler

JProfiler是另一个广泛使用的Java性能分析和调试工具。它提供了对Java应用程序的实时性能监控和分析,包括CPU、内存、线程等。JProfiler使用JVMTI接口与JVM进行通信,并提供了一个易于使用的图形界面,用于查看和分析收集到的数据。

主要功能:

  • 方法级别的CPU分析
  • 内存分配跟踪
  • 堆遍历和对象引用图
  • JDBC/JPA分析
  • 集成IDE支持

# 4、Java Flight Recorder (JFR)

Java Flight Recorder(JFR)是一个内置于Java虚拟机的诊断和分析工具,可用于收集应用程序和JVM的详细性能数据。JFR使用JVMTI接口与JVM进行通信,并生成详细的事件记录文件,可用于离线分析。这些事件记录文件可以使用JDK内置的Java Mission Control(JMC)工具进行分析。

主要功能:

  • 低开销的生产环境监控
  • 详细的事件记录
  • 自定义事件支持
  • 与JMC深度集成
  • 连续记录模式

# 5、Async-profiler

Async-profiler是一个低开销的Java性能分析工具,特别适合生产环境使用:

主要功能:

  • CPU采样(支持SafePoint偏差修正)
  • 内存分配分析
  • 锁竞争分析
  • 火焰图生成
  • 支持Linux和macOS

# 6、BTrace

BTrace是一个安全的动态跟踪工具,允许在运行时向Java应用程序注入跟踪代码:

主要功能:

  • 运行时代码注入
  • 安全限制(防止破坏应用)
  • 丰富的注解支持
  • 与DTrace集成

# 六、与Instrument API的关系

在Java运行期动态能力中介绍到Java Agent,底层其实就是借助JVMTI来进行完成的。

从Java SE 5开始,提供了Instrumentation接口(java.lang.instrument)来编写Agent。

# 1、vs Instrumentation API对比

特性 JVMTI Instrumentation API
语言 C/C++ Java
开发难度 较高,需要处理内存管理 较低,自动内存管理
功能范围 完整的JVM控制能力 主要聚焦字节码操作
性能开销 较低 略高(JNI调用开销)
平台依赖 需要为不同平台编译 跨平台
调试能力 完整的调试支持 有限的调试能力
使用场景 性能分析工具、调试器 APM、AOP框架

# 2、两者的关系

  1. Instrumentation API是JVMTI的上层封装:

    • Instrumentation API底层通过JVMTI实现
    • 提供了更加友好的Java接口
    • 隐藏了JVMTI的复杂性
  2. 功能互补:

    • JVMTI提供底层的完整控制
    • Instrumentation API提供便捷的字节码操作
    • 可以在同一应用中同时使用
  3. 典型的使用模式:

    // Java Agent使用Instrumentation API
    public class MyAgent {
        public static void premain(String args, Instrumentation inst) {
            inst.addTransformer(new ClassFileTransformer() {
                @Override
                public byte[] transform(ClassLoader loader, String className,
                                       Class<?> classBeingRedefined,
                                       ProtectionDomain protectionDomain,
                                       byte[] classfileBuffer) {
                    // 修改字节码
                    return modifiedBytecode;
                }
            });
        }
    }
    

# 七、JVMTI最佳实践

# 1、性能优化建议

  • 选择性启用事件:只启用必要的事件,避免性能开销
  • 使用采样而非跟踪:对于高频事件,使用采样模式
  • 缓存JVMTI环境:避免重复获取JVMTI环境指针
  • 批量操作:尽可能批量处理数据,减少JNI调用

# 2、内存管理

  • 及时释放内存:使用Deallocate释放JVMTI分配的内存
  • 避免内存泄漏:正确管理Agent内部分配的内存
  • 使用内存池:对于频繁的内存分配,考虑使用内存池

# 3、线程安全

  • 使用线程本地存储:通过SetThreadLocalStorage存储线程相关数据
  • 同步访问共享数据:使用互斥锁保护共享数据
  • 避免死锁:注意JVMTI回调中的锁顺序

# 4、错误处理

// 良好的错误处理示例
jvmtiError err = (*jvmti)->GetThreadInfo(jvmti, thread, &info);
if (err != JVMTI_ERROR_NONE) {
    char *error_name;
    (*jvmti)->GetErrorName(jvmti, err, &error_name);
    fprintf(stderr, "JVMTI error: %s\n", error_name);
    (*jvmti)->Deallocate(jvmti, (unsigned char *)error_name);
    return;
}

# 5、调试技巧

  • 使用日志记录:详细记录Agent的行为
  • 增量开发:逐步添加功能,每步验证
  • 使用调试器:GDB等工具调试Native代码
  • 测试环境隔离:在独立环境中测试Agent

# 八、总结

JVMTI(Java Virtual Machine Tool Interface)是Java虚拟机提供的一组强大的本地编程接口,它为开发者提供了对JVM内部状态的深度访问和控制能力。

# 1、核心价值

  1. 全面的监控能力:从线程管理到内存分析,从类加载到垃圾回收,JVMTI提供了对JVM各个方面的监控能力
  2. 强大的调试支持:断点、单步执行、变量检查等调试功能的底层实现
  3. 性能分析基础:几乎所有的Java性能分析工具都基于JVMTI构建
  4. 运行时代码修改:支持热部署、AOP等高级特性

# 2、适用场景

  • 开发性能分析工具:CPU分析器、内存分析器
  • 构建调试器:IDE调试功能的实现
  • 应用监控系统:APM(Application Performance Management)工具
  • 代码覆盖率工具:测试覆盖率统计
  • 故障诊断工具:生产环境问题排查

# 3、学习建议

  1. 循序渐进:从简单的事件监听开始,逐步深入复杂功能
  2. 实践为主:动手编写简单的JVMTI Agent,加深理解
  3. 结合Java Agent:学习Instrumentation API,理解两者的关系
  4. 关注性能:在使用JVMTI时始终考虑性能影响
  5. 参考开源项目:研究async-profiler、BTrace等优秀开源项目

通过深入理解JVMTI,你不仅能够更好地使用现有的Java工具,还能够开发出适合自己需求的定制化工具,为Java应用的开发、调试和优化提供强有力的支持。

祝你变得更强!

编辑 (opens new window)
#JVMTI
上次更新: 2025/08/15
深入理解JIT编译器
从Hotspot到GraalVM

← 深入理解JIT编译器 从Hotspot到GraalVM→

最近更新
01
AI时代的编程心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code实战之供应商切换工具
08-18
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式