JVM垃圾回收学习总结一

内存与垃圾回收机制

该笔记是通过学习尚硅谷视频而总结的 小白总结 有所不足之处 还请指出
视频地址https://www.bilibili.com/video/BV1PJ411n7xZ?p=1

1.JVM与java体系结构

简介
  • java-跨平台的语言:java运行都需要编译成字节码文件,字节码文件依托于jvm运行;
  • jvm-跨语言的平台:Kotlin\js\scala等语言只需要提供编译器编译成字节码文件即可在jvm上运行;
  • java不是最强大的语言,但是jvm是最强大的虚拟机。
jvm字节码
  • java字节码指的是java语言编译成的字节码,任何能在jvm上执行的字节码格式都是一样的,所以可以统称为jvm字节码;
三大虚拟机
  • HotSpot、JRockit、J9
  • 1996年java1.0时的第一台商用java虚拟机Sun Classic Vm(该虚拟机只提供解释器),java1.4淘汰;
java虚拟机
  • 简介:java虚拟机是一台执行java字节码的虚拟计算机,其拥有独立的运行机制,其运行的java字节码未必由java语言编译而成,也有可能由别的语言编译而成的字节码文件;
  • 特点:一次编译到处运行、自动内存管理、自动垃圾回收功能;
  • 功能:负责装载字节码到其内部,解释\编译为对应平台上的机器指令执行。
  • 位置:{用户{字节码文件{JVM{操作系统{硬件}}}}}jvm是运行在操作系统之上的,其与硬件没有直接的交互
架构模型
  • java编译器输入的指令流基本上是一种基于的指令集架构,另外一种指令集架构则是寄存器的指令集架构
  • 基于栈式架构的特点
    • 设计和实现更简单,适用于资源受限的系统;
    • 避开了寄存器的分配难题:使用零地址指令方式分配;
    • 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈;
    • 不需要硬件支持,可移植性更好,更好实现跨平台。
  • 基于寄存器架构的特点
    • 指令集架构则完全依赖硬件,可移植性差;
    • 性能优秀和执行更高效;
    • 相较于栈式架构,话费更少的指令去完成一项操作。
  • 总结
    • 由于跨平台性的设计,java的指令都是根据栈来设计的,不同平台cpu架构不同,所以不能设计为集于就寄存器的。
生命周期
  • 虚拟机的启动:java虚拟机的启动是通过引导类加载器创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的;
  • 虚拟机的执行:执行一个java程序的时候,真正执行的是一个java虚拟机的进程;
  • 虚拟机的退出:当程序正常执行完成/异常/错误/等时退出。

2.类加载器子系统

image-20200615164405891

image-20200615164800512

  • 类加载器子系统包括三个阶段:加载阶段、连接阶段、初始化阶段
加载阶段
  • 通过一个类的全限定名获取此类的二进制字节流;
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
连接阶段
验证Veritfy
  • 目的为确保字节码文件包含信息符合当前jvm要求,保证被加载类的正确性;
  • 主要包括四种验证:文件格式验证、元数据验证、符号引用验证。
准备Prepare
  • 目的为为类变量分配并且设置该类变量的默认初始值,(0/null/false)
  • 准备阶段不包含用final修饰的static,常量在编译的时候会分配了,准备阶段常量会显示初始化;
  • 准备阶段不会为实例变量分配初始化,类变量分配在方法区,而实例变量会随着对象一起分配到java中。
解析Resolve
  • 将常量池内的符号引用转化为直接引用的过程;(符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中,直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄)
  • 解析阶段主要针对类或接口、字段、类方法、接口方法、方法类型等。
初始化阶段
  • 初始化阶段即执行类构造器方法()的过程(该方法无需定义,是javac编译器自动收集类中的所有类变量的复制赋值动作和静态代码块中的语句合并而来,构造器方法与类的构造器不同);
  • 构造器方法中指令按语句在源文件中出现的顺序执行;
  • 若该类具有父类,jvm会保证子类的()执行前,父类的()已经执行完毕;
  • 虚拟机必须保证一个类的()方法在多线程下被同步加锁。
类加载器

image-20200616091429081

引导类加载器/启动类加载器(Bootstrap ClassLoader)
  • 使用c/c++加载,嵌套在jvm内部
  • 用来加载java核心库(jdk、jre、lib、rt.jar、resources.jar)用于提供jvm自身需要的类
  • Bootstrap ClassLoader没有父加载器
  • bootstrap ClassLoader启动类加载器只加载包含java、Javax、sun等开头的类。
自定义类加载器(User-Define ClassLoader)
  • 所有派生于抽象类的类加载器都划分为自定义类加载器
扩展类加载器(Extension ClasLoader)
  • 由java语言编写
  • 派生于ClassLoader类
  • 父类加载器为启动类加载器
  • 如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

image-20200616091403239

ClassLoader
  • 其是一个抽象类,其后所有的类加载器都继承自ClassLoader,但不包含启动类加载器;
双亲委派机制

