JVM系列之《深入了解Java虚拟机》笔记

23 篇文章 1 订阅

JVM系列 01 介绍

文章目录

一)简介

摘录

跨平台,“一次编写,到处运行”
相对安全的内存管理和访问机制,避免绝大部分的内存泄漏和指针越界问题
热点代码检测和运行时编译及优化
完善的API

Java技术体系 = Java程序设计语言,各平台JVM,Class文件结构,Java API,第三方类库
JDK = Java程序设计语言,JVM,Java API类库
JRE = Java API的SE,JVM

Java技术体系分为4个平台 = Card,ME,SE,EE

Java发展史/版本特征

JVM发展史:如HotSpot VM等

《深入理解Java虚拟机》Java待实现技术:模块化,混合语言,多核并行,丰富语法,64位虚拟机

二)关注

1. 运行时数据区

1.1 程序计数器
较小的内存空间

看作当前线程所执行的字节码的行号指示器
虚拟机的概念模型:字节码解释器,工作时通过改变这个计数器的值,选择下一条需执行的字节码指令,分支  循环  跳转  异常处理  线程恢复等基础功能,都需要依赖这个计数器来完成

JVM的多线程,通过线程轮流切换并分配CPU时间片的方式,来实现
单核如上,多核就是真正的并行

线程私有

线程执行Java方法,计数器记录正在执行的虚拟机字节码指令的地址
线程执行Native方法,计数器为空(Undefined)
1.2 Java虚拟机栈
同程序计数器,Java虚拟机栈也是线程私有,生命周期和线程相同

虚拟机栈描述的是:Java方法执行的内存模型。每个方法执行时,会创建一个栈帧用于存储“局部变量表、操作数栈、动态链接、方法出口等信息”;方法从调用至完成,对应着一个栈帧从入栈到出栈

局部变量表,存放了编译期可知的各种基本数据类型、对象引用类型、returnAddress类型。

64位的long和double类型数据,会占用2个局部变量空间(Slot),其余占用一个。
局部变量表所需内存空间,在编译期间完成分配,运行期不改变其大小。

JVM规范中,对这个区域规定了两种异常情况:栈溢出,内存溢出
1.3 本地方法栈
类Java虚拟机栈(为Java方法,字节码服务),而本地方法栈为Native方法服务

JVM规范中,对这个区域规定了两种异常情况:栈溢出,内存溢出
1.4 堆
堆,是JVM管理的内存中最大的一块。

线程共享。在JVM启动时创建,唯一目的就是存放对象实例。

JVM规范:所有对象实例及数组都要在堆上分配。
随着JIT编译期的发展以及逃逸分析技术逐渐成熟,栈上分配  标量替换优化技术导致“堆上分配对象”不再那么绝对

堆,是垃圾收集器管理的主要区域。
收集器基本采用分代收集算法,所以堆可细分为:新生代,老年代。
新生代可再分:Eden空间,From Survivor空间,To Survivor空间等

从内存分配角度,线程共享的堆可能划分出多个线程私有的分配缓冲区(TLAB)

JVM规范:堆可处于物理上不连续的内存空间,只要逻辑上连续即可。

固定大小或可扩展(主流JVM,通过-Xmx和-Xms控制堆大小)

堆无法扩展时,将会抛出内存溢出异常
1.5 方法区
线程共享。用于存储已被JVM加载的“类信息,常量,静态变量,即时编译器编译后的代码等数据”

习惯于HotSpot虚拟机开发部署的Programmer,称“方法区”为“永久代”,两者并不等价,原因是HotSpot将GC分代收集扩展至方法区。
永久代实现的方法区,更容易遇到内存溢出问题。
HotSpot使用元空间取代永久代,即方法区的实现——元空间
	永久代,物理上是堆的一部分,和新生代、老年代是连续的
	元空间,属于本地内存,存储内容不同(元空间存储类的元信息,静态变量和常量池等并入堆中)
运行时常量池
属于方法区的一部分。

