Jvm和Gc 最全的总结

Jvm

在这里插入图片描述

1.Java内存区域与内存溢出异常

1.1运行时数据区域
1.1.1程序计数器
# 内存空间小,线程私有。

* 字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成

		如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
		
		如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

1.1.2 Java 虚拟机栈

# 线程私有,生命周期和线程一致。

* 描述的是 Java 方法执行的内存模型:
		每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

* 局部变量表:
		存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

* java虚拟机栈可能出现的两种类型的异常:
		StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
		OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

# 需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小

在这里插入图片描述

1.1.3 本地方法栈
* 本地方法栈是与虚拟机栈发挥的作用十分相似,区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
		可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。
		也会有 StackOverflowError 和 OutOfMemoryError 异常。
1.1.4 Java 堆
* 对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。		
		内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
		

* OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。


* 注意:
		它是所有线程共享的,他的目的是存放对象实例。同时他也是GC管理的主要区域,因此常被称为GC堆,			又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代。再细致点还有Eden等
		
* 当前主流的虚拟机如HotSpot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存完成实例分配,而且对无法扩展将报OOM错误(OutOfMemoryError)
1.1.5 方法区
* 属于共享内存区域,
		存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
		如static修饰的变量加载类的时候就被加载到方法区中。

* 在老版本的jdk中,方法区也被称为永久代
		因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾回收器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过从JDK7以后,HotSpot虚拟机便将常量池从永久代移除了

一张图来说明每个区域的存储的内容

1.1.6 运行时常量池
* 属于方法区一部分,class文件除了有类的字段,接口,方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。

		编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。
1.1.7 直接内存
* 非虚拟机运行时数据区的部分

jdk8真正开始废弃永久代,而使用元空间(Metaspace)

1.2内存溢出
# 两种内存移除异常

1. StackOverFlowError(SOF)
		当请求的栈深度大于虚拟机所允许的最大深度
		
2. OutOfMemoryError(OOM)
		虚拟机在扩展栈时无法申请到足够的内存空间[一般都能设置扩大]

例子

 @Test
    public void test012() {
        ArrayList<Object> list = new ArrayList<>();
        try {
            //创建对象的速度可能高于jvm回收的速度
            for (; ; ) {
                list.add(new Object());
            }
        } catch (OutOfMemoryError e) {
            e.printStackTrace();
        }
        try {
            //递归造成的StackOverflowError 这边因为每运行一个方法就会创建一个栈帧,栈帧创建太多无法继续申请到内存扩展
            hi();
        } catch (StackOverflowError e) {
            e.printStackTrace();
        }
    }
    private void hi() {
        hi();
    }

在这里插入图片描述

2.Gc垃圾回收

2.1 哪些内存需要回收
1. Java运行时的五大区域
		方法区,虚拟机栈,本地方法栈,堆,程序计数器
		
2. 线程私有的:
		程序计数器,本地方法栈,虚拟机栈, 由线程的而生,随线程而灭。
		其中栈中的栈帧随着方法的进入顺序来执行入栈和出栈操作。
		一个栈帧需要分配多少内存取决于具体的虚拟机实现并且在编译期间即确定下来了
		当方法或线程执行完毕后,内存就随着回收,因此无需关心。
		
		
3. 线程共享的:
		堆,方法区
		方法区中加载着类加载的信息
		
		但是一个接口中多个实现类需要的内存可能不太一样,一个方法中多个分支需要的内存也可能不太一样
		所以只有在运行期间才知道这个方法创建了哪些对象,需要多少内存
		这部分内存份分配和回收都是动态分配的。
		GC也就是要关注这部分内存。
		

2.2 堆的回收区域
# 为了高效的回收 ,Jvm将堆分为三个区域
		新生代,老年代,永久代(jdk1.8之后metaspace代替永久代)
		
