JVM详解

笔记以学习周志明的《深入理解Java虚拟机》为主整理,非原创

1. JDK, JRE, JVM 的关系

JVM(Java Virtual Machine)= 类加载器子系统 + 运行时数据区 + 执行引擎,是虚拟的计算机用于在各种平台执行字节码文件。

JDK (Java Development Kit)= Java程序设计语言 + JVM + Java API类库,是用于支持Java程序开发的最小环境。

JRE(Java Runtime Environment)= JVM + Java SE API子集,是支持Java程序运行的标准环境。

具体如下图:https://docs.oracle.com/javase/7/docs

 

 

2. 运行时数据区(Runtime Data Areas)

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,其构成如下:http://java8.in/java-virtual-machine-run-time-data-areas/

JVM Structure

其中程序计数器,Java虚拟机栈,本地方法栈为“线程私有”;Java堆和方法区为“线程共享”。

 

1. 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器。在任何确定时刻,一个处理器只会执行一条线程中的指令,为了线程切换后恢复正确的执行位置,每条线程都要有独立的计数器。

 

2. Java虚拟机栈(JVM Stacks):描述的是Java方法执行的内存模型,每个方法被执行同时创建一个栈帧(上图Frame),每个方法被调用到执行完成对应栈帧在虚拟机栈中从入栈到出栈的过程。(-Xss 设置单个线程栈大小)单线程情况调用方法递归过多可能造成StackOverFlowError问题,而大量创建线程的情况会发生OutofMemoryError异常。

- 栈帧(Stack Frame):主要用于存储局部变量表(上图Local variables)+ 操作栈(上图Operand Stack)+ 动态链接(上图Current class constant pool reference)等信息。

局部变量表存放编译器可知的各种基本数据类型:boolean(1位),byte(8位,-128 to 127 ),short(16位,-32768 to 32767),char(16位,0 to 65535),int(32位,-2147483648 to 2147483647),long(64位,-9223372036854775808 to 9223372036854775807),float(32位),double(64位);对象引用(reference类型,指向对象起始地址的引用指针);returnAddress类型(指向一条字节码指令的地址)。其中long和double会占2个局部变量空间(Slot),其余占1个。

操作栈:是后入先出栈(LIFO),从局部变量表或对象实例中复制常量或变量到栈中,再将计算后的结果返回。

动态链接:引用 https://zhuanlan.zhihu.com/p/45354152

在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。

Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)

这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。

 

3. 本地方法栈(Native Method Stack):与虚拟机栈类似,区别是虚拟机栈为Java方法(字节码)服务,而本地方法栈则是为虚拟机使用到的本地方法服务。

 

4. Java堆(Heap):所有线程共享的内存区域,用于存放对象实例和数组(jdk1.8后,虚拟机默认开启子逃逸分析,如果变量A只在本方法中使用可以在栈为其分配对象,方法结束对象销毁)。其构成如下,Heap Space所表示区域为Hotspot对堆的分代实现:新生代(Eden + Suvivor0 + Survivor1)+ 老年代(Tenured)。可通过-Xmx和-Xms调整堆内存可被分配最大上限和初始值。

- 对象访问:以Hotspot虚拟机为例,对象的访问采用直接指针访问:其中每个对象都包含一个与之对应的class的信息(class信息存放在方法区,通过指针指向对象类型数据),对象实例数据储存在堆上,栈上reference类型保存的是堆上的对象地址。

 

5. 方法区(Method Area):所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。(jdk1.8后被元数据区Metaspace取代,使用物理内存,直接受本机物理内存限制)参考方法区详解几种常量池

è¿éåå¾çæè¿°

- 已加载类信息:类的全限定名(比如,java.long.String),类的直接父类的完整名称,类的直接实现接口的有序列表(因为一个类直接实现的接口可能不止一个,因此放到一个有序表中),类的修饰符(public,abstract,final等)。即类的元数据信息。

