JVM 基础、GC 算法与 JProfiler 监控工具详解

目录

1、引言

1.1 JVM内存与本地内存

1.2 JVM与JDK的关系

2、JVM基础

2.1 JVM(Java Virtual Machine)

2.2 Java与JVM的关系

2.3 JVM的内存结构

2.3.1 堆内存

2.3.2 栈内存

2.3.3 方法区

2.3.4 本地方法栈

2.3.5 程序计数器(PC寄存器)

2.4 JVM的位置

2.5 JVM的体系结构

2.6 JVM调优

3、类加载器及双亲委派机制

3.1 类加载器的作用

3.2 常见的类加载器

3.3 双亲委派机制

3.4 沙箱安全机制

4、Native方法与JNI(Java本地接口)

4.1 Native关键字

4.2 JNI的作用

5、程序计数器(PC寄存器)

5.1 定义

5.2 作用

6、方法区

6.1 定义

6.2 特点

7、Java栈与堆内存

7.1 栈

7.1.1数据结构

7.1.2 线程独立

7.1.3 栈溢出

7.2 堆内存

7.2.1 堆的结构

7.2.2 垃圾回收

7.2.3 堆内存溢出(OOM)

7.2.4 堆内存的调优

7.2.5 内存分配策略

8、 默认垃圾回收算法

8.1 分代垃圾回收

8.2 垃圾回收器的选择

8.3 总结

9、使用JProfiler工具分析OOM原因

9.1 OOM(OutOfMemoryError)分析

9.2 工具介绍

9.3 在IDEA中使用JProfiler

10、GC常见的算法

10.1 标记整理(标记压缩)

10.2 标记清除法

10.3 复制算法

10.4 引用计数器

10.5 GC算法总结

10.6 GC算法与分代收集

11、Java内存模型(JMM)


1、引言

1.1 JVM内存与本地内存

  • JVM内存 vs. 本地内存:

    在JVM中操作时,主要是使用JVM虚拟机的内存,这包括了堆内存(Heap)、方法区(Method Area)、栈内存(Stack)、本地方法栈(Native Method Stack)等。JVM负责管理和分配这部分内存。

    • 堆内存:用于存储对象实例,这是JVM主要使用的内存区域。
    • 栈内存:用于存储每个线程的局部变量、方法调用链等。
    • 方法区:用于存储类信息、常量、静态变量等。
    • 本地方法栈:用于调用本地(native)方法时使用的内存。

    JVM操作的这些内存都在其管理之下,虽然它运行在操作系统的本地环境中,但开发者一般不直接与操作系统的内存打交道。JVM虚拟内存是在操作系统的本地内存之上管理的。简单来说,JVM虚拟机的内存是操作系统为它分配的内存的一部分,它在此基础上进一步划分和管理。

1.2 JVM与JDK的关系

  • JVM和JDK的关系:

    当下载并安装JDK(Java Development Kit)时,JVM(Java Virtual Machine)就会自动包含在内。JVM是JDK中的一个核心组件,它负责执行Java字节码。JDK不仅包含JVM,还包括编译器(javac)、标准类库等开发工具。所以只要你安装了JDK,JVM就已经包含在你的环境中了,不需要单独下载。

2、JVM基础

2.1 JVM(Java Virtual Machine)

JVM是Java虚拟机,是运行Java程序的基础环境。它负责加载、执行Java字节码,并提供内存管理、垃圾回收等功能。Java的跨平台性正是由于JVM的存在,因为Java程序在不同操作系统上的JVM上运行,从而实现了“写一次,处处运行”的特性。

2.2 Java与JVM的关系

  • Java编译过程:Java源代码首先会通过编译器(如javac)编译成字节码(.class文件)。这个字节码并不直接运行在物理机器上,而是由JVM来解释和执行。
  • JVM的作用:JVM是Java程序运行时的核心,它接收和解释Java字节码,并将其转换为操作系统可执行的机器指令。通过JVM,Java程序可以跨平台运行,即同一份Java代码可以在不同操作系统上运行。

2.3 JVM的内存结构

官方JVM体系结构图

简易结构图