1. 新生代:
		分为三块:
			一个较大的Eden(伊甸)区和两块较小的Survivor(幸存者)区 默认比例8:1:1
			而Survivor又分为From Survivor区 和To Survivor区
		
		一般新new出来的对象都存在于新生代中,新生代的对象存活率很低,常规的一次垃圾回收可以回收70%~90%的空间,回收率很高。
		
		GC开始的时候,对象只会存在于Eden区和From Survivor区,To Survivor区是空着的(作为保留区域)。
		
		GC进行时,Eden区中所有存活的对象都会被复制到To Survivor 区,而在From Survivor区中,任存活的对象会根据他们的年龄值决定去向。年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一次垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。
		接着清空Eden区和From Survivor区,新生代存活的对象都在To Survivor区。
		接着,Form Survivor区和To Survivor区会交换他们的角色,也就是新的To Survivor区就是上一次GC清空的 From Survivor区,新的From Survivor区就是上次GC的To Survivor区
		总之,不管怎样,都会保证To Survivor在一轮GC后是空的
		
		GC时,当To Survivor去没有足够的空间存放上一次新生代收集下来的存活对象的时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
		
		
2. 老年代
		在新生代经历了多次(具体看虚拟机的配置的阀值)GC后任然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率较高,在老年代中进行Gc的频率相对较低,而且回收的速度也比较慢。
		
		
3. 永久代
		永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。

	
2.3 判断对象是否存活算法
1. 引用计数法:
		早期判断对象是否存活都是使用的这种算法,这种算法判断很简单。
		简单的说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效一次就减1,当为0的时候就判断对象不会再被引用。
		
		优点:实现简单效率高,被广泛使用
		缺点:难以解决循环引用的问题,就是假如两个对象相互引用已经不会再被其他引用。导致一直不会为0无法进行回收。
		
		
2. 可达性分析算法:
		目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。
		它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。
		
		即使可达性算法中不可达的对象,也不是马上要被回收的,还有可能抢救一下。
		
* 真正宣告对象死亡需要两个过程。
		1. 可达性分析后发现没有应用链
		2.查看对象是否有finalize方法,如果有重写且在方法内完成自救【比如再建立引用】,还是可以抢救一下的
		注意: 这边的一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功,第二次失败的情况。
		如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的队列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。

在这里插入图片描述

2.4 常见的垃圾回收算法
2.4.1 标记清除算法(Mark-Sweep)
* 这是最基础的垃圾回收算法,分为两个阶段:
		标记阶段和清除阶段
		
		标记阶段:
			就是标记出所有需要被回收的对象
		清除阶段:
        	就是回收被标记的对象所占用的空间

在这里插入图片描述

* 如上图,标记回收算法的实现

# 缺点:
		标价回收算法容易产生垃圾碎片,碎片太多会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
		


2.4.2 复制算法(Copying)
* 复制算法是将内存按容量分为大小相等的两块,每次只使用其中的一块。

		当这一块内存用完了,就将还存活着的对象复制到另一块的上面,然后将把这一块内存一次性清理掉。这样不容易产生垃圾碎片。
		
		

在这里插入图片描述

* 优点:
		这样的算法运行高效不容易产生内存碎片,
		
* 缺点:
		但是内存空间的使用作出了高昂的代价,因为能够使用的内存缩减到原来的一半。
		
* 复制算法是为了克服句柄的开销和解决内存碎片的问题。

		它开始的时候会把堆分为一个对象面和多个空闲面,程序会从对象面为对象分配空间,当对象满了,基于Copying算法的垃圾收集就从根集合(GC Roots)中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存空间没有空闲洞),这样空闲面就变成了对象面,原来的对象面就变成了空闲面,程序会在新的对象面中分配内存。

2.4.3 标记-整理算法(Mark-compact)
# 为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-compact算法。

		该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理对象,而是将存活的对象都向一端移动,然后清理掉端边界以外的内存
		记住: 标记之后,先不清理,先移动再清理回收对象
		
* 标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。
		标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动。因此成本更高,但是解决了内存碎片的问题。