Class文件除了有“类版本、字段、方法、接口等描述信息外,还有一项信息为常量池”
常量池:存放编译期生成的各种字面量和符号引用
类加载进入方法区,常量池内容(字面量,符号引用)存放到运行时常量池
	符号引用翻译出来的直接引用,也存储在运行时常量池中
	运行时进入常量池:如String类的intern()方法
1.6 其他
直接内存
不是JVM运行时数据区的一部分。

JDK 1.4引用了NIO类,基于通道与缓冲区的IO方式,可使用Native函数库直接分配堆外内存
堆外内存,通过一个存储在Java堆中的DirectByteBuffer对象,作为堆外内存的引用进行操作。
	避免Java堆和Native堆来回复制数据

2. 对象深入

基于HotSpot虚拟机

2.1 对象的创建
创建对象(如克隆、反序列化)通常仅仅依赖一个new关键字
	局限于普通Java对象,不包括数组和Class对象等
JVM如何处理?
  1. new指令

  2. 类加载检查:JVM检查该指令的参数(类),是否能在(运行时)常量池中定位到一个类的符号引用

    检测这个符号引用代表的类,是否已被加载、解析和初始化过
    	若没有,先执行相应的“类加载过程”
    
    类加载会执行<client>,进行静态变量/代码块的执行
    
  3. 分配内存:

    堆中内存:垃圾回收器是否带有压缩整理功能
    	指针碰撞:堆使用和未使用的内存泾渭分明,中间放着一个指针作为分界点的指示器
    		分配内存就是移动指针(指示器)
    	空闲列表:一个列表,记录所有可用的内存块
    
    并发分配,不安全
    	CAS + 失败重试:保证更新操作的原子性
    	堆中的线程私有的分配缓冲区,TLAB + 同步锁定
    
  4. 初始化零值:不包括对象头

  5. 设置对象头:对象所属类、如何找到类的元数据信息、对象哈希码、GC分代年龄等

    JVM当前运行状态不同,如是否启用偏向锁等,对象头会有不同的设置方式
    
  6. 执行<init>方法:由invokespecial指令决定

<client>和<init>见下文
2.2 对象的内存布局
对象头
包括两部分:
第一部分,Mark Word:存储对象自身的运行时数据(哈希码、GC分代年龄,锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等)
第二部分,类型指针:对象指向它的类元数据的指针

若对象是一个数组,对象头还有一块用于纪律数组长度的数据
实例数据
各种类型的字段:从父类继承而来,或子类定义
对齐填充

非必须。占位符,保证对象大小必须是8字节的整数倍

2.3 对象的访问定位
句柄
栈上引用
堆:句柄池(到实例数据的指针,到类型数据的指针),实例池
方法区:对象类型数据
直接指针
栈上引用
堆:到类型数据的指针,实例数据

3. 垃圾收集

3.1 垃圾收集区域
  • 方法区
3.2 垃圾对象探测
引用计数法

对象中添加一个引用计数器

可达性分析算法

一系列GC Roots对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链

若一个对象到GC Roots对象,没有任何引用链相连,证明此对象不可用
问题1:相互循环引用
Gc Roots对象
  • 虚拟机栈引用的对象
  • 方法区中类静态属性引用的变量
  • 方法区中常量引用的对象
  • 本地方法栈JNI(Native方法)引用的对象
四种引用类型
  • 强:如Object o = new Object()。只要强引用存在,GC不会回收被引用的对象

  • 软:有用但并非必需的对象。系统内存溢出前,会回收

  • 弱:非必需对象。被弱引用引用的对象,只能生存到下一次GC发生前

    JDK 1.2后,提供WeakReference

  • 虚:不影响引用对象的生存。目的是引用对象被回收时,能收到一个系统通知

    JDK 1.2后,提供PhantomReference

垃圾对象的二次标记
  1. 与GC Roots对象不相连,进行第一次标记
  2. 进行筛选:垃圾对象没有覆盖finalize(),或JVM已经调用过该对象的finalize()
    • 没有必要执行finalize()
  3. 有必要执行finalize(),对象被放入F-Queue队列,稍后由一个JVM自动建立、低优先级的Finalizer线程执行它
  4. 对F-Queue队列中的剩余对象进行第二次标记:执行finalize(),且重新与GC Roots对象相连,可免去回收,重新变成可用对象
