题目>>JVM>>1

2 篇文章 0 订阅

1、介绍一下JVM垃圾回收器?

垃圾回收器是垃圾回收算法的具体实现。垃圾回收算法包括标记-清除、标记-复制、标记-整理。

  • 根据分代回收,可以把垃圾收集器分为:
    • 新生代收集器:Serial、ParNew、Parallel Scavenge
    • 老年代收集器:Serial Old、Parallel Old、CMS;
    • 整堆收集器:G1

Serial收集器
在这里插入图片描述

  • 是一种新生代的垃圾收集器;是单线程(不是指垃圾收集器只有一个,而是这个收集器在工作时,必须暂停其他所有工作线程,即Stop The World)工作的,使用标记-复制算法进行回收。
  • 优点:简单高效,是所有收集器中额外内存消耗最小的,适用于单核处理器处理器核心数较少的环境。

Serial Old收集器
在这里插入图片描述

  • 同样也是单线程收集器,使用的是标记-整理算法。
  • 有两种用途,一是在JDK5之前与Parallel Scavenge收集器搭配使用;二是作为CMS收集器的备选。

ParNew收集器
在这里插入图片描述

  • ParNew是Serial的多线程版本,也是新生代收集器。除去同时使用多条线程外,其他参数和机制(STW、回收策略、对象分配规则等)都和Serial完全一致。
  • 单线程情况下不如Serial收集器效率高;处理效率随着处理器核数的增加而增加。

Parallel Scavenge收集器

  • 是一种新生代收集器,是基于标记-复制算法实现的,而且可以并行收集,类似于ParNew收集器。
  • 与ParNew不同的地方在于,其关注点主要在达到一个可控制的吞吐量上,吞吐量计算如下所示。
    在这里插入图片描述

Parallel Old收集器

  • 是一种老年代收集器。支持多线程并发,基于标记-整理算法实现。JDK6之后,在吞吐量上优先考虑Parallel Scavenge + Parallel Old的组合。
    在这里插入图片描述

CMS收集器(Concurrent Mark Sweep)
在这里插入图片描述

  • 是一种老年代收集器。基于标记-清除算法实现的。支持并发收集
  • 工作流程如下:
    • 初始标记:标记一下与GC ROOTS(老年代)年轻代引用的老年代的对象,速度较快;需要STW;
    • 并发标记
      • 从GC ROOTS的直接关联对象开始遍历整个对象图的过程,速度较慢,但是不需要停顿用户线程
      • 会发生错标、漏标的情况:在运行期间,会发生新生代对象晋升老年代、更新老年代中引用关系等现象。
    • 重新标记:将错标与漏标的对象,重新标记一下,会扫描新生代与老年代。需要STW;
    • 并发清理:所有存活对象已经被标记完成,清除没有标记的对象。
  • 缺点
    • CMS对处理器资源非常敏感,在并发标记/清除阶段,会因为占用一部分线程导致应用程序变慢降低吞吐量
    • CMS无法处理浮动垃圾(CMS并发清理时用户线程还在运行,有产生了新的垃圾,CMS无法在此次回收中处理他们,只能留到下一次GC时在处理,扎部分垃圾是浮动垃圾),可能会造成FULL GC;
    • 会出现大量的空间碎片,会给分配大对象带来困难。