JVM内存结构分为多个区域,每个区域负责管理不同类型的数据和任务:

  • JVM垃圾介绍:
    • 堆内存中的垃圾:堆中存储所有对象实例,由于Java的自动内存管理机制,垃圾回收器(GC)会负责回收不再使用的对象,这些未使用的对象即为“垃圾”。
    • 方法区中的垃圾:虽然方法区存储类信息和常量,但在某些情况下,未被使用的类或静态数据也会被回收,因此方法区本质上也可能产生垃圾。
    • 栈内存中的数据:Java栈、本地方法栈和程序计数器中存储的是临时数据,比如局部变量、方法调用信息等。由于这些区域的生命周期与方法的执行周期相关,方法执行完毕后栈帧会自动释放,因此不会有垃圾存在。
2.3.1 堆内存
  • 堆(Heap):用于存储所有Java对象及数组。堆是垃圾回收器(GC)的主要工作区域,因为在这个区域中会产生垃圾对象。
2.3.2 栈内存
  • Java栈(Java Stack):存储局部变量、方法调用栈帧等。每个线程都有自己独立的Java栈,栈中不会存在垃圾对象,因为栈帧随方法执行结束自动释放。
2.3.3 方法区
  • 方法区(Method Area):存储已加载的类信息、常量、静态变量、即将编译的代码等。方法区本质上是堆内存的一部分,但用于特定用途。
2.3.4 本地方法栈
  • 本地方法栈(Native Method Stack):专门用于调用本地方法(使用非Java语言编写的方法,如C或C++),类似于Java栈。
2.3.5 程序计数器(PC寄存器)
  • 程序计数器(PC寄存器):存储当前线程执行的字节码的地址(程序指令),用于跟踪线程的执行进度。

2.4 JVM的位置

JVM运行在操作系统之上,充当了Java程序和底层操作系统之间的中间层。JVM实际上是一个软件,其主要实现通常是使用C语言编写的,部分实现还可能使用其他低级语言(如汇编)来优化性能。JVM负责屏蔽不同操作系统的细节,使Java程序能够在多种平台上运行。

2.5 JVM的体系结构

JVM的体系结构包括多个组成部分,主要分为以下几个模块:

  • 类加载器系统(Class Loader Subsystem):负责加载.class文件,验证、准备、解析和初始化类。
  • 运行时数据区(Runtime Data Area):包括堆、栈、方法区等,用于管理程序执行时的各种数据。
  • 执行引擎(Execution Engine):负责解释执行字节码或将字节码编译为本地机器码。
  • 本地方法接口(Native Interface):用于调用非Java编写的本地方法。
  • 垃圾回收系统(Garbage Collection System):自动管理堆中的对象回收。

2.6 JVM调优

JVM的性能优化通常主要针对堆和方法区进行。因为这两个区域负责管理Java对象、类信息和静态数据,并且会产生垃圾。通过调整堆的大小、垃圾回收策略、类加载和卸载等,可以提高应用的性能和内存使用效率。

  • JVM调优的几个常见方面:
    • 堆大小设置:通过-Xms-Xmx参数设置堆的初始大小和最大大小。
    • 垃圾回收器选择:不同的GC算法适合不同的场景,如G1适用于大内存应用,CMS适用于低延迟应用。
    • 方法区调优:通过调整方法区的大小和类卸载策略,防止内存溢出或类加载过多导致的性能问题。

3、类加载器及双亲委派机制

  • 类加载器及双亲委派机制:确保类加载的安全性和有效性,通过双亲委派避免核心类被恶意替换。

3.1 类加载器的作用

类加载器(ClassLoader)负责将.class文件加载到JVM中并转化为内存中的Class对象。具体来说,当你创建一个对象(如new Student())时,类加载器会加载该类的字节码文件,而引用对象存放在栈中,实际的对象数据存储在堆内存中。

3.2 常见的类加载器

JVM中存在多种类加载器,它们按层次结构组织,负责加载不同范围的类:

  1. 虚拟机自带的加载器:由JVM内部实现,用于加载JDK核心类。
  2. 启动类加载器(Bootstrap ClassLoader):又称为根类加载器,负责加载JVM核心库,通常是$JAVA_HOME/lib中的类,如rt.jar
  3. 扩展类加载器(Extension ClassLoader):用于加载扩展库,通常是$JAVA_HOME/lib/ext目录下的类。
  4. 应用程序类加载器(Application ClassLoader):负责加载用户类路径中的类,通常加载classpath中的类,是默认的类加载器。