- 类型的常量池(Class Constant Pool):每个Class文件维护一个常量池(Constant Pool Table,保存在Class文件中,不是方法区的运行时常量池,其内容在类加载时被复制到运行时常量池),存放着编译器生成的字面值(文本字符串、8种基本类型值、final声明的常量等)和符号引用(类和方法全限定名、字段的名称描述符、方法的名称和描述符),他们以序列化形式存储。字符串常量池是运行时产生的,位于堆上唯一存在(JDK6.0前在方法区,JDK7.0后移入堆中)常量池和字符串常量池区别

- 字段信息:声明顺序、修饰符、类型、名字等

- 方法信息:声明顺序、修饰符、返回类型、名字、参数列表、异常表、方法字节码、操作数栈和局部变量表大小

- 类变量:非final类变量(即static变量。因为final类变量可以在编译期确定加载类后进入运行时常量池,final类变量存储在运行时常量池里面,每一个使用它的类保存着一个对其的引用)

- 指向类加载器的引用:jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。

- 指向Class实例的引用:jvm为每个加载的类都创建一个java.lang.Class的实例(存储在堆上)。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据(类的元数据)联系起来, 因此,类的元数据里面保存了一个Class对象的引用;

- 方法表:一组对类实例方法的直接引用

- 运行时常量池

  • 运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用
  • JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
     

 

6. 直接内存(Direct Memory):https://www.cnblogs.com/qingchen521/p/9177357.html

直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

 

3. 垃圾收集

垃圾收集基本发生在堆和方法区(线程私有的三个区域随线程生存和毁灭因此内存分配和回收具有确定性)

1. 判断对象是否存活:引用计数算法 / 根搜索算法

- 引用计数算法:对于对象A有一个引用它时,它的计数器+1,失效后-1,若计数器为0则回收该对象。存在A,B互相引用,引用器都不为0,但没有其他引用对象A,B,则会引发内存泄漏。

- 根搜索算法

根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

Java虚拟机将以下对象定义为 GC Roots :

Java虚拟机栈中引用的对象:比如方法里面定义这种局部变量 User user= new User();
静态属性引用的对象:比如 private static User user = new User();
常量引用的对象:比如 private static final  User user = new User();
本地方法栈中JNI引用的对象

 

2. 引用:强引用、软引用、弱引用、虚引用

 

3. 方法区回收:主要回收废弃常量(常量池中没有对象引用的常量)和无用的类(Java堆中不存在该类任何实例 / 加载该类的ClassLoader已被回收 / 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类的方法)

 

4. 垃圾收集算法

- 标记-清除算法(Mark-Sweep):通过根节点,标记所有从根节点开始的较大对象,未被标记的就是未被引用的垃圾对象;清除未被标记的对象。

- 复制算法(Copying):主要用于年轻代的垃圾收集(存活对象少,垃圾多),将内存分为两块,当一块用完时,将存活的对象复制到另一块,然后清理掉已使用的内存空间。HotSpot虚拟机实现为将内存分为1块较大的Eden空间和2块较小的Survivor空间,每次使用1个Eden和其中1块Survivor,当回收时将Eden和Survivor中存活的对象拷贝到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。默认Eden和Survivor 大小比例为8:1。

- 标记-整理算法(Mark-Compact):针对老年区(大部分为存活对象),标记需要回收的对象,将所有对象压缩到内存的一端,再清理边界所有的空间,避免碎片产生。

- 分代收集算法:新生代用复制算法,老年代用标记-清除或标记-整理算法(Hotspot将内存分为年轻代和老年代的原因)。

 

5. 垃圾收集器:

- Serial收集器:用于新生代的单线程收集器,垃圾收集时暂停所有工作线程。优点是简单高效,没有线程交互开销,常用于Client模式下虚拟机。

- ParNew收集器:Serial收集器的多线程版本,多用于Server模式下的虚拟机。

- Parallel Scavenge收集器:使用复制算法,并行(Parallel)的多线程收集器。关注吞吐量(Throughout)高效利用CPU(CMS等关注于缩短垃圾收集时用户线程的停顿时间,提升用户体验)。尽快完成程序运算任务,适合后台运算没有太多交互的任务。