方法区回收
  • 废弃常量
  • 无用类
如String,常量池对象没有被引用——废弃String常量

无用类判断:满足下述3条件的无用类,可以回收,并非必然回收
1. 类所有实例,都被回收
2. 该类的ClassLoader已被回收
3. 该类的Class对象,没有被引用,无法在任何地方通过反射访问该类方法
3.3 垃圾收集算法
算法描述优缺
标记—清除1. 标记;2. 清除效率低;产生内存碎片
复制等分(优化:8-1-1)效率低;可用内存变少
标记-整理1. 标记;2. 移动;3. 清理
分代收集新生代——复制;老年代——标记清除/整理
标记-清除算法

在这里插入图片描述

复制算法

标记-整理算法

3.4 垃圾收集器

理解不够,暂略

GC介绍区域
Serial单线程收集器,收集会暂停所有工作线程新生代
ParNewSerial的多线程版本,s-t-w新生代
Parallel并行新生代
Serial OldSerial的老年代版本老年代
Parallel Old
CMS并发收集器老年代
G1
3.5 内存分配策略
  1. 对象优先在Eden
  2. 大对象直接进入老年代
  3. 长期存活的对象将进入老年代
  4. 动态对象年龄判定:Survivor空间中,相同年龄所有对象大小综合大于Survivor空间的一半,年龄大于等于该年龄的对象,都直接进入老年代
3.6 内存回收策略
Minor GC
老年代最大可用的连续空间,大于新生代所有对象总空间
	Minor GC可确保是安全的

不成立,JVM查看"HandlePromotionFailure"是否允许担保失败
	允许,检查老年代最大可用连续空间,是否大于“历次晋升到老年代对象的平均大小”	
		大于,尝试Minor GC(存在风险)
		小于,Full GC
    不允许,Full GC
Full/Major GC

回收老年代。

3.7 GC日志

一条日志大体包括如下:详细请自行查阅。

  • GC发生的时间
  • GC停顿类型:GC或Full GC(stop-the-world)
  • GC发生区域:
    • GC前已使用容量→GC后已使用容量(总容量)
    • GC前已使用容量→GC后已使用容量(总容量)
  • 此次GC占用时间

4. 工具

4.1 JDK命令行工具
命令介绍
jpsJVM进程
jstatJVM运行状态信息(类加载、内存、GC、JIT等)
jinfoJVM配置信息
jmap获取堆转储快照等
jhat堆转储快照分析工具
jstackJava堆栈跟踪工具
4.2 JDK可视化工具
JConsole

JDK 5提供的虚拟机监控工具

VisualVM

JDK 6多合一故障处理工具(监控、故障、性能分析等)

4.3 反编译字节码
  • javap
  • JD-GUI
  • jclasslib

5. 类文件结构

底层系统→适配JVM→跨平台字节码→各种编译器→各种语言程序

5.1 Class是什么
Class文件是一组以8位字节为基础单位的二进制流,其数据按序紧凑排列
5.2 class文件结构
  • 无符号数:属于基本数据类型,以"u1, u2, u4, u8"代表字节长度,可用来描述数字、索引引用、数量值、UTF-8编码的字节串值
  • 表:由多个无符号数,或其他表作为数据项构成的复合数据类型
常量池
  • 字面量:如文本字符串、声明为final的常量值等
  • 符号引用:类和接口全限定名、字段的名称和描述符、方法的名称和描述符
访问标志

标识类、接口层次的访问信息,包括:

  • Class是类还是接口
  • 是否定义为public
  • 是否定义为abstract
  • 若是类,是否声明为final
5.3 字节码指令

JVM的指令,由一个字节长度的操作码,零或多个操作数构成。

JVM面向操作数栈,而不是寄存器架构
  • 加载和存储指令
  • 运算指令
  • 类型转换指令
  • 对象创建和访问指令
  • 操作数栈管理指令
  • 控制转移指令
  • 异常处理指令
  • 同步指令