2.4.4分代收集算法(Generatonal Collection)
# 分代收集算法是目前大部分JVM垃圾收集器采用的算法。
		它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。
		
		一般情况下分为老年代和新生代,在堆区之外还有一个代是永久代。
		
		老年代的特点:
			每次的垃圾回收时只有少量对象需要被回收
			
		新生代的特点:
			每次垃圾回收时都有大量的对象需要被回收
			
		那么就根据不同代的特点采取不同的收集算法
		
* 新生代采用的是复制(Copying)算法
		因为新生代每次垃圾回收都要回收大部分对象,也就是说复制的操作的次数较少。
		
		新生代的空间是分为三部分的一个较大的Eden区和两个较小的Survivor区。占比为8:1:1
		
		每次使用Eden区和Survivor区的From区的空间,当进行回收的时候,将Eden区和Survivor中还存活的对象复制到另一块Survivor中去,然后清理掉Eden区和刚才使用的Survivor区。
		
* 由于老年代的特点是每次都回收少量的对象,一般使用的是标记整理算(Mark-Compact)法
		在新生代经过了n次的Gc后任然存活下来的对象(即超过指定的阀值的GC次数还存活的对象),就会被放在老年代中去,因此可以认为老年代中的对象都是存活的生命周期较长的对象
		
		内存也比新生代大很多(比例是1:2)
		
		当老年来内存满的时候触发Major Gc 即Full Gc,Full Gc发生的频率比较低
		
# 当Eden区没有足够的空间的时候就会触发一次Minor Gc
		发生在新生代的GC叫做Minor GC,不一定等Eden区满了才触发的
		
		当To区空间不足以存放Eden区和Survivor区存活的对象的时候,就将存活的对象直接放到老年代中
		
		若是老年代也慢了就会触发一次Full GC(Major Gc),也就是新生代和老年代都进行回收。



新生代的GC流程图

在这里插入图片描述

2.4.5 新生代和老年代的区别
* 所谓的新生代和老年代是针对于分代收集算法来定义的

		新生代分为Eden区和两个Survivor区
		
		对象会首先进入到Eden区,(当然也有特殊情况,如果是大对象那么会直接放入到老年代(大对象是指需要大量连续内存空间的java对象)。),当Eden没有足够空间的时候就会 触发jvm发起一次Minor GC。如果对象经过一次Minor GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空 间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代 中了,当然晋升老年代的年龄是可以设置的。如果老年代满了就执行:Full GC 因为不经常执行,因此采用了 Mark-Compact算法清理

2.5 常见的垃圾回收器

在这里插入图片描述

2.5.1 新生代收集器
2.5.1.1 Serial收集器
# Serial是用于新生代单线程的收集器,采用复制算法进行垃圾收集
		serial 进行垃圾收集的时候,不仅只用一条线程执行垃圾收集工作,它在收集数据的同时,所有的用户线程必须暂停(Stop The World)。
		
		
		如下是Serial收集器和Serial Old 收集器结合进行垃圾回收的示意图,当用户线程都执行到安全点的时候,所有的线程暂停执行,Serial收集器以单线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程开始执行
		
* 适用场景:
		Client 模式;单核服务器
		
可以使用-XX:+UserSerialGC 来选择Serial作为新生代收集器
		

在这里插入图片描述

2.5.1.2 ParNew 收集器
1. ParNew是一个Serial的多线程版本,和 Serial无区别
		ParNew在单核CPU环境下并不会比Serial收集器达到更好的效果,它默认开始的收集线程和CPU的数量是一致的,
		可以通过 -XX:ParallelGCThreads来设置垃圾收集的线程数
		
		如下图


* 使用场景: 
		多核服务器;
		与CMS收集器搭配使用。
		当使用 -XX:+UserConcMarkSweepGC 来选择 CMS 作为老年代收集器时,新生代收集器默认就是 ParNew
		也可以用 -XX:+UseParNewGC 来指定使用 ParNew 作为新生代收集器。
		
		

在这里插入图片描述