3.3 双亲委派机制

双亲委派机制是一种类加载器的工作方式,用来避免类重复加载并保证Java核心类的安全性。

双亲委派机制的执行过程如下:

  1. 类加载请求:当某个类加载器接收到一个类的加载请求时,它不会立即尝试加载,而是将请求委派给它的父类加载器去处理。
  2. 逐级向上委派:每个类加载器都会将请求向上委托给父类加载器,直到最顶层的启动类加载器(Bootstrap ClassLoader)。
  3. 启动类加载器检查:启动类加载器检查是否能够加载该类,如果能,则加载并返回;否则将请求传递给子类加载器。
  4. 子类加载器处理:如果启动类加载器无法加载,才由子类加载器尝试加载。

注意:双亲委派机制的一个常见问题是如果我们定义了一个与JDK核心类(如java.lang.String)同名的类,它将永远不会被加载。因为启动类加载器会优先加载java.lang.String类,导致我们的自定义类无法加载,进而报错,如找不到main方法。

通过obj.getClass().getClassLoader()方法可以获取对象的类加载器,利用反射可以深入了解类加载器的工作机制。

3.4 沙箱安全机制

沙箱机制是一种保护机制,限制程序运行时的系统资源访问,防止恶意代码执行或访问敏感资源。类加载器在沙箱机制中扮演了重要角色,特别是采用了双亲委派机制,防止用户定义的恶意类替代核心类。

沙箱安全机制的组成:

  • 字节码校验器:确保字节码的正确性和安全性。
  • 类加载器:采用双亲委派机制防止类加载冲突。
  • 存取控制器:控制程序对文件、网络等系统资源的访问。
  • 安全管理器:提供更细粒度的安全策略控制。
  • 安全软件包:实现Java平台的安全功能,如加密、认证等。

4、Native方法与JNI(Java本地接口)

  • Native方法与JNI:允许Java调用底层代码,通过JNI扩展Java的使用范围。

4.1 Native关键字

native关键字用于声明本地方法,即用非Java语言(如C/C++)实现的方法。这些方法是因为Java无法直接实现某些底层操作,必须调用操作系统或底层库来完成。native方法调用时,会进入本地方法栈,并通过JNI(Java Native Interface,Java本地接口)与底层代码交互。

4.2 JNI的作用

JNI是Java与其他语言(如C/C++)进行交互的桥梁,主要作用是扩展Java的能力,让Java可以调用非Java的底层代码,尤其是在需要高性能或者与系统底层紧密相关的场景下(如硬件交互、系统调用等)。

5、程序计数器(PC寄存器)

  • PC寄存器:用于线程的字节码指令跟踪,是实现多线程的关键。

5.1 定义

程序计数器(Program Counter Register)是一个非常小的内存区域,每个线程都有一个独立的PC寄存器。它用来存储当前线程正在执行的字节码指令地址,指向下一条即将执行的指令。由于Java是多线程的,PC寄存器是线程私有的。

5.2 作用

程序计数器用于跟踪线程的执行进度,线程切换时通过它恢复到正确的执行位置,因此它是实现Java多线程的关键组件之一。

6、方法区

  • 方法区:用于存储类信息、静态变量、常量等,是JVM内存结构的重要组成部分。

6.1 定义

方法区(Method Area)是JVM内存的一个逻辑区域,它由所有线程共享,主要用于存储:

  • 已加载的类信息(类名、方法、字段等)
  • 运行时常量池(存储编译器生成的常量)
  • 静态变量
  • 类的字节码和方法(包括构造方法、接口定义等)

6.2 特点

  • 共享区域:方法区是线程共享的内存空间,不像栈和PC寄存器那样是线程私有的。
  • 存储内容:主要存储类的元数据、静态变量、常量池等,而对象实例是存储在堆中的。
  • 垃圾回收:尽管方法区存储静态信息,但在某些情况下(如类卸载)也会触发垃圾回收。