同步指令:管程
JVM支持方法级和方法内部的同步,都是用管程(Moitor)支持

方法级同步,隐式,即无须通过字节码指令来控制
	方法常量池的方法表中的ACC_SYNCHRONIZED访问标志
方法内同步,显示,需Java语言中的synchronized语句块
	JVM的指令集使用两条指令支持synchronized关键字的语义
	- monitorenter
	- monitorexit

6. 类加载机制

类加载:JVM把描述类的数据,从Class文件加载到内存,并对数据进行:
	“校验、转换解析和初始化”。
最终形成可以被虚拟机直接使用的Java类型。

Class文件:一串二进制的字节流。(来自磁盘.class文件、网络、压缩包等)
6.1 生命周期
加载:
连接:包括“验证、准备、解析”三个部分
	验证:
	准备:
	解析:
初始化
使用和卸载
6.1.1 各个阶段的顺序如何?
加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的。
而:解析阶段不一定,发生在“初始化前或后”
	在初始化前开始,即一般流程
	在初始化后开始,为支持“运行时绑定”(或动态绑定、晚期绑定)

另,开始即意味不是进行、完成,仅仅是开始解析阶段
6.2 类加载时机
有且只有5种情况的“初始化”——主动引用类
未发生初始化,遇到以下5种情况,需立即初始化
1)遇到new、getstatic、putstatic、invokestatic这四条字节码指令
	new对象,调用类成员(包括静态变量、静态方法)
2)反射
3)初始化类时,发现父类未初始化,需要先初始化其父类
4)JVM启动,指定的主类——main(),先初始化
5)JDK 7的动态语言支持

说明:final static字段,在编译期被放入常量池,不会导致初始化
无初始化——被动引用类
1)子类引用,访问父类静态字段,不会导致子类初始化
2)数组定义引用的类,不会触发初始化
	T[] arr = new T[n];
	不会触发类T的初始化
3)调用一个类的常量字段,不会初始化该类
	class A{final a..}
	class B{ main(){ System.out.println(A.a);} }
	A.a,实际上在编译阶段通过常量传播优化,常量值被存储到B类的常量池种
	调用相当于B类对自身常量池的引用,与A类毫无关系
接口——初始化
主要在于第3)种情况:初始化类时,发现父类未初始化,需要先初始化其父类

在接口中,初始化子接口时,不要求其父接口已完成初始化,只要在真正使用到父接口,如:
	引用父接口中定义的常量。才会初始化父接口
6.3 类加载过程
6.3.1 加载
  1. 类全限定名,获取此类二进制字节流
  2. 字节流,转化为方法区的运行时数据结构
  3. 内存中,生成对应Class对象,作为方法区该类各种数据的访问入口
java.lang.Class对象存储区域
JVM没有明确规定,Class对象存放在堆中
HotSpot虚拟机,Class对象存放在方法区
6.3.2 验证
  • 目的:确保Class文件的字节流包含的信息,符合当前虚拟机的要求,不会危害虚拟机自身安全
  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证:将符号引用转化为直接引用,发生在解析阶段
6.3.3 准备
  • 目的:正式为类变量分配内存,并设置其零值(初始值)

    传统方法区实现——永久代,静态变量和常量池存入方法区
    新的实现——元空间,静态变量和常量池等并入堆中
    
6.3.4 解析
  • 目的:常量池内的符号引用,替换为直接引用(运行时常量池)
6.3.5 初始化
  • 目的:执行类构造器<client>()方法的过程

    <client>由下述语句合并:
    	类变量的赋值动作
    	静态语句块的语句
    
    收集顺序,由语句在源文件出现顺序决定
    1. 静态语句块,只能访问到定义在之前的变量
    2. 静态语句块,对于定于在之后的变量,可以赋值,但是不能访问
    3. 父类的<client>先于子类
    
<client>例子
static class A {
	static {
		i = 0;
	}
	static int i = 1;
}		

输出:i=1
6.4 类加载器

作用:实现类的加载动作。

类在JVM中的唯一性确定
  • 类加载器
  • 类本身