image-20200616094314752

  • 简介:jvm对class文件采用的是按需加载的方式,即当需要使用该类时才会将其class文件加载到内存生成class对象,而且加载某个类的class文件时,jvm采用的双亲委派机制,即把请求交由父类处理,双亲委派机制是一种任务委派模式

  • 工作原理:
    • 如果一个类加载器收到了类加载请求,其并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,直到委托给顶层的启动类加载器(Bootstrap ClassLoader);
    • 委托到最顶层之后,如果Bootstrap ClassLoader可以完成加载任务,就返 回成功,若无法完成加载任务,会逐层向下委托,直到有一层能成功加载。
  • 优势:
    • 避免类的重复加载;
    • 保护程序安全,防止核心API被随意篡改。
  • 沙箱安全机制:
    • 当自定义String类,但是在加载该类时会率先使用引导类加载器加载,而引导类加载器在加载的过程中会加载jdk自带的文件(re.jar包中Java、lang、String.class),报错信息没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源码的保护,该方法即为沙箱安全机制。
    • image-20200616095853087

3.运行时数据区概述及线程

image-20200616100531703

    • 红色区域:每个进程一份;
    • 灰色区域:每个线程一份。
JVM线程
    • 虚拟机线程:当jvm达到安全点才会出现;
    • 周期任务线程:一般用于周期性操作的调度执行;
    • GC线程:对在jvm里不同种类的垃圾收集行为提供了支持;
    • 编译线程:在运行时会将字节码编译成本地代码;
    • 信号调度线程:接送信号并发给jvm。

4.程序计数器(Program Counter Register)

介绍
    • JVM中的PC是对物理PC寄存器的一种抽象模拟;
    • 是一块很小的内存空间,是运行速度最快的存储区域;
    • 每个线程都有自己的PC,生命周期与线程的生命周期相同
    • 任何事件一个线程都只有一个方法在执行,也就是所谓的当前方法,PC会存储当前线程正在执行的java方法的JVM指令地址
    • 其是唯一一个在jvm规范中没有规定任何OutOfMemoryError情况的区域
    • 其是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
    • image-20200616103725501
作用
  • PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令。
问题一使用PC寄存器存储字节码指令地址有什么用呢?为什么使用PC寄存器记录当前线程的执行地址呢?
  • 因为每个线程都有一个PC,CPU需要不停的切换各个线程,这时候线程切换回来,就得知道接着从哪开始继续执行,通过PC可以知道下一条指令的执行地址。
问题二PC线程寄存器为什么会被设定为线程私有,即每个线程一份?
  • 为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每个线程都分配一个PC寄存器。

5.虚拟机栈

简介
    • 栈是运行时的单位,而堆是存储的单位;
    • 一个线程对应一个java虚拟机栈。
    • 虚拟机栈内部保存一个个栈帧(基本单位为栈帧),对应着一次次的java方法调用;
    • 生命周期和线程相同;
作用
    • 主管java程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回。
优点
    • 一种快速有效的分配存储方式,访问速度仅次于程序计数器;
    • jvm直接堆栈的操作只有两种(每个方法执行,伴随着进栈/执行结束后的出栈工作);
    • 对于栈而言不存在垃圾回收问题(存在OOM)
两类异常
  • jvm规范允许java栈的大小是动态或者固定不变
    • 如果采用固定大小栈,当请求分配的容量超过最大容量,抛出StackOverflowError异常
    • 如果是动态分配栈,当在创建新的线程而没有足够的容量则会跑OutOfMemoryError异常。

image-20200616111458153

栈运行原理
    • jvm直接堆栈的操作只有进栈和出栈,每个方法对应一个栈帧,先加载的方法先进栈,在一条线程上,只会有一个活动的栈帧,该活动栈帧被称为当前栈帧,与之相对应的是当前方法、当前类,如果在该方法内调用了其它方法,其它方法称为栈帧;
    • 不同线程不允许相互引用不同 ;
    • 栈帧弹出条件:函数正常返回return、抛出未处理异常;
栈帧的内部结构

image-20200616152655104

1 局部变量表/局部变量数组/本地变量表
    • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
    • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不能存在数据安全问题;
    • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximun local variables数据项中,在方法运行期间不会改变局部变量表的大小;
    • 当方法调用结束后,随着方法栈帧的销毁,局部表量表也会随之销毁;
    • 局部变量表最基本的存储单元是Slot(变量槽);
    • 局部变量表中存放编译器可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量;
    • 32为以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
    • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排序(因此静态方法内无法使用this.xxx调用 而非静态方法因为有this变量所以能调用);
    • 栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的;
    • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或简介引用的对象都不会被回收。
2 操作数栈/表达式栈
  • 操作数栈存在于每一个栈帧中;
  • jvm的解释引擎是集于栈的执行引擎,其中的栈指的就是操作数栈;
  • 操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间;
  • 操作数栈的深度在编译期间就定义好了,保存在方法的Code属性中为max_stack的值;
  • 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问;
  • 32位的类型占用一个栈单位深度、64位的类型占用两个栈单位深度;
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC中下一条需要执行的字节码指令;
3 动态链栈(或指向运行时常量池的方法引用)
  • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态连接;
  • 动态连接的作用是为了将符号引用(所有的变量和方法引用都称为符号引用)转化为调用方法的直接引用;
补充:虚方法与非虚方法
  • 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法;
  • 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法,其它为虚方法
4 方法返回地址(或方法正常退出或者异常退出的定义)
  • 存放调用该方法的pc寄存器的值;
5 一些附加信息
  • 包括java虚拟机实现相关的附加信息;

6.本地方法接口

  • 使用native修饰的方法即为本地方法,本地方法有方法体,但不是使用java语言,而是c或c++