GARBAGE FIRST收集器(G1)
在这里插入图片描述

  • G1是面向局部的垃圾收集器,在JDK9之后,替代了Parallel Scavenge + Parallel Old的组合,作为默认的垃圾收集器。
  • JVM设置:
    • -XX:+UseG1GC 开启G1收集器
    • -XX:MaxGCPauseMillis=10 GC最大停顿毫秒数,JVM会调整Java堆的大小与GC相关参数,使GC引起的暂停时间短于10ms。
    • -XX:G1HeapRegionSize=1M 设置Region大小:1M - 32M
  • G1将堆内存划分为多个大小相等的独立区域(Region),可能是新生代(Eden、Survivor)或老年代空间。Region可能是Eden、Survivor、Old、Humongous(用于存储大对象:大小超过了Region容量的一半)
  • 运行过程:
    • 初始标记
      • 标记一下GC ROOTS及其可以直接关联的对象;并修改TAMS指针的值,使得下一阶段并发运行时,**能够准确的在可用的Region中分配新对象。**速度很快;
      • TAMS:Top At Mark Start:在GC于用户线程并发的过程中,需要解决回收过程中新对象的分配,G1为每个Region设计了两个 TAMS指针,用于记录并发回收过程中的新对象。这样的对象被认为是存活的,不纳入垃圾回收范围。
    • 并发标记
      • 从GC ROOTS开始对堆中的对象进行可达性分析,扫描整个堆中的对象图,找出要回收的对象。耗时较长,可与用户线程并发执行。
      • 扫描时也会出现错标、漏标的情况,G1会使用SATB算法来解决。
      • SATB:
    • 最终标记
      • 对用户线程做一个短暂的暂停,用于处理并发标记阶段遗留下来的少量的SATB记录。
    • 筛选回收
      • 负责Region的统计数据,对各个Region的回收价值和成本进行排序
      • 根据用户期望停顿时间制定回收计划,可由多个Region组成回收集,然后把决定回收的那部分Region的存活对象复制到空的Region中,再清理掉旧Region的全部空间
      • 操作涉及到对象的移动,所以必须要暂停用户线程,由多个收集器线程并行完成。
  • 目标:并非追求低延迟,目标是在延迟可控的情况下尽可能提高吞吐量。
  • 缺点
    • Region中存在跨代引用的问题;
    • 需要保证收集线程与用户线程互不干扰?G1使用的是SATB(原始快照)。G1为每个Region分配了两个TAMS指针,并发回收时新分配的对象地址都必须在这两个指针位置上。如果内存回收速度赶不上分配速度,G1收集器会冻结用户线程执行,会造成FULL GC。
    • 无法建立可预测的停顿模型。

2、GC调优的过程?确定参数的原因?

常用调优手段
-Xms256m:初始化堆大小为 256m;
-Xmx2g:最大内存为 2g;
-Xmn50m:新生代的大小50m;
-XX:+PrintGCDetails 打印 gc 详细信息;
-XX:+HeapDumpOnOutOfMemoryError 在发生OutOfMemoryError错误时,来 dump 出堆快照;
-XX:NewRatio=4 设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8 设置新生代 Eden 和 Survivor 比例为 8:2;
-XX:+UseSerialGC 新生代和老年代都用串行收集器 Serial + Serial Old
-XX:+UseParNewGC 指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelGC 新生代使用 Parallel Scavenge,老年代使用 Serial Old
-XX:+UseParallelOldGC:新生代 ParallelScavenge + 老年代 ParallelOld 组合;
-XX:+UseConcMarkSweepGC:新生代使用 ParNew,老年代使用 CMS;
-XX:NewSize:新生代最小值;
-XX:MaxNewSize:新生代最大值
-XX:MetaspaceSize 元空间初始化大小
-XX:MaxMetaspaceSize 元空间最大值

3、JVM类加载机制?

简介:Java虚拟机负责把描述类的数据从Class文件加载到系统内存中,并对类的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,被称作是Java的类加载机制。