两个不同类加载器,加载同一个类,最终在JVM中是两个类
双亲委派模型
  • 启动类加载器:BootStrap ClassLoader。C++实现,是JVM一部分
  • 其他类加载器:由Java实现,独立于JVM
    • 扩展类加载器:Extension ClassLoader
    • 应用程序类加载器:Application ClassLoader
    • 自定义类加载器:可选
双亲委派模型的工作过程:
	一个类加载器,收到类加载的请求,委派给父类加载器去完成,
	当父类加载器反馈无法完成这个加载请求,子加载器才尝试自己加载
	
	根类加载器,即启动类加载器,执行加载类
破坏双亲委派模型
  1. JDK 1.1和JDK 1.2的先后关系,具体略
  2. 线程上下文类加载器,接口提供者(SPI, Service Provider Interface)
  3. 动态性,如代码热替换、模块热部署等。导致双拼委派模型的树形结构→网状结构

7. 字节码执行引擎

7.1 运行时栈帧结构

栈帧,是JVM运行时数据区的虚拟机栈的栈元素。

栈帧存储了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息
局部变量表

局部变量表,是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

  • 最小单位:变量槽,Slot。一个Slot可存放一个32位以内的数据类型

    • 32位以内,一个slot存储
    • 64位的long和double,两个slot存储
    局部变量表中第0位索引,默认用于传递方法所属对象实例的引用,方法中以this表示
    
操作数栈

操作数栈每个元素可以是任意的Java数据类型,包括long和double

动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。(运行期,符号引用转为直接引用,称为动态链接)

方法返回地址等
7.2 方法调用:摘录

确定被调用方法的版本(即调用哪一个方法)

类加载解析阶段:解析

方法调用中的目标方法,在Class文件里都是一个常量池的符号引用。

类加载的解析阶段,一部分符号引用直接转化为直接引用。前提条件:

  • 方法在运行前,就有一个可确定的调用版本,且运行期不可改变
    • 静态方法:与类相关
    • 私有方法:外部不可访问
    • …等非虚方法
分派:运行还是编译阶段?
  • 静态分派:方法重载,编译阶段
  • 动态分派:方法重写
执行引擎执行代码
  • 解释执行:解释器执行字节码
  • 编译执行:即时编译器产生本地代码执行

三)程序编译与代码优化

1. 概述

JVM在编译期,能干些什么
编译期是一段不确定的操作过程,可能执行如下操作:
	前端编译器或编译器的前端,将.java转变成.class文件的过程
	后端运行期编译器(JIT,Just In Time),将.class转变成本地机器码的过程
	静态提前编译器(AOT,Ahead Of Time),直接把.java转变成本地机器码的过程
优化发生在哪个阶段

在编译期,运行效率几乎无优化。大部分都是发生在运行期,收益更多(.class跨平台,可能来自Java Scala等语言)。

Sun的Javac编译器,通过“语法糖”,使编码更简单。

2. 编译期优化

早期优化。

Javac编译器

由Java编写实现。

编译过程

1和2可能循环执行,主要看2是否对抽象语法树有更新

  1. 解析与填充符号表
    1. 词法、语法分析
    2. 填充符号表
  2. 插入式注解处理器的注解处理:编译期处理(大部分注解在运行期起作用)
  3. 分析与字节码生成
语法糖
  • 泛型
  • 自动拆装箱
  • 遍历循环
  • 条件编译

3. 运行期优化

运行期最初,由解释器进行解释执行。

  • 热点代码:为提升效率,运行时,热点代码会被编译成本地机器码(由JIT编译器完成)
热点代码
  • 多次调用的方法
  • 多次执行的循环体:依然以方法为单位作为编译对象
热点探测判定方式
  • 基于采样的热点探测
  • 基于计数器的热点探测

4. 编译优化技术

略。

逃逸分析

四)高效并发

1. 了解

硬件架构

CPU,高速缓存,主存…

一致性问题
内存模型

2. Java内存模型

目的:屏蔽掉各种硬件和操作系统的内存访问差异,以实现Java程序在各平台下都能达到一致内存访问效果。

主内存与工作内存

主内存类比硬件主存