7.本地方法栈

  • java虚拟机栈用于管理java方法的调用,而本地方法栈用于管理本地方法的调用
  • 与虚拟机栈相同,当分配栈容量大于最大容量,抛出stackOverFlowError;当申请扩展容量实际不足分配时抛出OutOfMemoryError异常;
  • 当某个线程调用一个本地方法时,其就进入了不再受虚拟机限制的世界,与虚拟机拥有相同的权限;
  • 并非所有的虚拟机都支持本地方法。

8.堆

核心概述

image-20200617084314930

  • 堆每个进程一份

  • 堆区在jvm启动的时候即被创建,其空间大小也就确定了,是jvm管理的最大一块内存空间(堆内存大小是可调节的);

  • 堆在物理上可以不连续,但在逻辑上是连续的;

  • 堆每个进程一份,堆内可以划分为线程私有的缓冲区(TLAB)

  • 所有的对象实例及数组都应当运行时分配在堆上;

  • 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,该引用指向对象或者数组在堆中的位置;

  • 在方法结束后,堆中的对象不会马上被移除,当发生垃圾回收(GC)时才会被溢出

  • 堆是GC执行垃圾回收的重点

  • 内存细分

image-20200617091617889

    • jdk7及以前:新生区(eden+(from+to)Survivor0+Survivor1)、养老区、永久区
    • jdk8及以后:新生区(eden+(from+to)Survivor0+Survivor1)、养老区、元空间
运行原理
#配置新生代与老年代在堆结构的占比
默认-XX:NewRatio=2 : 表示新生代占1,老年代占2,新生代占整个堆的1/3
可修改-XX:NewRatio=4 : 表示新生代占1,老年代占4,新生代占整个堆的1/5
-Xmn : 设置新生代的空间的大小
-XX:MaxTenuringTreshole=<N> : 设置最大晋升到老年区的阈值
  • 若实例对象的生命周期较短,则存放于新生区,否则老年区
  • Eden空间:幸存一区:幸存三区:8:1:1(需-XX:SurivorRatio:设置)
  • 绝大部分的java对象的销毁都在新生区进行;
  • 幸存区不会单独进行GC,只有当Eden满的时候进行YGC,幸存区才会发生垃圾回收

image-20200617093720370

image-20200617094224108

Minor GC、Major GC、Full GC
部分收集
  • 新生代收集(Minor GC/Young GC):只是新生代(Eden\S0,S1)的垃圾回收,Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行;
  • 老年代收集(Magor GC/Old GC):只是老年代的垃圾回收
    • 目前只有CMS GC会有单独收集老年的行为
    • !!!注意:很多时候存在Major GC会和Full GC的混淆使用,需要具体分辨是老年代回收还是整堆回收,即Magor GC不等于Full GC;
  • 混合收集(Mixed GC):整个新生代以及部分老年代的垃圾回收;
整堆收集(Full GC):整个java堆和方法区的垃圾回收。
  • 触发条件:
    • 调用System.gc()时,系统建议执行Full GC,但是不必然执行;
    • 老年代空间不足或方法区空间不足;
    • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
    • 由Eden区,from向to区复制时,对象大小大于To区的可用内存,则把该对象转到老年区,但老年区的可用内存小于该对象大小时。
内存分配策略
  • 优先分配Eden
  • 大对象直接分配到老年代,尽量避免程序中出现过多的大对象
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断
    • 如果Survior区中的相同年龄的所有对象大小的总和大于Survior空间的一半,年龄大于或等于该年龄的对象可用直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
  • 空间分配担保-XX:HandlePromotionFailue
TLAB(Thread Local Allocation Butter)
TLAB是每个线程独有的,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域。

image-20200617102904374