2.5.1.3 Paraller Scavenge 收集器
# Paraller Scavenge 收集器也是一款用于新生代的多线程收集器

		与ParNew 的不同之处是ParNew 的目标是尽可能的缩短垃圾收集时用户线程的停顿时间,
		Parller Scavenge 的目标是达到一个可控制的吞吐量。
		
# 吞吐量:
		吞吐量 就是CPU执行用户线程的时间与CPU执行总时间的比值
		吞吐量 = 运行用户代码的时间/(运行用户代码的时间+垃圾回收的时间 )
		
		比如虚拟机一共运行了100分钟,其中垃圾回收使用了1分钟,那么吞吐量就是99%。

* 如下图是Paraller 收集器和Paraller Old收集器结合进行垃圾收集的示意图
		在新生代,当用户线程都执行到安全点时,所有线程暂停执行,Paraller Scavenge 收集器以多线程,采用复制算法进行收集工作,收集完之后,用户线程继续开始工作。
		
		在老年代,当用户线程都执行到安全点的时候,所有线程暂停工作,Paraller Old收集器以多线程,采用标记整理算法进行垃圾收集工作。


* 适用场景:
		注重吞吐量,高效利用CPU,需要高效运算且不需要太多交互
		
* 可以使用 -XX:+UseParallelGC 来选择Parallel Scavenge 作为新生代收集器

# jdk7、jdk8 默认使用 Parallel Scavenge 作为新生代收集器。


* 可以通过 -XX:MaxGCPauseMillis 来设置收集器尽可能在多长时间内完成内存回收
* 可以通过 -XX:GCTimeRatio 来精确控制吞吐量。

在这里插入图片描述

2.5.2 老年代收集器
2.5.2.1 Serial Old 收集器
# Serial Old 收集器是Serial的老年代版本,同样是一个单线程收集器,采用标记-整理算法

* 适用场景:
		Client模式(桌面应用);
		单核服务器;
		与Parallel Scavenge收集器搭配;
		作为CMS收集器的后备预案

在这里插入图片描述

2.5.2.2 CMS(Concurrent Mark Sweep) 收集器
# CMS 收集器是一种以最短回收停顿时间为目标的收集器,以“最短时间用户停顿时间”著称。使用的是标记清除算法

* 整个垃圾回收过程分为4个步骤:

1. 初始标记:
		标记一下GC Roots 能直接关联到的对象,速度较快。
		
2. 并发标记:
		进行GC Roots Tracing, 标记出全部的垃圾对象,耗时较长。
		
3. 重新标记:
		修正并发标记阶段因用户程序继续运行而导致变化的对象的标记记录,耗时较短。
		
4. 并发清除:
		用标记-清除算法清除垃圾对象,耗时较长
		
* 整个过程耗时最长的并发标记和并发清除都是和用户线程一起工作,所以总体上来说,
		CMS收集器收集可以看做是和用户线程并发执行的。


# 使用场景
		重视服务器的响应速度,要求系统提顿时间最短
		可以使用 -XX:+UserConMarkSweepGC 来选择 CMS 作为老年代收集器		
# 缺点: 
		1、对CPU资源敏感:
			默认分配的垃圾回收线程数为(CPU数+3)/4,随着CPU数量下降,占用CPU资源越多,吞吐量越小
			
		2、无法浮动处理垃圾:
			在并发清理阶段,由于用户线程还在运行,还会不断的产生新的垃圾,CMS收集器无法在当次收集中清除这部分垃圾。
			
			同时由于在垃圾收集阶段(并发标记)阶段用户线程也在并发执行,CMS收集器不能像其他收集器那样等老年代被填满时在进行收集,需要预留一部分空间提供用户线程运行使用。
			
			当CMS运行的时候,预留的空间无法满足用户线程的需要,就会出现 “ Concurrent Mode Failure ”的错误,这时将会启动后备预案,临时用 Serial Old 来重新进行老年代的垃圾收集。
			
			
			
		因为CMS是基于标记清除算法的,所以垃圾回收后会产生空间碎片:
        	可以通过-XX:UserCMSCompactAtFullCollection 开启碎片整理(默认开启)
        	
        	在CMS进行Full GC之前,会进行内存碎片的整理。
			还可以用 -XX:CMSFullGCsBeforeCompaction 设置执行多少次不压缩(不进行碎片整理)的 Full GC 之后,跟着来一次带压缩(碎片整理)的 Full GC。		