7、Java栈与堆内存

7.1 栈

7.1.1数据结构
  • 数据结构:栈是一种后进先出(LIFO,Last In First Out)的数据结构,只有栈顶的元素可以被访问和修改。
7.1.2 线程独立
  • 线程独立:每个线程都有自己的栈,栈的生命周期与线程相同。
  • 栈内存内容:
    • 八大基本数据类型(如intchar等)
    • 对象引用:指向堆中实际对象的引用
    • 方法调用:每当一个方法被调用时,相关信息(如局部变量、参数等)会存放在栈中。
7.1.3 栈溢出
  • 栈溢出

    栈溢出(Stack Overflow)通常发生在递归调用中,如果方法循环调用而没有终止条件,最终会耗尽栈空间,导致溢出错误。

7.2 堆内存

  • 堆内存

    • 单一堆:一个JVM实例只有一个堆内存,所有线程共享这个堆。
    • 堆内存的调节:可以通过JVM参数进行配置,通常会调整堆的初始大小和最大大小。
  • JVM实现

    三种常见的JVM实现:

    1. Sun公司的HotSpot
    2. BEA的JRockit
    3. IBM的J9 VM

    注意:从JDK 1.8开始,永久代(PermGen)被移除,改为使用元空间(Metaspace)来存储类的元数据。

7.2.1 堆的结构
  • 堆的结构

    堆内存通常被细分为几个区域:

    1. 新生区(Eden Space):
      • 对象在此区域被创建(使用new关键字)。
      • 如果新生区满了,会触发轻量级垃圾回收(轻GC)。
    2. 幸存者区(Survivor Space,分为S0和S1):
      • 新生区经过一次GC后存活的对象会被转移到这里。经过几次GC后仍然存活的对象可能被转移到老年代。
    3. 老年区(Old Generation):
      • 存放长时间存活的对象,通常是经历过多次GC的对象。
    4. 元空间(Metaspace):
      • 存放类的元数据,从JDK 1.8开始取代永久代。元空间使用本地内存,物理上不存在限制。
7.2.2 垃圾回收
  • 垃圾回收
    • 轻量级垃圾回收(轻GC):主要针对新生区的对象。
    • 重量级垃圾回收(重GC):通常针对老年区的对象,处理复杂和耗时。
7.2.3 堆内存溢出(OOM)
  • 堆内存溢出(OOM)
    • 当堆内存不足以满足对象创建需求时,会抛出OutOfMemoryError(OOM)。

    • 常见原因包括创建过多的对象,尤其是字符串等长生命周期的对象。

    • 图:

  • 字符串处理
    • 在Java中,字符串的长度是动态的,但受限于可用内存。大量字符串或过长字符串会导致堆内存溢出。
7.2.4 堆内存的调优
  • 堆内存的调优
    • 新生区调优:设置合适的大小以优化轻GC的频率和效率。
    • 老年区调优:通过调整老年区大小来减少重GC的次数
7.2.5 内存分配策略
  • JVM默认情况下分配的总内存通常是计算机内存的1/4,而初始化内存是1/64。可以通过命令行参数调整这些值。
  • 默认分配值(idea代码)
        public static void main(String[] args) {
            
            long max = Runtime.getRuntime().maxMemory();
            long total = Runtime.getRuntime().totalMemory();
    
            System.out.println("max" + max + "字节\t" + (max / (double) 1024 / 1024) + "MB");
            System.out.println("total" + total + "字节\t" + (total / (double) 1024 / 1024) + "MB");
        }
  • IDEA中进行JVM调参
  • 参数调优:JVM调优就是在这边地方调优。(调优区别:原本241MB内存大小,调成了981MB)
  • 参考:原始参数,下图是调优后的参数。(避免堆内存OOM错误)

    调优参数: -Xms1024m -Xmx1024m -XX:+PrintGCDetails

  • 控制台中:新生区(伊甸园)、老年区、元空间        
  • 总结
    • 栈用于存储方法调用和局部变量,每个线程都有独立的栈。
    • 堆用于存储对象,所有线程共享一个堆,内存分为新生区、幸存者区、老年区和元空间。
    • 垃圾回收的策略影响对象的生存周期与内存使用效率。
    • 堆内存的调优对于大型应用性能至关重要,合理配置内存可以有效防止OOM错误。