- Serial Old收集器:Serial收集器老年代版本。单线程收集器使用“标记-整理算法”。

- Parallel Old收集器:Parallel Scavenge收集器老年代版本。多线程收集器使用“标记-整理算法”。

- CMS(Concurrent Mark Sweep)收集器:基于“标记-清除”,并发收集、低停顿的收集器,重视服务响应速度,第一款真正意义上的并发(Concurrent)收集器。“初始标记”标记GC Roots能关联到的对象;“并发标记”进行GC Roots Tracing(即顺着引用链标记,耗时长);“重新标记”修正并发标记期间,用户程序继续运作导致标记产生变动的那一部分对象的标记记录(比初始标记稍长)。缺点:对CPU资源敏感、无法处理浮动垃圾(用户线程继续运行中产生的垃圾)、容易产生空间碎片。

- G1(Garbage First)收集器:基于“标记-整理”,可预测停顿的收集器。https://www.cnblogs.com/haitaofeiyang/p/7811311.html

G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合

G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表。每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来)

  • 初始标记(Initial Marking)
    初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。

  • 并发标记(Concurrent Marking)
    并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

  • 最终标记(Final Marking)
    最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

  • 筛选回收(Live Data Counting and Evacuation)
    筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分价值高的Region区的垃圾对象,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。回收时,采用“复制”算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存。

 

6. 内存分配与回收策略:(堆内存分配策略)

- 对象优先在Eden区分配:Eden区没有足够空间时,虚拟机发起一次Minor GC。

Minor GC(新生代GC):非常频繁,回收速度快

Full GC / Major GC(老年代GC):经常伴随Minor GC,速度慢10倍以上。触发条件:老年代或方法区空间不足、调用System.gc时、空间分配担保等。

- 大对象直接进入老年代:大对象指大量连续内存空间:字符串,数组等。目的是为了避免“分配担保机制”(Eden满了把对象移入老年代)带来的复制导致的效率降低。

- 长期存活对象进入老年代:每次Minor GC存活加1岁,(默认15为老),-XX:MaxTenuringThreshold设置阈值

- 动态对象年龄判断:Survivor中相同年龄大小总和超过空间一半,年龄大于或等于的对象进入老年代(无需年龄大于阈值)

- 空间分配担保:发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代剩余空间大小,如果大于则改成一次Full GC。

 

Tips:如何减少GC次数? https://blog.csdn.net/mccand1234/article/details/52078645

  1. 对象不用时显示置null。
  2. 少用System.gc()。
  3. 尽量少用静态变量。
  4. 尽量使用StringBuffer,而不用String累加字符串。
  5. 分散对象创建或删除的时间。
  6. 少用finalize函数。
  7. 如果需要使用经常用到的图片,可以使用软引用类型,它可以尽可能将图片保存在内存中,供程序调用,而不引起OOM。
  8. 能用基本类型就不用基本类型封装类。
  9. 增大-Xmx的值。

 

 

4. JVM调优案例和分析

这里摘录文章中的几个例子和解决方法:

1. 案例:选用64位JDK1.5,网站不定期长时间未响应

排查:监控服务器 ——> GC停顿久未响应 + 大对象多导致经常Full GC ——> 吞吐量优先收集器的GC停顿长+64位JDK目前性能差

解决:若干32位虚拟机建立逻辑集群 + 前端搭建负载均衡器,反向代理分配访问 + 更换响应快的收集器CMS用于交互多的系统

2. 案例:集群共享数据从数据库迁移到全局缓存,不定期内存溢出

排查:分析heapdump文件,大量NAKACK对象 ——> 网络交互频繁,网络不满足传输要求,重发数据内存堆积导致内存溢出

解决:减少频繁写操作

3. 案例:考试系统不定时内存溢出