在这里插入图片描述

2.5.2.3 Parallel Old收集器
# Parallel Old 收集器是 Parallel Scavenge 的老年代版本,是一个多线程收集器,采用标记-整理算法。
		可以与 Parallel Scavenge 收集器搭配,可以充分利用多核 CPU 的计算能力。
		
		
* 使用场景:
		与Parallel Scavenge 收集器搭配使用;注重吞吐量。
		jdk7、jdk8 默认使用该收集器作为老年代收集器,
		使用 -XX:+UseParallelOldGC 来指定使用 Paralle Old 收集器。
		

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DzVA4o2j-1593590104610)(E:\notes\运维\jvm\Jvm.assets\Parallel Scavenge 收集器-1593583787810.png)]

2.5.3 新生代和老年代的垃圾收集器
2.5.3.1 G1收集器
# G1 进行垃圾收集的范围是整个堆内存,是jdk1.7才正式引用的商用收集器。现在jdk9默认的收集器;
		它采用的是“化整为零”的思路。把整个堆内存划分为多个大小相等的独立区域(Region),在 G1 收集器中还保留着新生代和老年代的概念,它们分别都是一部分 Region


1. 每一个方块就是一个区域,每个区域可能是Eden,Survivor,老年代。每种区域的数量也不一定。
		JVM启动的时候,会自动设置每个区域的大小(1M~32M,必须是2的次幂),
		最多可以设置2048个区域(即支持的最大的堆内存为32M*2048 = 64G),
		假如设置 -Xmx8g ,-Xms8g,则每个区域大小为8g/2048 = 4M;
		
		
* 为了在GC Roots Tracing 的时候避免扫描全堆,在每个Region中,都有一个 Remembered Set 来实时记录该区域内的引用类型数据与其他区域数据的引用关系,在标记时直接参考这些引用关系就可以知道这些对象是否应该被清除,而不用扫描全堆的数据。		

* G1 收集器可以 “ 建立可预测的停顿时间模型 ”,它维护了一个列表用于记录每个 Region 回收的价值大小(回收后获得的空间大小以及回收所需时间的经验值),这样可以保证 G1 收集器在有限的时间内可以获得最大的回收效率。

在这里插入图片描述

# G1收集器收集的过程有:
	1、初始标记
	2、并发标记
	3、最终标价
	4、筛选回收
	和CMS收集器的前几步过程很像
	
1. 初始标记:
		标记出 GC Roots 直接关联的对象,这个阶段速度较快,需要停止用户线程,单线程执行。
		
2. 并发标记:
		从 GC Root 开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。
		
3. 最终标记:
		修正在并发标记阶段引用户程序执行而产生变动的标记记录。
		
4.  筛选回收:
		筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是 Garbage First 的由来——第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程。



# 使用场景:
		要求尽可能可控 GC 停顿时间;内存占用较大的应用。
		可以使用 -XX:+UseG1GC 使用 G1 收集器,jdk9 默认使用 G1 收集器。

在这里插入图片描述

2.6 Jvm垃圾回收器的总结
* -XX:+UseSerialGC:在新生代和老年代使用串行收集器

* -XX:+UseParNewGC:在新生代使用并行收集器

* -XX:+UseParallelGC :新生代使用并行回收收集器,更加关注吞吐量

* -XX:+UseParallelOldGC:老年代使用并行回收收集器

* -XX:ParallelGCThreads:设置用于垃圾回收的线程数

* -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器

* -XX:ParallelCMSThreads:设定CMS的线程数量

* -XX:+UseG1GC:启用G1垃圾回收器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值