在 Java 中,垃圾回收器(GC,Garbage Collector)主要负责自动管理内存的分配和释放。Java 虚拟机(JVM)中的垃圾回收机制是基于 分代回收 理念的,这种机制认为对象的生命周期是不一样的,因此可以根据对象存活的时间长短采用不同的回收策略。

8、 默认垃圾回收算法

Java 的垃圾回收器默认使用 HotSpot VM,它采用的垃圾回收算法是 分代垃圾回收算法(Generational Garbage Collection),结合了多种不同的垃圾回收算法,具体包括:

  • 新生代回收算法:复制算法(Copying Algorithm)
  • 老年代回收算法:标记-清除算法(Mark-Sweep Algorithm)和 标记-整理算法(Mark-Compact Algorithm)

8.1 分代垃圾回收

  • 新生代:大多数新创建的对象会被分配在新生代。新生代中的对象大部分都是“朝生夕死”的,因此新生代的垃圾回收主要使用 复制算法,将存活对象从一个区域复制到另一个区域,非存活对象则直接被清理。

    • Eden 区:新对象首先分配在 Eden 区。
    • Survivor 区:对象在 Eden 区中经过一次垃圾回收后,如果没有被清理,会被移动到 Survivor 区。

    算法: 新生代中主要使用 复制算法(Copying Algorithm),它将存活的对象复制到另一个区域,而非存活的对象则会被回收。这种方法回收速度很快,因为它不需要遍历所有对象,只需处理存活的对象。

  • 老年代:新生代中经过多次垃圾回收后仍然存活的对象会被晋升到老年代。老年代中的对象通常存活时间较长,因此回收频率较低。

    算法:

    • 标记-清除算法(Mark-Sweep Algorithm):先标记出所有存活的对象,然后清理掉未标记的对象。缺点是容易产生内存碎片。
    • 标记-整理算法(Mark-Compact Algorithm):在标记完存活对象后,不仅清理掉未存活的对象,还会将存活对象整理到一起,避免内存碎片问题。

8.2 垃圾回收器的选择

虽然 Java 默认使用的是分代回收机制,但具体使用的垃圾回收器可以根据 JVM 的配置不同而变化,以下是常见的几种垃圾回收器:

  • Serial GC:一个简单的单线程垃圾回收器,适合单核 CPU 和小型应用。
  • Parallel GC:多线程垃圾回收器,适合多核 CPU 和注重吞吐量的应用。
  • G1 GC:一种面向服务端应用的垃圾回收器,旨在减少 GC 造成的停顿时间,替代了老年代的 CMS(Concurrent Mark-Sweep)回收器。
  • ZGC 和 Shenandoah GC:专注于极低停顿时间的垃圾回收器,适用于超低延迟应用。

8.3 总结

Java 默认采用的是基于 分代垃圾回收算法 的策略,其中新生代主要使用 复制算法,而老年代结合 标记-清除 和 标记-整理 算法。这种分代回收机制优化了内存管理,使得不同生命周期的对象能被高效回收。

9、使用JProfiler工具分析OOM原因

9.1 OOM(OutOfMemoryError)分析

  • OOM(OutOfMemoryError)分析
    • 内存快照分析:使用内存快照工具(如JProfiler、MAT)可以方便地查看在OOM发生时的内存状态,定位具体出错的代码行。
    • 代码行定位:通过内存快照分析,可以快速找到导致内存泄漏或堆溢出的代码位置,避免逐行调试的麻烦。

9.2 工具介绍

  • 工具介绍
    • MAT(Memory Analyzer Tool):用于分析Java堆转储文件,帮助定位内存泄漏、获取堆中对象的数据、找到占用内存大的对象等。
    • JProfiler:强大的性能分析工具,能够监控应用的内存使用情况,生成内存快照并分析内存泄漏。(实时监控)