排查:-XX:HeapDumpOnOutOfMemoryError 和 jstat ——> 溢出没有文件产生 + 堆和永久代无异常 ——> 系统日志找到异常堆栈 ——> NIO导致直接内存不够(直接内存会等老年代满了Full GC顺便清理)

解决:增加直接内存空间

4. 案例:外部命令导致系统缓慢,压测CPU利用率高

排查:Dtrace看哪些系统调用花CPU资源 ——> “fork”系统调用 ——> Java调用shell脚本,频繁创建新进程执行外部命令

解决:使用Java API获取信息

5. 案例:系统运行期间频繁出现集群节点虚拟机进程关闭

排查:系统日志,远程断开连接问题 ——> 同步Web服务调用时间长,连接错误 ——> 异步调用积累Web服务,大量等待线程和Socket连接

解决:异步调用改为生产者/消费者模式的消息队列

 

 

5. 虚拟机性能监控与故障处理工具

1. jps:JVM Process Status,虚拟机进程状况工具。类似Unix命令ps,列出正在运行的虚拟机进程,并显示虚拟机执行主类(main函数所在类),以及进程的本地虚拟机唯一ID(类似操作系统进程PID)。

2. jstat:JVM Statistics Monitoring Tool,虚拟机统计信息监视工具。运行时期定位虚拟机性能问题的工具,显示类装载、内存、垃圾收集、JIT编译等运行数据。(具体参数用法P81页)

3. jinfo:Configuration Info for Java,Java配置信息工具。实时查看和调整虚拟机各项参数。

4. jmap:Memory map for Java,Java内存映射工具。生成堆转储快照,查询堆和永久代详细信息。

5. jstack:Stack Trace for Java,Java堆栈跟踪工具。生成当前时刻线程快照,用于定位线程出现长时间停顿的原因(死锁,死循环等)

 

6. 类文件结构

Class文件是以8字节为基础单位的二进制流

1. 魔数(Magic Number):头4个字节,确认文件是否是能被虚拟机接受的Class文件(值为0xCAFEBABE)。

2. 次版本号(Minor Version)+ 主版本号(Major Version):第5,6字节 + 第7,8字节。确认该Class文件可被什么版本JDK虚拟机执行。例:主版本号为0x0032,为十进制的50,该版本可被JDK1.6及以上版本虚拟机执行。

3. 常量池:(第141页)

常量池容量计数值(入口处,u2类型),表示包含常量的数量。

常量:字面量(字符串、被声明final的常量值)+ 符号引用(类和接口的全限定名 + 字段的名称和描述符 + 方法的名称和描述符)。

4. 访问标志:占2个字节,识别类或接口的访问信息(类还是接口、是否public、是否abstract等)。

5. 类索引、父类索引、接口索引集合:这三项用于确认类的继承关系。

6. 字段表集合:用于描述接口或类声明的变量(包含字段作用域:public等、类变量还是实例变量:static、可变性:final、并发可见性:volatile、可否序列化:transient、字段数据类型:基本类型,对象,数组、字段名称)。

7. 方法表集合:用于描述方法,类似字段表集合。

8. 属性表集合:包含在Class文件、字段表、方法表中,用于描述某些场景专有的信息。(详细内容第155页)

 

 

7. 虚拟机加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程就是虚拟机的类加载机制。JVM属于懒加载,即当用到这个class类的时候,才会把它从Class文件加载到内存

1. 加载

1)通过一个类的全限定名来获取定义此类的二进制字节流。(灵活的方式获取例如从jar,war格式获取、Applet从网络获取、运行时计算生成)

2)将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。(按虚拟机所需格式从外部二进制字节流存储到方法区)

3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

2. 验证

确保Class文件的字节流中包含的信息符合当前虚拟机的要求。包含:文件格式验证(魔数、版本、编码等)+ 元数据验证(是否有父类、是否正确继承、是否有与父类矛盾的方法或字段等)+ 字节码验证(类型转换安全校验等)+ 符号引用验证(权限定名是否能找到对应类、合法字段描述符、方法字段访问性等)