加载过程:一个类从被加载到虚拟机内存开始,一直到卸载结束,中间过程为:加载 -> 链接(验证、准备、解析) -> 初始化 -> 使用 -> 卸载。如图。
类加载过程

  • 加载
    加载是整个类加载过程中的第一个阶段,在此期间Java虚拟机需要完成三件事:
    • 通过一个类的**全限定名(包名+类名:java.lang.String)**来获取定义此类的二进制字节流,获取的方式可以是jar、war、网络、JSP文件生成等;
    • 将这个字节流代表的静态存储结构转化为运行时数据区中方法区的数据结构
    • 内存中生成一个Class对象,作为方法区这个类的各种数据的访问入口。对象会存放在方法区中
  • 链接
    类的加载过程生成了类的Class对象。在链接过程中,会将类的二进制数据合并入JRE(Java运行时环境)中。
    • 验证
      • 验证被加载的类(Class文件字节流)是否有正确的结构,类数据的结构是否符合虚拟机的要求;
      • 验证分为四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证
    • 准备
      为类的静态变量在方法区分配内存,并赋初始值(0/null)。如static int a = 100,在此过程中会给a赋值0;
      当使用final进行修饰时,会直接赋值;static final int a = 666;则将a赋值为666。
    • 解析
      • 将类的二进制数据中的符号引用(符号引用以一组符号来描述所引用的目标,只要符号可以无歧义的定位到目标即可)转换为直接引用(直接应用可以直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。直接引用的目标一定被加载到了内存中)
      • 在编译的时候虚拟机并不知道所引用类的地址,所以就用符号来代替。在解析阶段就是为了把符号引用转化为真正的地址的阶段
      • 分为四个步骤:类或接口的解析、字段解析、方法解析、接口方法解析。
  • 初始化
    • 初始化主要工作是为静态变量赋程序设定的初值。如static int a = 100,在链接-准备中已有初始化a=0,此初始化将a赋值为100;
    • 加载和链接均以虚拟机为主导初始化由应用程序主导,即真正开始执行java代码
    • java虚拟机规范中规定了以下四种场景出发初始化:
      • 调用new关键字的时候进行初始化;使用getstatic或putstatic方法读取或者设置一个静态字段的时候;invokestatic调用静态方法的时候;
      • 在初始化类的时候,如果父类还没有初始化,需要首先对父类进行初始化;
      • 在使用java.lang.reflect包的方法进行反射调用的时候;
      • 当虚拟机启动时,用户需要制定执行主类的时候,即**首先初始化包含main()**方法的类。
  • 使用
    初始化之后的代码由JVM来动态调用执行。
  • 卸载
    • 当代表一个类的Class对象不再被引用,那么Class对象的生命周期就结束了,在方法区中的数据也会被卸载
    • JVM自带的类加载器装载的类,是不会卸载的;用户自定义的类加载器加载的类是可以卸载的。

4、JVM内置锁的特性?以及与lock的区别?

5、双亲委派机制?

JVM类加载默认使用的是双亲委派机制

在这里插入图片描述

JVM存在三种类加载器:

  • 启动类加载器(BootStrap Class Loader)
    • 是由C++实现的,是java虚拟机自身的一部分,启动类加载器无法被java程序直接通过引用调用;
    • 负责加载存放在 JAVA_HOME \ lib目录的类库。JDK中的常用类(java.*)的加载都是通过启动类加载器来完成的。
  • 扩展类加载器(Extension Class Loader)
    • 是由java实现的;
    • 负责加载 JAVA_HOME\lib\ext目录下的类库。
  • 应用程序类加载器(Application Class Loader)
    • 是由sum.misc.Launcher$AppClassLoader 来实现的,负责加载ClassPath(用户路径)上所有的类库;如果应用程序中未定义类加载器,则默认使用此加载器。

在这里插入图片描述

双亲委派机制

  • 当加载器需要加载一个类时,子类加载器并不会马上去加载,而是向上委派去请求父类加载器,一直请求到启动类加载器
  • 启动类加载器加载不了的时候,依次向下委派再往下让子加载器进行加载。

双亲委派机制的缺陷

  • 父类加载器无法使用子类加载器加载过的类。
    • 如:由启动类加载器定义的外部接口(SPI,Service Provider Interface),允许第三方实现此类接口;实现接口的类应该由应用程序类加载器加载,而启动类加载器无法找到实现类,也不能代理给应用程序类加载器加载。这时,会出现无法创建由应用程序类加载器加载的应用实例。

6、JVM的内存结构?各个结构的作用?

在这里插入图片描述

如图所示,JVM内存结构包括:
线程独占: 虚拟机栈、本地方法栈、程序计数器
线程共享: 方法区、堆