9.3 在IDEA中使用JProfiler

  1. 插件安装:在IDEA中安装JProfiler插件,可以集成性能分析和监控功能。
  2. JProfiler官网上下载
  3. 编写OOM异常代码
    • 在IDEA中编写可能导致OOM的代码,利用JProfiler监控具体行数:
      public class OOMExample {
          public static void main(String[] args) {
              List<String> list = new ArrayList<>();
              while (true) {
                  list.add("This is a very long string that will keep consuming memory...");
              }
          }
      }
  4. 调参
    1. -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

    • -Xms:设置JVM的初始内存分配大小,建议设置为总内存的1/64(例如:-Xms512m)。

    • -Xmx:设置JVM的最大内存分配大小,建议设置为总内存的1/4(例如:-Xmx2048m)。

    • -XX:+PrintGCDetails:打印垃圾回收的详细信息,帮助分析内存使用情况和垃圾回收的效果。

  5. 监控软件使用
    • 配置
      • -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=C:\Users\Sachsen\Desktop\testKZ -Xms512m -Xmx512m 
    • 代码
          public static void main(String[] args) {
      
              String str = "zhangSan";
              while (true) {
                  str += str + new Random().nextInt(1888888888) + new Random().nextInt(999999999);
              }
          }
    • java.lang.OutOfMemoryError 
    • 配置的文件目录下生成快照
    • 查看当前对象集
    • 查看栈溢出的对象(第一种方式)Current Object Set对象集传递引用查看
    • 查看栈溢出的对象(第二种方式)Current Object Set对象集图像结构查看
    • 查看栈溢出的对象(查看的第三种方式)Thread Dump线程转储查看 
      • 线程转储记录了快照时的所有线程,可以查看线程运行状态(运行、等待、阻塞等)和线程调用栈(展现线程执行的方法调用路径)。运行出现错误的线程会用一个特殊的图标标记。

  • 总结
    • 使用JProfiler和MAT等工具可以快速定位和分析OOM的原因,减少线下调试的时间。
    • 合理配置JVM内存参数有助于防止内存溢出。
    • 对于GC的了解能够帮助优化应用的内存使用和性能。

10、GC常见的算法

10.1 标记整理(标记压缩)

  • 概念:首先标记活着的对象,然后将它们整理到内存的一端,释放出连续的空闲空间。
  • 优点:减少了内存碎片,使内存使用更高效。
  • 缺点:需要额外的时间来移动对象。

10.2 标记清除法

  • 概念:标记所有活着的对象,然后直接清除未标记的对象。
  • 优点:实现简单,不需要额外空间。
  • 缺点:存在内存碎片,可能导致内存浪费。

10.3 复制算法

  • 概念:将存活的对象从Eden区复制到两个幸存者区中的一个(To区),当进行垃圾回收时,清空Eden区。
  • 工作过程:
    1. Eden区:每次GC时,将Eden区的存活对象复制到幸存者区的To区。
    2. 养老区:对象经历一定次数的GC(默认15次)后,若仍存活,则被转移到老年区。
  • 优点:内存利用率高,适合存活时间较短的对象。
  • 缺点:Eden和幸存者区可能会产生空闲区域,浪费内存。

10.4 引用计数器

  • 概念:为每个对象维护一个计数器,记录引用该对象的数量。当计数器为0时,自动回收对象。
  • 优点:简单易实现。
  • 缺点:无法处理循环引用,效率低,不推荐使用。

10.5 GC算法总结

内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)

内存整齐度:复制算法=标记压缩算法>标记清除算法

内存利用率:标记压缩算法=标记清除算法>复制算法

10.6 GC算法与分代收集

  • GC算法:通过不同的策略管理内存,优化性能与资源利用。
  • GC算法也称为分代收集算法,因为它利用了对象生命周期的特性,将对象分为新生代(Eden和幸存者区)和老年区。

11、Java内存模型(JMM)

  • JMM:确保多线程编程中内存访问的一致性和安全性。
  • 定义:Java内存模型规定了Java程序中线程如何访问共享内存,确保线程间的可见性与一致性。
  • 主要内容:
    • 主内存与工作内存:JMM将内存划分为主内存(共享内存)和工作内存(线程的私有内存)。
    • 内存屏障:确保特定的操作顺序和内存可见性。
    • 可见性:确保一个线程对共享变量的修改能被其他线程看到。
    • 原子性:保证对共享变量的操作是不可分割的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值