堆空间的参数设置
-XX:+PrintFlagsInitial:查看所有的参数的默认初始值
-XX:+PrintFlagsFinial:查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms:初始堆空间内存(默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小
-XX:NewRatio:配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中Eden和s0/s1空间的比例
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的GC处理日志
	打印简要信息:1- XX:+PrintGC  2- -verbose:gc
-XX:HandlePromotionFailure:是否设置空间担保
逃逸分析(待理解)
查看new的对象是否可能在方法外被调用,如果一个对象并没有逃逸处方法的话,就可能被优化成栈上分配,无需进行垃圾回收。

9.方法区

image-20200617113002832

image-20200617110833177
栈、堆、方法区的交互关系

image-20200617110955687

方法区的理解
  • 方法区看作是一块独立于java堆的内存空间;
  • 方法区与java堆一样,是各个线程共享的内存区域;
  • 关闭jvm即释放方法区;
  • 方法区的大小决定了系统可用保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:PermGen space(jdk以前)/ava.lang.OutOfMemoryError:Metaspace(jdk8及以后)
设置方法区容量
jdk7及之前
  • 通过-XX:PermSize来设置永久代初始分配空间,默认值是20.75M;
  • -XX:MaxPermSize来设置永久代最大可分配空间,32位机器默认是64M,64位机器默认是82M;
jdk8及以后
  • 通过**-XX:MetaspaceSize和-XX:MaxMetaSpaceSize设置**,默认值分别位21M及-1,即最大没有限制;
  • 初始值21M即为初始的高水平位线,一旦触及这个水位线,Full GC将会被触发,然后该水位线将为被重置,新的线将取决于GC释放了多少元空间,如果释放的空间不足,那么在不超过MaxMetaspanceSize时,适当提高该线,如果释放空间过多,则适当降低该值
存储种类

主要用于存储被虚拟机加载的运行时常量池、域信息、方法信息类型信息、常量、静态变量、即时编译器编译后的代码缓存等

运行时常量池
  • 字节码文件内的常量池加载到方法区即变为运行时常量池
  • 常量池:一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用
  • 运行时常量池相对于class文件常量池的另一重要特性是具备动态性;
方法区的演进细节(面试)
  • jdk1.6及之前:有永久代,静态变量存放在永久代上;
  • jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中;
  • jdk1.8及之后:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆。
永久代为什么要被元空间替换?
  • 官网解释:在融合JRockit和Hotspot两个虚拟机时,JRockit没有永久代;
  • 为永久代设置空间大小是很难确定的;
  • 对永久代进行调优是很困难的。
方法区的垃圾回收
  • 主要包括两部分:常量池中废弃的常量和不再使用的类型

image-20200617155153401

10.直接内存

简介
  • 直接内存不属于运行时数据区;

  • 其是java堆外的,直接向系统申请的内存空间;

  • 访问直接内存的速度会优于java堆,即读写性能高,读写频繁更适合采用直接内存;

  • 来源于NIO,可通过存在堆中的DirectBytrBuffer操作native内存容量;

  • 直接内存也可产生OOM;

  • 直接内存可通过MaxDirectMemorySize设置容量,如果不指定,默认与堆的最大值-Xmx参数值一致。

11.执行引擎

image-20200618082245769

概述
  • 虚拟机的执行引擎是由软件自行实现的,能够执行那些不被硬件直接支持的指令集格式;
  • 执行引擎的任务是将字节码指令解释/编译为对应平台上的本地机器指令(后端编译)。
java代码编译和执行的过程

image-20200618083242599

(其中绿色路径为解释器,而蓝色部分为即时编译器(JIT编译器))

解释器
  • 当java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件的内容“翻译”为对应平台的本地机器指令执行。
JIT编译器(即时编译器)
  • 指虚拟机将源代码直接编译成和本地机器平台相关的机器语言,运行速度快;
  • 在HotSpot中内嵌两个JIT编译器,分别为Client/c1和Server/c2,64位系统默认为server。
问题:为什么说java是半编译半解释型语言?
  • jvm在解释编译字节码文件时,既可以使用解释器也可以使用JIT编译器。

image-20200618084159704

image-20200618084732736

  • 解释器编译字节码文件效率较为低下,因此出现了即时编译器(JIT编译器)。
问题:为何不淘汰解释器
  • 当程序启动之后,解释器相较于JIT编译器反应速度快;
  • 随着程序运行时间加长,JIT逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令;
  • 解释执行在编译器进行激进优化不成立的时候,解释器可作为编译器的“逃生门”。
#HotSpot 设置程序执行方式
-Xint:完全采用解释器模式执行程序
-Xcomp:完全采用即时编译器模式执行程序,如果即时编译出现问题,解释器会介入执行
-Xmixed:采用解释器+即时编译器的混合模式共同执行程序
热点代码及探测方式
  • 根据代码被调用执行的频率(热点探测功能)可推出热点代码,JIT在运行时会针对热点代码做出深度优化;
  • 使用方法调用计数器或回边计数器统计代码被调用次数。

12.StringTable

  • String在jdk8以前内部定义了final char[] value用于存储字符串数据,jdk9时改为byte[];
  • String声明为final的,不可被继承;
  • String拥有不可变性
  • 字符串常量池是不会存储相同内容的字符串的;
  • 通过-XX:StringTableSize可设置StringTable的长度
    • jdk6中默认长度位1009,所以如果常量池中的字符串过多就会导致效率下降很快,设置长度没有要求;
    • jdk7中,StringTable的长度默认位60013,设置长度没有要求;
    • jdk8开始,设置StringTable长度时,1009为可设置的最小值。
String内存的分配
内存分配情况
  • jdk6以前,字符串常量池存放于永久代;
  • jdk7中,字符串常量池的位置调整到java堆;
  • jdk8永久代更改为元空间,字符串常量存放于堆。
String类型的常量池主要使用方式有两种
  • 直接使用双引号声明出来的String对象会直接存储在常量池中;
  • 如果不是用双引号声明的String对象,可以使用String提供的intern()方法。
问题一 new String(“ab”)会创建几个对象? 2
  • 对象一 new关键字在堆空间创建的
  • 对象二 字符串常量池中的对象"ab"
问题二 new String(“a”) + new String(“b”)创建了几个对象?
  • 对象一 new StringBuilder()
  • 对象二 new String(“a”)
  • 对象三 常量池中的"a"
  • 对象四 new String(“b”)
  • 对象五 常量池中的"b"
  • 深入解析 StringBuilder的toString()
    • 对象六 new String(“ab”)
    • !!注意 toString()的调用,在字符串常量池中,没有生成"ab"
/**
 * 如何保证变量s指向的是字符串常量池中的数据呢?
 * 有两种方式:
 * 方式一: String s = "shkstart";//字面量定义的方式
 * 方式二: 调用intern()
 *         String s = new String("shkstart").intern();
 *         String s = new StringBuilder("shkstart").toString().intern();
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  18:49
 */
public class StringIntern {
    public static void main(String[] args) {

        String s = new String("1");
        s.intern();//调用此方法之前,字符串常量池中已经存在了"1"
        String s2 = "1";
        System.out.println(s == s2);//jdk6:false   jdk7/8:false
        String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11")
        //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
        s3.intern();//在字符串常量池中生成"11"。如何理解:jdk6:创建了一个新的对象"11",也就有新的地址。
                                            //         jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String("11")的地址
        String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
        System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
    }


}
intern()的使用
  • jdk1.6中,将这个字符串对象尝试放入串中
    • 如果串池中有,则并不会放入,返回已有的串池中的对象的地址;
    • 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址;
  • jdk1.7起,将这个字符串对象尝试放入串池
    • 如果串池中有,则并不会放入,返回已有的串池中的对象的地址;
    • 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址。

13.垃圾回收概述

  • 指在运行程序中没有任何指针指向的对象,该对象就是需要被回收的垃圾;
  • 堆空间为垃圾回收的工作重点。
问题
问题一 为什么需要GC
  • 若不进行垃圾回收,内存迟早会被消耗完;
  • 进行GC,JVM可以将整理处的内存分配给新的对象;
  • 没有GC就不能保证应用程序的正常进行。

14.垃圾回收相关算法

标记阶段(当一个对象不再被任何的存活对象引用时就被标记)
  • 引用计数算法
    • 原理:一个对象只要有任何一个对象引用了A,则A的引用计数器+1,当引用失效-1,当达到0时,即被标记
    • 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
    • 缺点:增加了存储空间的开销;
    • 增加了时间开销;
    • 无法处理循环引用的情况,因此java没有选择该算法
  • 可达性分析算法/根搜索算法、追踪性垃圾收集
  • 基本思想
    • 以**根对象集合(GCRoots)**为起点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达;
    • 使用该算法后,内存的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链
    • 如果目标对象没有任何引用链相连,则是不可达的,就以为着该对象已经死亡,可以标记为垃圾对象;
    • 在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象。
问题一 在java语言中,GCRoots包括哪几类元素(重点)
  • 虚拟机栈中引用的对象;
    • 如各个线程被调用的方法中使用到的参数、局部变量等
  • 本地方法栈内本地方法引用的对象;
  • 方法区中类静态属性引用的对象;
    • 如java类的引用类型静态变量
  • 方法区中常量池引用的对象;
    • 如字符串常量池里的引用
  • 所有被同步锁synchronized持有的对象;
    • 反映java虚拟机内部情况的JMXBean、JVMTi中注册的回调、本地代码缓存等。
对象的finalization机制
简介
  • java语言提供了对象终止机制来允许开发人员提供对象被销毁之前的自定义处理逻辑;

  • 该方法在垃圾回收之前被调用;

  • 主要用于在对象被回收时进行资源释放。

  • 由于该机制的存在,虚拟机中的对象一般处于三种可能状态,一个根节点无法触及的节点有可能“复活”

    • 可触及的:从根节点开始,可以到达这个对象。
    • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活
    • 不可触及的:对象的finalize()被调用,并且没有复活,即被列为不可触及状态,(finalize方法只能被调用一次
清除阶段
  • 标记-清除算法(Mark-Sweep)
  • image-20200618163121109

  • 执行过程
    • 当堆中有效内存空间被耗尽时,就会停止整个程序(STW/stop the world),然后进行标记和清除;
    • 标记:Collector从引用根节点开始遍历,标志所有被应用的对象,一般是在对象的Header中记录为可达对象(可达对象为非垃圾);
    • 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
  • 缺点
    • 效率不算高;
    • 在进行GC的时候,需要停止整个应用程序,导致用户体验差;
    • 该方式清理出来的空闲内存是不连续的,产生内存碎片,需要维护一个空闲列表。
  • 优点
    • 简易
  • 何为清除
    • 该处的清除并非置空,而是把需要清除的对象地址保存在空闲的地址列表,下次有新对象需要加载时,判断垃圾的位置空间是否足够,若足够,就存放。
  • 复制算法
  • image-20200618163032166

  • 核心思想
    • 将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
  • 缺点
    • 所需要的内存空间需要两倍;
    • 对于G1这种分拆成大量region的GC,复制而不是移动,以为着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小;
  • 优点
    • 没有标记和清除过程,实现简单,运行高效;
    • 复制过去以后保证空间的连续性,不会出现“碎片”问题。
  • 标记-压缩算法
  • image-20200618164350483

  • 执行过程
    • 第一阶段从根节点开始标记所有被引用对象;
    • 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放,之后清理边界外所有的空间。
  • 优点
    • 消除了标记-清除算法当中,内存区域分散的缺点,当需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
    • 消除了复制算法当中,内存减半的高额代价。
  • 缺点
    • 效率低于复制算法;
    • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
    • 移动过程中,需要全程暂停用户应用程序,即STW
分代收集算法
  • 原理:不同声明周期的对象可以采取不同的收集方式,以便提供回收效率,例如根据新生代和老年代使用不同的回收算法。
  • 分代
    • 年轻代:区域相对老年代小,对象生命周期短,存活率低、回收频繁,适合采用复制算法;
    • 老年代:区域较大,生命周期长,存活率高,回收频率低模式和采用标志-清除标记-回收算法。
增量收集算法(时间优化)
  • 原理:垃圾收集线程和应用程序线程交替执行,通过堆线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
  • 缺点:造成系统吞吐量的下降。
分区算法(空间优化)
  • 原理:将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个对空间,从而减少一次GC所产生的停顿。

15.垃圾回收相关概念

System.gc()
  • 显示触发Full GC,然而该方法无法保证对垃圾收集器的调用(不一定执行)
内存溢出(OOM)与内存泄露
内存溢出OutOfMemoryError:没有空闲内存,并且垃圾收集器也无法提供更多内存(在OOM之前会进行一次Full GC,若分配的对象容量超大,则直接报OOM而不进行Full GC)
  • java虚拟机的堆内存设置不够;
  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集。
内存泄漏Memory Leak:只有对象不会再被程序用到了,但是GC又不能回收他们的情况才称为内存泄漏
案例
  • 单例模式:单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生;
  • 一些提供close的资源未关闭导致内存泄漏:连接数据库连接(dataSource getConnection())网络连接(socket)和io连接必须手动close,否则是不能被回收的。
Stop The World(STW)
产生情况
  • 可达性分析算法中枚举根节点(GC Roots)会导致所有java执行线程停顿即STW;
  • 调用system.gc()会触发STW。
操作系统中的并行与并发
并行:错开运行(单处理器)

image-20200619084452461

并发:同时运行(多处理器)

image-20200619084532942

对比
  • 并发,指的是多个事情在同一时间段内同时发生;
  • 并发,指的是多个事情在同一时间点上同时发生。
垃圾回收中的并行与并发
并行
  • 多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态;
串行
  • 相较于并行的概念,单线程执行;
  • 如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收,回收完,再启动程序的线程。
并发
  • 用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。
安全点(Safe Point)
  • 程序执行时并非所有地方都能进行GC,只有特定位置才能GC,该位置即为安全点。
安全区域(Safe Region)
  • 安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。
引用
  • 强引用(StrongReference):最传统的引用定义,指在程序代码之中普遍存在的引用赋值,即类似”Object obj = new Object()"这种关系,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被应用的对象。
    • 强引用为默认的引用类型;

    • 强引用的对象是可触及的,垃圾收集器不会收集该对象;

    • 强引用是造成java内存泄漏的主要原因之一。

    • //设置对象为软引用
      Object obj = new Object();//声明强引用
      
      obj = null;//销毁强引用
      
  • 软引用(SoftReference):在系统将要发生内存溢出之前,将会把该对象列入回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
    • 软引用是软可触及的;

    • 软引用不会导致OOM,报OOM之前软引用就被GC了;

    • 当内存足够不会被GC,但内存不足则GC。

    • //设置对象为软引用
      //方法一
      Object obj = new Object();//声明强引用
      SoftReference<Object> sf = new SoftReference<Object>(obj);//设置为软引用
      obj = null;//销毁强引用
      
      //方法二
      SoftReference<User> userSoftRef = new SoftReference<User>(new User(1,"sss"));
      
  • 弱引用(WeakReference):被若引用关联的对象只能生存到下一次垃圾收集之前,当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
    • 弱引用与强引用的最大区别为,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对象弱引用对象,GC总是进行回收,若引用对象更容易、更快被GC回收。
    • //设置对象为弱引用
      //方法一
      Object obj = new Object();//声明强引用
      WeakReference<Object> sf = new WeakReference<Object>(obj);//设置为弱引用
      obj = null;//销毁强引用
      
      //方法二
      WeakReference<User> userSoftRef = new WeakReference<User>(new User(1,"sss"));
      
  • 虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成应用,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
    • 设置虚引用的意义为实现垃圾通知;

    • //设置对象为虚引用
      //方法一
      Object obj = new Object();//声明强引用
      ReferenceQueue phantomQueue = new ReferenceQueue();
      PhantomReference<Object> sf = new PhantomReference<Object,phantomQueue>(obj);//设置为虚引用
      obj = null;//销毁强引用
      
问题
  • 问题一 强引用、软引用、弱引用、虚引用有什么区别,具体使用场景?
  • 以上

16.垃圾回收器

image-20200619104243713

jdk10发布的ZGC有望替换hotSpot虚拟机,目前仍处于实验期
七种经典的垃圾收集器
  • 串行回收器:Serial、Serial Old
  • 并行回收器:ParNew、Parallel Scavenge、Parallel Old
  • 并行回收器:CMS G1
七种经典收集器与垃圾分代之间的关系
  • 新生代收集器:Serial、ParNew、Parallel Scavenge
  • 老年代收集器:Serial Old、Parallel Old、CMS;

  • 整堆收集器:G1。

  • image-20200619163326616

  • image-20200619105208357

垃圾收集器的组合关系

image-20200619105339195

  • 红线表示jdk8为显示过时,jdk9时被移除;

  • CMS GC在jdk9时被标志过时,jdk14时被移除;
  • 绿色连线表示jdk14被标志过时。
Serial回收器
  • Serial收集器采用赋值算法、串行回收和“Stop-the-World”机制的方式执行内存回收;

  • Serial同时提供了SerialOld收集器,其同样采用串行回收和STW机制,只不过内存回收算法使用的是标记-压缩算法;

  • 该垃圾回收器适合在单核情况下运行,不适合多核运行

    • Serial Old是运行在Client模式下默认的老年代的垃圾回收器;
    • Serial Old在Server模式下有两个用途
      • 与新生代的ParallelScavenge配合使用;
      • 作为老年代CMS收集器的后备垃圾收集方案。
  • 优势
    • 简单而高效;
    • 在用户的桌面应用场景中,可用内存一般不大,可以在较短时间内完成垃圾收集,只要不频繁发生,使用串行回收器是可以接受的。
  • 设置
    • 使用-XX:+UserSerialGC默认新生代用SerialGC老年代用SerialOldGC。
ParNew回收器
  • 采用并行回收,采用复制算法,”Stop-The-World“机制;
  • -XX:+UseParNewGC:手动设置使用ParNew,不影响老年代;
  • -XX:ParallelGcThreads:限制线程数量,默认开启和Cpu数据相同的线程数。
Parallel Scavenge回收器
  • 在jdk8默认使用Parallel及ParallelOld;

  • 采用并行回收,采用复制算法,”Stop-The-World“机制;

  • 和ParNew收集器不同,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量

  • 自适应调节策略也是Parallel与ParNew的一个重要区别;

  • Parallel 是一个吞吐量优先的垃圾回收器,因此其常在服务器环境中使用。

  • Parallel收集器在jdk1.6提供了老年代垃圾收集器Parallel Old用来替代Serial Old

    • Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和“Stop-the-World"机制。
  • 参数设置
    • -XX:+UseOarallelGc:指定年轻代使用Parallel;(互相激活
    • -XX:+UseParallelOldGc:指定老年代使用ParallelOld;(互相激活
    • -XX:ParallelGCThreads:设置年轻代并行收集器的线程数(线程数最好与CPU线程数量相同);
    • -XX:GCTimeRatio:垃圾收集时间占总时间的比例(=1/(N+1))用于衡量吞吐量的大小;
    • -XX:MaxGCPauseMillis:设置垃圾收集器最大停顿时间(即STW的时间),单位是毫秒;
    • -XX:+UseAdaptiveSizePolicy:设置Parallel Scavenge收集器具有自适应调节策略。
CMS回收器(Concurrent-Mark-Sweep)
  • 该款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,其第一次实现垃圾收集线程与用用户线程同时工作;

  • CMS的关注点是尽可能缩短垃圾收集时用户线程的停顿时间(低延迟);

  • 采用标记-清除算法+STW

  • 工作原理
  • image-20200619143507405

    • 初始标记:仅仅只是标记出GC Roots能直接关联到的对象,需要STW,但是速度非常快;
    • 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,该过程耗时长但不需要停顿用户线程;
    • 并发清理:修正并发标记阶段,因用户程序继续运作而导致标记变动的那一部分标记记录,需要STW
    • 重置线程:清理删除掉标记阶段判断已经死亡的对象,并发进行。
  • CMS由于是并发进行,因此并不能当内存不足时才进行垃圾回收,而是当堆内存达到使用率的一定阈值时,并触发垃圾回收,若阈值所剩内存不足用户线程进行,则报”Concurrent Mode Failure“,且临时切换成Serial Old收集器重新进行老年代垃圾回收,则变成串行。

  • 弊端
    • 由于采用标记-清除算法,因此会产生内存碎片;
    • CMS收集器堆CPU资源非常敏感;
    • CMS收集器无法处理浮动垃圾,浮动垃圾是指在并发标记阶段新产生的垃圾,浮动垃圾只有在二次垃圾回收才会被清理。
  • 参数设置
    • -XX:+UseConcMarkSweepGC:手动指定使用CMS收集器执行内存回收;
    • -XX:CMSInitiatingOccupanyFraction:设置堆内存使用率的阈值,一旦达到该阈值便开始垃圾回收;
    • -XX:+UseCMSCompactAtFullCollection:用于指定在执行完FullGC后堆内存空间进行压缩整理,一次避免内存碎片的产生;
    • -XX:CMSFullGCsBeforeCompaction:设置在执行多少次FullGC后对内存空间进行压缩整理;
    • -XX:ParallelCMSThreads:设置CMS的线程数量。
  • JDK后续版本中CMS的变化
    • JDK9新特性:CMS被标记为Deprecate;
    • JDK14新特性:删除CMS垃圾回收器,若指定使用CMS则会发出警告,JVM会自动回退以默认GC方式启动。
G1回收器(区域化分代式)
  • 官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起”全功能收集器“的重任与期望;

  • 并行回收器,其讲堆分割为不同的区域,使用不同的Region来表示Eden、S1、S2、老年代等,根据垃圾堆积与空间之间的价值大小,优先回收价值最大的Region;

  • 主要面向服务端应用的垃圾收集器,主要针对配置多核即大容量的机器;

  • 优势
    • 并行与并发
      • 在回收期间可以多个GC线程同时工作;
      • G1拥有与应用程序交替执行的能力,部分工作和应用程序同时执行。
    • 分代收集
      • 分代上看,G1属于分代型垃圾回收器,其会区分年轻代和老年代,年轻代依然有Eden区和Survivor区;但从堆结构上看,其要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量;
      • 将堆空间分为若干个区域(Region),这些区域包含了逻辑上的年轻代和老年代,G1还有另外的Humongous内存区域专门用于存放大对象,如果一个对象超过1.5Region则放入H区,若H区仍放不下,则使用连续H区
      • 其同时兼顾了年轻代和老年代
    • 空间整合
      • Region之间是复制算法,但整体上是标记-压缩算法,着两种算法都能避免内存碎片。
    • 可预测的停顿时间模型
      • 能让用户明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过M毫秒。
  • 参数设置
  • -XX:+UseG1GC:手动指定使用G1收集器执行内存回收任务;

  • -XX:G1HeapRegionSize:设置每个Region的大小,默认为1、2、4、8、16、32Mb;

  • -XX:MaxGCPauseMillis:设置期望达到的最大GC停顿时间,默认200ms;

  • -XX:ParallelGCThread:设置STW工作线程数的值,最大设置为8;

  • -XX:InitatingHeapOccupancyPercent:设置触发并发GC周期的Java堆占用率阈值,默认45。

  • 使用场景
    • 面向服务端应用,针对具有大内存、多处理器的机器;、
    • 超过百分之50的java堆被活动数据占用;
    • 对象分配频率或年代提升频率变化很大;
    • GC停顿时间过长(长0.5至1秒)。
  • RememberSet(记忆集)
  • image-20200619160253771

  • 由于G1使用Region分区域进行垃圾回收,例如当Eden区的对象直接或间接引用到其他堆的对象,则需要遍历其他GCRoot,则这会大大降低GC效率,因此引入了RememberSet;

  • 每一个Region都设有记忆集,用来记录其他Region引用到该Region的对象,当该Region进行GC时,扫描记忆集是否有其他region的引用。

  • G1回收器垃圾回收过程
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l5xWE9md-1593657602233)(https://gulimall-ghyang.oss-cn-shenzhen.aliyuncs.com/img/image-20200619161528268.png)]

  • YoungGC
  • image-20200619161514717

    • 年轻代的Eden区用尽时开始回收,并行的独占式收集,触发STW,将存活对象以到Survivor或老年区(YoungGC回收只会回收Eden区和Survior区 );

    • 具体回收步骤
      • 扫描根–更新RSet–处理RSet–复制对象–处理引用
  • Concurrent Marking(老年代并发标记过程)
  • image-20200619161528268

    • 当内存达到一定值(默认百分之45)开始触发;
    • 具体回收步骤
      • 初始标记阶段–根区域扫描–并发标记–再次标记–独占清理–并发清理阶段
  • MixedGC
    • 老年代并发标记过程标记后开始混合回收,移动从老年区移动存活对象到空闲区间,一次只需要扫描/回收一小部分Region即可,混合回收伴随着回收整个Young Region。
  • Full GC(失败保护机制强力回收)
  • 触发条件
    • Evacuation的适合没有足够的to-space来存放晋升的对象;
    • 并发处理过程完成之前空间耗尽。
性能指标
吞吐量
  • 运行用户代码的时间占总运行时间的比例
暂停时间
  • 执行垃圾收集时,程序的工作线程被暂停的时间
内存占用
  • java堆区所占的内存大小
选择方式
  • 优先调整堆的容量让jvm自适应;
  • 若内存小于100M,使用串行;
  • 若单核、单机程序,切没有停顿要求,串行回收器;
  • 若多核、需高吞吐、运行停顿超过1s,选择并行或者JVM自行选择;
  • 若多喝、最求低停顿、低延迟,使用并发回收器;
  • 官方推荐G1,性能高,目前多数项目使用G1
GC日志分析
  • -XX:+PrintGc:输出GC日志,类似-verbose:gc;
  • -XX:+PrintGCDetails:输出GC的详细日志;
  • -XX:+PrintGCTimeStamps:输出的时间戳;
  • -XX:+PrintGCDateStamps:输出GC的时间戳;
  • -XX:+PrintHeapAtGC:在进行GC的前后打印出堆的信息;
  • -Xloggc:…/logs/gc.log:日志文件的输出路径。
  • 使用Serial显示为DefNew、使用ParNew显示为ParNew、使用Parallel显示PSYoung、使用G1显示”garbage-first heap“
  • image-20200619165913338
面试问题
  • 垃圾收集的算法有哪些?如何判断一个对象是否可以护手?
  • 垃圾收集器的基本流程?

17.相关命令

#1-设置堆空间大小
-Xms:用来设置对空间(年轻代+老年代)的初始内存大小
#-X 是jvm的运行参数 ms是memory start
-Xmx:用来设置堆空间(年轻代+老年代)的最大内存大小
#2-关闭自适应的内存分配策略
-XX:-UseAdaptiveSizePolicy

18.对象的实例化内存布局与访问定位

对象的实例化

image-20200617155704408

执行init方法进行初始化:显示初始化+代码块中的初始化+构造器中的初始化
对象内存布局

image-20200617165657533

对象头
  • 运行时元数据
  • 类型指针
  • 说明:如果是数组,还包括数组的长度

image-20200617170010779

对象的访问定位

image-20200617170326709

image-20200617170344176

句柄访问

image-20200617170439433

优点:reference存储稳定句柄地址,对象被移动(垃圾回收时移动对象很不变)时只会改变句柄中实例数据指针即可,reference本身不需要被修改。
直接指针(HotSpot采用)

image-20200617170520649

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值