3. 准备

为类变量分配内存并初始化为默认值。

4. 解析:(第183页详细过程)

将常量池中的符号引用替换为直接引用。(针对类或接口、字段、类方法、接口方法)

符号引用:一组符号来描述所引用的目标,符号可以是任何字面量。

直接引用:直接指向目标的指针、相对偏移量或间接定位目标的句柄。引用目标在内存中。

5. 初始化:

对类的静态变量、静态代码块初始化赋值。

 

 

8. 类加载器

用于在Java虚拟机外部实现类的加载阶段(即通过全限定名获取二进制字节流)

对于任意一个类,它和加载它的类加载器确认其在Java虚拟机中的唯一性。(同一类被不同加载器加载,结果两个类不相等)

1. 启动类加载器(Bootstrap ClassLoader):由C++实现,将<JAVA_HOME>/lib目录中类库加载到内存。

2. 扩展类加载器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>/lib/ext目录下类库加载到内存。开发者可以直接使用。

3. 应用程序类加载器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径(ClassPath)上所指定的类库

 

双亲委派模型

如果一个类加载器收到了类加载请求,它首先不会尝试加载这个类,而是把这个请求委派给父类加载器去完成,只有当父加载器反馈无法完成加载请求时,子加载器才尝试自己去加载。

优点:防止重复,保证安全(java.lang.Object只会被启动类加载器加载;自定义同名类String不会被加载)

双亲委派图

Tomcat类加载器架构:

Tomcat目录结构有4组目录可以存放Java类库:

1. /common目录:类库可以被Tomcat和所有Web应用程序共同使用。由CommonClassLoader加载。

2. /server目录:类库被Tomcat使用,对所有Web应用程序都不可见。由CatalinaClassLoader加载,隔离Tomcat和Web应用程序的类库保证服务器自身安全。

3. /shared目录:类库可被所有Web应用程序使用,对Tomcat不可见。由SharedClassLoader加载,用于应用程序间共享类库。

4. /WebApp/WEB-INF目录:类库仅被此Web应用程序使用,对Tomcat和其他Web应用不可见。由WebappClassLoader加载,隔离不同Web应用的Java类库。

Jsp加载器实现了热修改:jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。(来源

Tomcat自定义了WebAppClassLoader类加载器重写了loadClass方法(默认采用双亲委派)。打破了双亲委派的机制,即如果收到类加载的请求,会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载Web应用自己定义的类。

加载顺序为:Bootstrap ——> Extension ——> Application ——> JasperLoader ——> WebApp ——> Shared ——> Common

 

 

9. 其他

1. 使用Java命令行,在同一台电脑上,同时启动了3个java程序,这个时候,在系统中有几个jvm?

3个JVM实例。一般来说每个Java应用会有自己的JVM实例和操作系统进程,每个JVM实例互相独立(Class Data Sharing可实现JVM实例共享内存)。但对于一些应用服务器,如Tomcat,可以跑多个Web app,并且他们都共用一个JVM实例。https://stackoverflow.com/questions/5947207/is-there-one-jvm-per-java-application

 

2. JVM为什么要解释执行和即时编译(编译执行)混合用?

我们知道要执行java代码需要编译器将其编译为字节码文件,再由jvm去翻译执行字节码文件。翻译执行的过程分为两种:

  • 解释执行。边翻译边执行代码
  • 即使编译(JIT)。直接把字节码编译成机器代码,再由机器直接执行,机器执行时效率很高

大部分代码执行采用前者,对于热点代码(执行多次的)采用后者。后者虽然速度更快,但其实有两个缺点:一个是编译时间长/整体时间长,即如果全由字节码翻译为机器码+机器码执行的时间可能不如翻译为字节码+边解释边运行的速度;其次是全部采用即时编译的话,jvm无法获得程序的运行时信息,这就使得jvm无法对代码进行很好的优化。因此对于那种使用率高的代码,翻译成机器码再执行多次,速度会大大提升

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值