虚拟机栈

  • 线程私有
  • 存放内容:方法在执行过程中,会在虚拟机栈中创建一个栈帧每个方法的执行过程对应了一个入栈和出栈的过程。
    • 局部变量表
      • 单位是slot。用于存储方法参数和定义在方法体内的局部变量。包括java基本数据类型、对象引用等。
      • 局部变量表中直接或间接引用的对象都不会被回收
      • 在一个实例方法被调用时,局部变量表的第0位指向当前对象的引用,即this
    • 操作数栈
      • 在方法执行过程中,根据字节码指令,在操作数栈中写入或提取数据;
      • 保留了计算过程的中间结果,同时作为计算过程中变量的存储空间。
    • 动态链接
      • 每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。目的是为了支持当前方法的代码能够实现动态链接。
      • 当java源文件被编译为字节码文件时,所有变量和方法引用都会作为符号引用保存在class文件的常量池中。
      • 动态链接的作用是在运行期间将符号引用都转换为调用方法的直接引用
    • 返回地址
      • 用于存放PC寄存器的值。
      • 方法退出时,会返回到该方法被调用的位置。返回地址记录的就是这个位置的pc寄存器的值。

本地方法栈

  • 线程私有
  • 存放内容:存储的是java中使用的native关键字修饰的方法存储的区域。

程序计数器

  • 线程私有
  • 存放内容当前线程执行的字节码的行号指示器,用于存储线程的指令地址;用于判断线程的循环、分支、跳转、异常、切换和恢复等功能。

方法区

  • 线程共享
  • 介绍:JVM8之前放到永久代中;JVM8之后由元空间实现,放到本地内存,不再受JVM限制;原来的字符串常量池静态变量转移到了里。
  • 存放内容
    • 类元信息:放置了类的基本信息:版本、字段、方法、接口和常量池表(存储了类编译生成的字面量(字符串、基本类型的常量)、符号引用(引用常量池中的字符串,存储的是索引),在类加载完之后会被解析到运行时常量池中);
    • 运行时常量池:主要存放类加载完之后的字面量和符号运用;具备动态性,编译期间和运行期间都可以将新的常量放到常量池中,如String的intern()方法。
    • 对象实例:类初始化生成的对象、基本数据类型的数组。
    • 字符串常量池:存储了String类型的对象的直接引用

  • 线程共享
  • 介绍:堆是线程共享的数据区。
  • 组成部分
    • eden区:8/10的年轻代
    • survivor 0:1/10年轻代
    • survivor 1:1/10年轻代
    • 老年代:2/3的堆空间
    • 年轻代:1/3的堆空间

7、虚拟机栈的几道面试题

7.1、举出栈溢出的情况?
  • StackOverFlowError:当线程的请求超过了虚拟机栈的深度,会报错;多出现于递归调用;可以调节-Xss参数,调整栈的大小。
  • OutOfMemoryError:虚拟机栈扩展无法申请到足够的内存,会报错。
  • 局部变量表内容越多,栈帧越大,栈深度越小。
7.2、调整栈大小,就能保证不出现溢出么?

不能

7.3、分配的占内存是否越大越好?

不是。由于物理内存是一定的,栈内存越大,可执行的线程数越少。

7.4、垃圾回收会回收虚拟机栈么?

不会

7.5、方法中定义的局部变量是否线程安全?

只有一个线程操作,则线程安全;多个线程操作,则不安全。

8、JVM中,对象是如何创建的?

  • 当虚拟机获取到一个new的指令(字节码)时,首先会检查这个指令的参数是否能在常量池中定位到一个类的符号引用;并且检查这个符号引用所代表的类是否已经被加载、解析和初始化;
  • 如果发现类没有经过上述的类加载过程,则执行相应的类加载过程
  • 类加载完成后,虚拟机将会对新生对象分配内存,所需内存的大小在类加载完成后即可确定;
  • 接下来虚拟机会对对象进行设置,如确定对象的所属类、对象的hashcode、对象的gc分代年龄等。这些信息存储在对象头中;
  • 到此,对于虚拟机来说,对象创建完毕了。new 之后会执行构造函数(()方法)。

9、内存分配方式有几种?

在类加载完成后,虚拟机需要为新生对象分配内存。

  • 指针碰撞
    • java内存:java堆中内存是规整的,所有使用过的内存放到一边,未使用的放在另一边,中间放着一个指针(即分界指示器)。
    • 新对象分配过程:将指针向未使用的一边挪动对象大小相等的距离
  • 空闲列表
    • Java内存:并不是规整的,已经被使用和未被使用的内存相互交错在一起。
    • 空闲列表:记录了未被使用的内存块,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表。