工作内存类比硬件高速缓存

工作内存:保存对应线程,使用到的变量的主内存副本拷贝
	线程对变量操作(读写),在工作内存进行
	线程间无法直接访问对方工作内存中变量,需借助主存传值
内存间交互操作——协议

内存间交互操作,都是原子、不可再分

lock
unlock
read
load
use
assign
store
write

举例:线程A,B。
线程A “lock”主存变量x,使用,使用完“unlock”
	先“read” x,传到线程A自己的工作内存
	再“load",把“read”的值放入工作内存的“副本x"中
	JVM使用变量,即“副本x”:“use”,执行引擎获取“副本x”
	...//执行引擎处理
	JVM回写变量,即“副本x”:“assgin”,执行引擎将新值,写给“副本x”
	“store”:将“副本x”从工作内存传递到主存
	“write”:将“store”的值,写到主存变量x
	
线程B执行如上

举例2:并发执行
线程A,从主存读取x,工作内存存储副本x1
线程B,从主存读取x,工作内存存储副本x2
volatile的特殊规则
  1. 保证可见性。执行引擎获取变量,每次都直接从主存获取,而不是工作内存,避免一致性问题

  2. 禁止指令重排序优化。

    读,写,读,写...
    
    普通变量,仅保证再该方法的执行过程中,所有依赖赋值结果的地方,都能获取到正确的结果,而不能保证
    变量赋值操作的顺序  与程序代码中的执行顺序一致。
    某一个读过程,依赖上一个写过程,它们相对顺序不会发生改变
    

    原理:volatile修饰的变量,其字节码中,赋值后多执行一个“lock …”——内存屏障

内存屏障:指令重排序时,不能将后面的指令重拍到内存屏障之前的位置。

long和double的特殊规则

long和double的非原子性协定:读写共4个操作,非原子性

long和double,未被volatile修饰的读写会被划分为2次32位操作,即JVM可以不保证64位数据类型的“load、store、read、write”的原子性。

原子性,可见性,有序性

原子性,原子性变量操作——主存到工作内存的读 赋值,工作内存和执行引擎的读写,工作内存到主存的 读 赋值

可见性,线程间共享变量x,线程A修改x 其他线程立即获取这个修改

有序性,

线程内,所有操作有序
线程间,所有操作无序:指令重排序,工作内存与主存同步延迟(不一致)
	volatile,synchronized保证
	先行发生原则作了一些约定
先行发生原则
  • 程序次序规则
  • 管程规定规则
  • volatile变量规则
  • 线程启动规则
  • 线程终止规则
  • 线程中断规则
  • 对象终结规则
  • 传递性

3. Java线程

线程实现方式
  • 内核线程
  • 用户线程
  • Java线程:基于操作系统原生线程模型实现
线程调度

操作系统为线程分配CPU使用权的过程。

  • 协同式
  • 抢占式
线程状态及迁移
  • 新建
  • 运行
  • 无限期等待
  • 限期等待
  • 阻塞
  • 结束

4. 线程安全与锁优化

线程安全如何体现
  • 不可变
  • 绝对线程安全:锁,如Vector批操作的原子性
  • 相对线程安全:如Vector
  • 线程兼容:同步
  • 线程对立:同步也无法实现并发安全
线程安全如何实现
  • 互斥同步
  • 非阻塞同步
  • 无同步方案:如不适用共享变量(狭义)、线程本地存储
锁优化
适应性自旋
锁消除
锁粗化
轻量级锁
偏向锁
等等
自旋锁和自适应自旋

自旋,执行一个忙循环

  • 自旋时间|次数限制
  • 自适应,自旋时间不定:前一次在同一个锁上的自旋时间及锁的拥有者的状态
    • 自旋时间
    • 锁,自旋很少获取,或经常成功
锁消除

不存在共享数据的锁——消除

实现:逃逸分析的数据支持

锁粗化

连续加锁解锁,加锁同步的范围扩展到整个操作序列的外部——粗化

轻量级锁

无竞争 + CAS

偏向锁

无竞争 + 连续由同一个线程获取锁(无同步)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值