垃圾收集器中,Serial、ParNew等压缩整理过程的收集器,使用的指针碰撞;CMS等基于清楚算法的收集器使用的是空闲列表。

10、对象的内存分布是怎样的?

在虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据、对齐填充

在这里插入图片描述

  • 对象头
    • 对象头主要包含MarkWord和对象指针Klass Point;如果是数组的话,还要包含数组长度。
    • 在32/64位虚拟机中,MarkWord、Klass Point和数组长度都分别占32/64位,也就是4/8字节。
    • MarkWord:存储对象的HashCode或锁信息
    • KlassPoint:存储对象类型数据的指针。也就是对象指向它的类元数据的指针。虚拟机通过这个指针来确定这个对象是哪个类的实例
      在这里插入图片描述
  • 实例数据
    • 存放的是对象真正存储的有效信息,也是代码中定义的各个字段的字节大小,如一个byte占一个字节,一个int占4个字节。
  • 对齐填充
    • 对齐不是必须存在的,只是起到了占位符(%d、%c)的作用。因为虚拟机要求对象的起始地址必须是8的整数倍,不够的用对齐填充补全。

11、对象访问定位的方式有哪些?

虚拟机中访问对象分为两种方式:通过句柄访问、通过直接指针访问。

  • 通过句柄访问
    在这里插入图片描述
    • 堆中划分出一块内存作为句柄池,栈中的引用存储的是对象的句柄地址;句柄地址中对象实例数据的指针指向了堆中的实例数据;句柄地址中对象类型数据指向了方法区中的类型数据
    • 优点:对象变化移动时,只需要更新句柄地址即可,无需修改对象本身

  • 通过直接指针访问
    在这里插入图片描述
    • 栈中指针指向了堆中实例数据的地址,对象类型数据的指针依旧指向方法区的类型数据。
    • 优点:访问速度快,节省了一次指针定位的时间开销。

12、如何判断对象已经死亡?

  • 引用计数法
    • 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就会+1;当引用失效时,计数器-1;只要任何时刻计数器为0的对象就是不会再被使用的对象。
    • 不能解决对象之间**循环引用(两个对象互相依赖对方,计数器都不为0)**的问题,所以Hotspot虚拟机未使用这种方式。

  • 可达性分析
    • 基本思路:
      • 通过一系列被称为GC ROOTS的根对象作为起始节点集
      • 起始节点集开始,根据引用关系向下搜索,搜索的过程路径被称为引用链。
      • 如果某个对象到GC ROOTS之间没有任何引用链相连接,或者从GC ROOTS到这个对象不可达时,证明此对象是无用对象,需要被垃圾回收。
        在这里插入图片描述
        如图,object1、2、3、4存在引用关系,5、6、7可被回收。

13、GC ROOT相关

可作为GC ROOTS对象的包括以下几种:

  • 虚拟机栈(本地方发表)中引用的对象;
  • 方法区的类静态属性引用的对象,如Java类的引用类型静态变量;
  • 方法区的常量引用的对象,如字符串常量池中的引用;
  • 本地方法栈中JNI的引用的对象;
  • 所有被synchronized持有的对象。

14、如何判断一个不再使用的类?

不再使用的类需要满足以下三个条件:

  • 这个类的所有实例已经被回收,也就是堆中不存在该类的实例对象
  • 加载这个类的类加载器被回收
  • 这个类对应的Class对象没有任何地方被引用,即无法在任何时刻通过反射访问这个类的属性和方法。

15、JVM分代收集理论有哪些?

强分代假说:JVM认为绝大多数对象都是朝生夕灭的;
弱分代假说:熬过越多次垃圾回收的对象也就越难回收;
跨代引用假说:一个新生代对象引用了一个老年代对象,那么垃圾回收时不会回收新生代对象;只有等新生代对象进入到老年代对象时,跨代引用才会消除。

  • 记忆集:未解决跨代引用问题,提出的概念。是一个在新生代中使用的数据结构,记录了一些指针的集合,指向了老年代中哪些对象存在跨代引用。记忆集中存在不同粒度。
    • 字长精度:每个记录精确到一个字长,机器字长就是处理器的寻址位数,比如常见的 32 位或者 64 位处理器,这个精度决定了机器访问物理内存地址的指针长度,字中包含跨代指针。
    • 对象精度:每个记录精确到一个对象,该对象里含有跨代指针。
    • 卡精度:每个记录精确到一块内存区域,区域内含有跨代指针。卡精度是使用了卡表作为记忆集的实现。关于记忆集和卡表的关系,大家可以想象成是 HashMap 和 Map 的关系。

16、JVM中的垃圾回收算法?

首先回答如何判断对象已经死亡的问题。

标记-清除算法

  • 分为两个阶段:标记阶段、清除阶段。
  • 方法:首先标记所有需要回收的对象;在标记完成后,同一回收掉所有被标记的对象。也可以标记存活的对象,回收未被标记的对象。
  • 缺点
    • 执行效率不稳定:如果堆中存在大量的无用对象,且大部分需要回收的情况下,需要进行大量的标记和清理,执行效率随数量增长而降低
    • 内存碎片化:标记清理会在堆内产生大量不连续的内存碎片。碎片太多会造成分配大对象时没有空间,不得不进行下一次垃圾回收。

标记-复制算法

  • 方法:将内存大小划分为相等的两部分A、B,每次只使用其中的一部分A,用完一部分A后,将存活的对象复制到B,然后再把用过的一部分A进行清除。
  • 缺点
    • 复制开销:复制对象存在开销;
    • 内存使用率:虽然解决了碎片问题,不过内存空间缩小到原来的一般,浪费空间。
  • 内存担保方式
    • 方法:
      • 将新生代内存分为Eden和两块Survivor空间,每次分配内存只使用其中一个项
      • 发生垃圾回收时,将Eden和Survivor中存活的对象一次性复制到另一块Survivor空间上,然后直接清理掉Eden和已使用过的Survivor空间

标记-整理算法

  • 方法:标记所有存活的对象,将存活的对象都向一端移动,然后清理掉端边界以外的内存

17、什么是三色标记法?三色标记法会造成什么问题?

可达性分析可知,如果要找到存活对象,需要从GC ROOTS开始遍历,然后搜索每个对象是否可达。根据对象和其引用是否被访问过可分为三种颜色

  • 白色:表示GC ROOTS遍历过程中没有被访问过的对象。在访问开始时,所有对象均为白色;访问结束后为白色的对象,就表示不可达,可以被回收。
  • 灰色:代表对象已经被访问过了,但是这个对象的引用还没有被访问完毕。
  • 黑色:表示这个对象及其引用都被访问过了。

三色标记法的问题

  • 若用户线程正在修改引用关系,此时收集器正在回收引用关系,会把已消亡的对象标记为存活;下次回收时就会回收
  • 若标记时为白色,回收时用户线程正在使用,会造成存户对象处理成死亡的情况。会使用增量更新和原是快照的方法。

18、JVM常用命令

  • jps:虚拟机进程工具
    • 列出当前正在运行的虚拟机进程,并显示虚拟机主类Main Class所在的本地虚拟机唯一ID;
  • jstat:虚拟机统计信息工具
    • 用于监视虚拟机各种运行状态的信息。可以展示虚拟机的类加载、内存、垃圾收集、即时编译等运行时数据
  • jinfo:Java配置信息工具
    • 可以实时调整虚拟机的各项参数。
  • jmap:Java内存映像工具
    • 用于生成存储快照,排查内存占用情况。
  • jhat:虚拟机堆转储快照分析工具
    • 通常与jmap搭配使用,jhat 内置了一个 HTTP/Web 服务器,生成转储快照后可以在浏览器中查看。
  • jstack:Java堆栈跟踪工具
    • 用来追踪堆栈的使用情况。用于查看虚拟机当前时刻的线程快照,线程快照就是当前虚拟机每一条正在执行的方法堆栈的集合。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值