jvm(动力节点)

探讨了Java虚拟机(JVM)中类的加载时机、加载过程、初始化顺序及类加载器种类。分析了JVM内存结构,包括年轻代、老年代、元空间的特点与垃圾回收策略,特别是G1收集器的工作原理。同时涉及直接内存使用与潜在的溢出问题。
摘要由CSDN通过智能技术生成

面试必备JVM面试题解惑

主讲:Cat 老师

北京动力节点教育科技有限公司
2021 • 北京

动力节点 • 版权所有

1.Java运行时一个类是什么时候被加载的?

一个类在什么时候开始被加载,《Java虚拟机规范》中并没有进行强制约束,交给了虚拟机自己去自由实现,HotSpot虚拟机是按需加载,在需要用到该类的时候加载这个类;

1、Sun公司最早的 Classic虚拟机;

2、Sun/Oracle公司的HotSpot虚拟机;1

3、BEA公司的JRockit虚拟机;

4、IBM公司的IBM J9虚拟机;

官方:https://docs.oracle.com/javase/8/

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

-XX:+TraceClassLoading

2.JVM一个类的加载过程?

一个类从加载到jvm内存,到从jvm内存卸载,它的整个生命周期会经历7个阶段:

1、加载(Loading)

2、验证(Verification)

3、准备(Preparation)

4、解析(Resolution)

5、初始化(Initialization)

6、使用(Using)

7、卸载(Unloading)

其中验证、准备、解析三个阶段统称为连接(Linking);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qV9nDQej-1676628015822)(jvm面试题(动力节点)].assets/image-20221117094007957.png)

加载:classpath、jar包、网络、某个磁盘位置下的类的class二进制字节流读进来,在内存中生成一个代表这个类的java.lang.Class对象放入元空间(方法区),此阶段我们程序员可以干预,我们可以自定义类加载器来实现类的加载;

**验证:**验证Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证虚拟机的安全

准备:类变量赋默认初始值,int为0,long为0L,boolean为false,引用类型为null;常量赋正式值

**解析:**把符号引用翻译为直接引用

**初始化:**当我们new一个类的对象,访问一个类的静态属性,修改一个类的静态属性,调用一个类的静态方法,用反射API对一个类进行调用,初始化当前类,其父类也会被初始化…
那么这些都会触发类的初始化;

**使用:**使用这个类;

卸载:

1.该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;

2.加载该类的ClassLoader已经被GC;

3.该类的java.lang.Class
对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法;

3.一个类被初始化的过程?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YCNKMVPX-1676628015823)(jvm(动力节点)].assets/image-20221117150646464.png)

类的初始化阶段Java虚拟机才真正开始执行类中编写的Java程序代码;

进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,才真正初始化类变量和其他资源;

4.继承时父子类的初始化顺序是怎样的?

父类–静态变量

父类–静态初始化块

子类–静态变量

子类–静态初始化块

父类–变量

父类–初始化块

父类–构造器

子类–变量

子类–初始化块

子类–构造器

4.究竟什么是类加载器?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SuDBe19J-1676628015823)(jvm(动力节点)].assets/image-20221117150646464.png)

在类"加载"阶段,通过一个类的全限定名来获取描述该类的二进制字节流的这个动作的"代码"被称为"类加载器"(Class
Loader),这个动作是可以自定义实现的;

5.JVM有哪些类加载器?

站在Java虚拟机的角度来看,只存在两种不同的类加载器:

1、启动类加载器(Bootstrap
ClassLoader),使用C++语言实现,是虚拟机自身的一部分;

2、其他所有的类加载器,由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader;

站在Java开发者的角度来看,自JDK
1.2开始,Java一直保持着三层类加载器架构;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PMxNKbvD-1676628015824)(jvm(动力节点)].assets/image-20221120151549059.png)

6.JVM中不同的类加载器加载哪些文件?

1、启动类加载器(Bootstrap ClassLoader):(根的类加载器)C++语言实现的

<JAVA_HOME>\jre==\lib\rt.jar,resources.jar、charsets.jar

被-Xbootclasspath参数所指定的路径中存放的类库;

2、扩展类加载器(Extension ClassLoader):

sun.misc.Launcher$ExtClassLoader,

<JAVA_HOME>\jre\lib\ext,

被java.ext.dirs系统变量所指定的路径中所有的类库;

3、应用程序类加载器(Application ClassLoader):系统的类加载器

sun.misc.Launcher$AppClassLoader

加载用户类路径**(ClassPath)上所有的类库**;

7.JVM三层类加载器之间的关系是继承吗?

类加载之间没有继承关系,但都有相同的父类。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HtBUzN9Z-1676628015825)(jvm(动力节点)].assets/image-20221120152026612.png)

8.JVM类加载的双亲委派模型吗?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rv1skEPx-1676628015825)(jvm(动力节点)].assets/image-20221120152223702.png)

双亲委派模型的工作过程是:

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,

而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,

因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当上一层类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到这个类)时,

下一层类加载器才会尝试自己去加载;

9.JDK为什么要设计双亲委派模型,有什么好处?

1、确保安全,避免Java核心类库被修改;

2、避免重复加载;

3、保证类的唯一性;

如果你写一个jaa.lang.String的类去运行,发现会抛出如下异常;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BgiIJjaf-1676628015825)(jvm(动力节点)].assets/image-20221120152415001.png)

10.可以打破JVM双亲委派模型吗?如何打破JVM双亲委派模型?

可以;

想要打破这种模型,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可;

11.如何自定义自己的类加载器?

1、继承ClassLoader

2、覆盖findClass(String name)方法 或者 loadClass() 方法;

findClass(String name)方法 不会打破双亲委派;

loadClass() 方法 可以打破双亲委派;

12.ClassLoader中的loadClass()、findClass()、defineClass()区别?

loadClass()
就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中;

findClass() 根据名称位置加载.class字节码

definclass() 把字节码转化为java.lang.Class(大Class对象);

1、当我们想要自定义一个类加载器的时候,并且想破坏双亲委派模型时,我们会重写loadClass()方法;

2、如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?可以可以重写findClass方法(),findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法,这个方法只抛出了一个异常,没有默认实现;

JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中;

所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass()中实现你自己的加载逻辑即可;

13.加载一个类采用Class.forName()方法和ClassLoader.loadClass()有什么区别?

Class.forName()得到的Class是已经初始化的
ClassLoader.loadClass()得到的Class是还没初始化的

14你了解Tomcat 的类加载机制吗?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JBoCLXpG-1676628015826)(.//media/image7.jpeg)]{width=“4.303472222222222in”
height=“3.975in”}

可以看到,在原来的Java的类加载机制基础上,Tomcat新增了3个基础类加载器和每个Web应用的类加载器+JSP类加载器;

3个基础类加载器在 conf/catalina.properties 中进行配置:

common.loader=“${catalina.base}/lib”,“${catalina.base}/lib/*.jar”,“${catalina.home}/lib”,“${catalina.home}/lib/*.jar”
server.loader=
shared.loader=

Tomcat自定义了WebAppClassLoader类加载器,打破了双亲委派的机制,即如果收到类加载的请求,首先会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载Web应用自己定义的类,我们知道ClassLoader默认的loadClass方法是以双亲委派的模型进行加载类的,那么Tomcat打破了这个规则,重写了loadClass方法,我们可以看到WebAppClassLoader类中重写了loadClass方法;

15.为什么Tomcat要破坏双亲委派模型?

Tomcat是web容器,那么一个web容器可能需要部署多个应用程序;

1、部署在同一个Tomcat上的两个Web应用所使用的Java类库要相互隔离

2、部署在同一个Tomcat上的两个Web应用所使用的Java类库要互相共享;

3、保证Tomcat服务器自身的安全不受部署的Web应用程序影响

4、需要支持JSP页面的热部署和热加载;

16.有没有听说过热加载和热部署,如何自己实现一个热加载?

热加载
是指可以在不重启服务的情况下让更改的代码生效,热加载可以显著的提升开发以及调试的效率,它是基于Java的类加载器实现的,但是由于热加载的不安全性,一般不会用于正式的生产环境;

热部署
是指可以在不重启服务的情况下重新部署整个项目,比如Tomcat热部署就是在程序运行时,如果我们修改了War包中的内容,那么Tomcat就会删除之前的War包解压的文件夹,重新解压新的War包生成新的文件夹;

1、热加载是在运行时重新加载class,后台会启动一个线程不断检测你的class是否发生改变;

2、热部署是在运行时重新部署整个项目,耗时相对较高;

如何实现热加载呢?

在程序代码更改且重新编译后,让运行的进程可以实时获取到新编译后的class文件,然后重新进行加载;

1、实现自己的类加载器;

2、从自己的类加载器中加载要热加载的类;

3、不断轮训要热加载的类class文件是否有更新,如果有更新,重新加载;

17.Java代码到底是如何运行起来的?

1、Mall.java -->javac --> Mall.class --> java Mall
(jvm进程,也就是一个jvm虚拟机)

2、Mall.java -->javac–>Mall.class -->Mall.jar --> java -jar
Mall.jar

3、Mall.java --> javac --> Mall.class -->Mall.war --> Tomcat
–> startup.sh --> org.apache.catalina.startup.Bootstrap
(jvm进程,也就是一个jvm虚拟机)

其实运行起来一个Java程序,都是通过D:\dev\Java\jdk1.8.0_251\bin\java、启动一个JVM虚拟机,在虚拟机里面运行Mall.class字节码文件;

18.来,画一下JVM整个运行原理图?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OrbLTdMn-1676628015826)(jvm(动力节点)].assets/image-20230131111658963.png)

19.请介绍一下JVM的内存结构划分?

加载进来的.class字节码文件、代码执行创建的对象、代码执行调用方法,方法中有变量等数据需要一个地方存放,所以JVM划分出了几个区域,用于存放这些信息;hotspot

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x6InBQQD-1676628015827)(jvm(动力节点)].assets/image-20230131111717253.png)

在JDK1.8之前,元空间就是原来的**方法区(永久代)**;

20.JVM哪些区域是线程私有的,哪些区域是线程共享的?

1、堆、元空间(方法区)是线程共享的;

2、其他区域是线程私有的;

21.从JVM角度剖析如下程序代码如何执行?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U03fxGLz-1676628015827)(jvm(动力节点)].assets/image-20230131111802192.png)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ErHltiPn-1676628015828)(jvm(动力节点)].assets/image-20230131111815739.png)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wnGnOaBN-1676628015828)(jvm(动力节点)].assets/image-20230131111826270.png)

22.JVM运行时数据区 程序计数器 的特点及作用?

1、程序计数器是一块较小的内存空间,几乎可以忽略;

2、是当前线程所执行的字节码的行号指示器;

3、Java多线程执行时,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响;

4、该区域是"线程私有"的内存,每个线程独立存储;

5、该区域不存在OutOfMemoryError;

6、无GC回收;

23.JVM运行时数据区 虚拟机栈的特点及作用?

1、线程私有;

2、方法执行会创建栈帧,存储局部变量表等信息;

3、方法执行入虚拟机栈,方法执行完出虚拟机栈;(先进后出)

4、栈深度大于虚拟机所允许StackOverflowError

5、栈需扩展而无法申请空间OutOfMemoryError(比较少见);hotspot虚拟机没有

6、栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值、对象值等)都存放到堆上的;

7、栈一般都不设置大小,栈所占的空间其实很小,可以通过-Xss1M进行设置,如果不设置默认为1M

8、随线程而生,随线程而灭;

9、该区域不会有GC回收;

24.JVM运行时数据区 本地方法栈的特点及作用?

1、与虚拟机栈基本类似;

2、区别在于本地方法栈为Native方法服务

3、HotSpot虚拟机将虚拟机栈和本地方法栈合并

4、有StackOverflowError和OutOfMemoryError(比较少见);

5、随线程而生,随线程而灭;

6、GC不会回收该区域;

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;

25.JVM运行时数据区 Java堆的特点及作用?

  1. 线程共享的一块区域;

2、虚拟机启动时创建;

3、虚拟机所管理的内存中最大的一块区域;

4、存放所有实例对象或数组;

5、GC垃圾收集器的主要管理区域;

6、可分为新生代、老年代;

7、新生代更细化可分为Eden、From Survivor、To Survivor,Eden:Survivor =
8:1:1

8、可通过-Xmx、-Xms调节堆大小;

9、无法再扩展java.lang.OutOfMemoryError: Java heap space

10、如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread
Local Allocation Buffer,TLAB),以提升对象分配时的效率;

26.JVM中对象如何在堆内存分配?

1、指针碰撞(Bump The Pointer):内存规整的情况下;

2、空闲列表(Free List):内存不规整的情况下;

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定;

因此,当使用Serial、 ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效

而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存;

3、==本地线程分配缓冲(==Thread Local Allocation
Buffer,TLAB):对象创建在虚拟机中频繁发生,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况;

那么解决方案有两种:

(1)同步锁定,JVM是采用CAS配上失败重试的方式保证更新操作的原子性;

(2)线程隔离,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread
Local Allocation
Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定;

-XX:TLABSize=512k 设置大小;

27.JVM堆内存中的对象布局?

在 HotSpot
虚拟机中,一个对象的存储结构分为3块区域:对象头(Header)实例数据(Instance
Data)对齐填充(Padding)

对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit,官方称为
‘Mark Word’;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dg2ACRhl-1676628015829)(.//media/image8.png)]{width=“6.541666666666667in”
height=“1.6145833333333333in”}

第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是Java数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过
Java 对象元数据确定大小,而数组对象不可以;

实例数据(Instance
Data):程序代码中所定义的**各种成员变量类型的字段内容(**包含父类继承下来的和子类中定义的);

对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍,HotSpot虚拟机,任何对象的大小都是8字节的整数倍;

28.JVM什么情况下会发生堆内存溢出?

Java堆中用于储存对象,只要不断地创建对象,并且保持**GC
Roots**到对象之间有可达路径

来避免垃圾回收机制清理这些对象,那么随着对象数量的增加,总容量达到最大堆的容量限制后就会产生内存溢出;

MAT工具分析xxx.hprof文件,排查溢出的原因;

-Xms3072M

-Xmx3072M

-Xmn1536M

-Xss1M

-XX:-UseCompressedClassPointers

-XX:MetaspaceSize=256M

-XX:MaxMetaspaceSize=256M

-XX:SurvivorRatio=8

-XX:MaxTenuringThreshold=5

-XX:PretenureSizeThreshold=1M

-XX:+PrintGCDetails

-XX:+PrintGCDateStamps

-Xloggc:d:/dev/gc.log

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=d:/dev/heapdump.hprof

28.JVM如何判断对象可以被回收?

在JVM堆里面存放着所有的Java对象,垃圾收集器在对堆进行回收前,首先要确定这些对象之中哪些还"存活"着,哪些已经"死去";

Java通过 ==可达性分析(Reachability Analysis)==算法 来判定对象是否存活的;

该算法的基本思路:通过一系列称为"GC
Roots"的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为"引用链"(Reference
Chain),如果某个对象到GC
Roots间没有任何引用链相连(也称为不可达),则证明此对象是不可能再被使用的对象,就可以被垃圾回收器回收;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5fEiGSoo-1676628015829)(jvm(动力节点)].assets/image-20230131111906413.png)

对象object 5、object 6、object 7虽然有关联,但它们到GCRoots是不可达的,所以它们将会被判定为可回收的对象;

29.哪些对象可以作为GC Roots呢?

1、在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等所引用的对象;

2、堆中的类静态属性引用的对象

3、方法区/元空间中的常量引用的对象

4、在本地方法栈中JNI(即通常所说的**Native方法**)引用的对象;

5、Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如

NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器;

6、所有被同步锁(synchronized关键字)持有的对象;

7、反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等;

8、其他可能临时性加入的对象;

29.谈谈Java中不同的引用类型?

Java里有不同的引用类型,分别是强引用、软引用、弱引用 和 虚引用;

强引用:Object object = new Object();

软引用:SoftReference 内存充足时不回收,内存不足时则回收;

弱引用:WeakReference 不管内存是否充足,只要GC一运行就会回收该引用对象;

虚引用:PhantomReference这个其实暂时忽略也行,因为很少用,它形同虚设,就像没有引用一样,其作用就是该引用对象被GC回收时候触发一个系统通知,或者触发进一步的处理;

30.JVM堆内存分代模型?

JVM堆内存的分代模型:年轻代、老年代;

大部分对象朝生夕死,少数对象长期存活;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F88YLtPe-1676628015829)(jvm(动力节点)].assets/image-20230208092955077.png)

31.请介绍一下JVM堆中新生代的垃圾回收过程?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p9i5hhS7-1676628015830)(jvm(动力节点)].assets/image-20230208093005379.png)

JVM里垃圾回收针对的是 新生代,老年代,还有元空间/方法区(永久代),

不会针对方法的栈帧进行回收,方法一旦执行完毕,栈帧出栈,里面的局部变量直接就从内存里清理掉,也就是虚拟机栈不存在垃圾回收;

代码里创建出来的对象,一般就是两种:

1、一种是短期存活的,分配在Java堆内存之后,迅速使用完就会被垃圾回收;

2、一种是长期存活的,需要一直生存在Java堆内存里,让程序后续不停的去使用;

第一种短期存活的对象,是在Java堆内存的新生代里分配;

第二种长期存活的对象,通过在新生代S0区和S1区来回被垃圾回收15次后,进入Java堆内存的老年代中,这里的15次,我们也称为对象的年龄,即对象的年龄为15岁;

java -XX:+PrintFlagsFinal 打印jvm默认参数值;

32.JVM对象动态年龄判断是怎么回事?

虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold=15才能晋升老年代;

**结论–>****动态年龄判断:Survivor区的对象年龄从小到大进行累加,当累加到X年龄(某个年龄)时占用空间的总和大于50%(可以使用-XX:TargetSurvivorRatio=?
来设置保留多少空闲空间,默认值是50),那么比X年龄大的对象都会晋升到老年代;

1、Survivor区分布如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9vpePIx7-1676628015830)(jvm(动力节点)].assets/image-20230208093733151.png)

1-3岁总和小于50%

2、此时新生代GC后,有6%的对象进入Survivor区,Survivor区分布如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1SEl9g1g-1676628015830)(jvm(动力节点)].assets/image-20230208093741008.png)

这时从1岁加到4岁时,总和51%
大于50%,但此时没有大于四岁的对象,即没有对象晋升

3、又经过一次新生代GC后,有40%的对象进入Survivor区,Survivor区分布如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X0OEiRcp-1676628015831)(jvm(动力节点)].assets/image-20230208093749940.png)

Survivor区的对象年龄从小到大进行累加,当累加到 3
年龄时的总和大于50%,那么比3大的都会晋升到老年代,即4岁的20%、5岁的20%晋升到老年代;

33.什么是老年代空间分配担保机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fjLuDj5f-1676628015831)(jvm(动力节点)].assets/image-20230208093956471.png)

Eden:800m -->300m

S0:100m

S1:100m

老年代:1000m,剩350m、200m

新生代Minor
GC后剩余存活对象太多,无法放入Survivor区中,此时就必须将这些存活对象直接转移到老年代去,如果此时老年代空间也不够怎么办?

1、执行任何一次Minor
GC之前,JVM会先检查一下老年代可用内存空间,是否大于新生代所有对象的总大小,因为在极端情况下,可能新生代Minor
GC之后,新生代所有对象都需要存活,那就会造成新生代所有对象全部要进入老年代;

2、如果老年代的可用内存大于新生代所有对象总大小,此时就可以放心大胆的对新生代发起一次MinorGC,因为MinorGC之后即使所有对象都存活,Survivor区放不下了,也可以转移到老年代去;

3、如果执行Minor
GC之前,检测发现老年代的可用空间已经小于新生代的全部对象总大小,那么就会进行下一个判断,判断老年代的可用空间大小,是否大于之前每一次Minor
GC后进入老年代的对象的平均大小,如果判断发现老年代的内存大小,大于之前每一次MinorGC后进入老年代的对象的平均大小,那么就是说可以冒险尝试一下MinorGC,但是此时真的可能有风险,那就是MinorGC过后,剩余的存活对象的大小,大于Survivor空间的大小,也大于老年代可用空间的大小,老年代都放不下这些存活对象了,此时就会触发一次"FullGC";

所以老年代空间分配担保机制的目的?也是为了避免频繁进行Full GC

  1. 如果Full GC之后,老年代还是没有足够的空间存放MinorGC过后的剩余存活对象,那么此时就会导致"OOM"内存溢出 ;在JDK6的时候有一个参数-XX:+HandlePromotionFailure用于开启是否要进行空间担保;

34.什么情况下对象会进入老年代?

1、躲过15次GC之后进入老年代,可通过JVM参数"-XX:MaxTenuringThreshold"来设置年龄,默认为15岁;

2、动态对象年龄判断;

3、老年代空间担保机制(新生代放不下了);

4、大对象直接进入老年代;

大对象是指需要大量连续内存空间的Java对象,比如很长的字符串或者是很大的数组或者List集合,大对象在分配空间时,容易导致内存明明还有不少空间时就提前触发垃圾回收以获得足够的连续空间来存放它们,而当复制对象时,大对象又会引起高额的内存复制开销,为了避免新生代里出现那些大对象,然后屡次躲过GC而进行来回复制,此时JVM就直接把该大对象放入老年代,而不会经过新生代;

我们可以通过JVM参数"-XX:PretenureSizeThreshold"设置多大的对象直接进入老年代,该值为字节数,比如"1048576"字节就是1MB,该参数表示如果创建一个大于这个大小的对象,比如一个超大的数组或者List集合,此时就直接把该大对象放入老年代,而不会经过新生代;

-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,其他新生代垃圾收集器不支持该参数,如果必须使用此参数进行调优,可考虑
ParNew+CMS的收集器组合;

35.JVM运行时数据区 元空间的特点及作用?

1、在JDK1.8开始才出现元空间的概念,之前叫方法区/永久代;

2、元空间与Java堆类似,是线程共享的内存区域;

3、存储被加载的类信息、常量、静态变量、常量池、即时编译后的代码等数据;

4、元空间采用的是本地内存,本地内存有多少剩余空间,它就能扩展到多大空间,也可以设置元空间大小;

-XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m

5、元空间很少有GC垃圾收集,一般该区域回收条件苛刻,能回收的信息比较少,所以GC很少来回收;

6、元空间内存不足时,将抛出OutOfMemoryError;

36.JVM本机直接内存的特点及作用?

1、直接内存(Direct Memory)不属于JVM运行时数据区,是本机直接物理内存;

2、像在JDK 1.4中新加入了NIO(New
Input/Output)类,一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的
DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据;

3、可能导致OutOfMemoryError异常出现; netty

37.JVM本机直接内存溢出问题?

直接内存(Direct
Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,该参数表示设置新I/O(java.nio程序包)直接缓冲区分配的最大总大小(以字节为单位);默认情况下,大小设置为0,这意味着JVM自动为NIO直接缓冲区分配选择大小;

由直接内存导致的内存溢出,无法生成Heap
Dump文件,如果程序中直接或间接使用了NIO技术,那就可以重点考虑检查一下直接内存方面的原因;

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/dev/heapdump.hprof

38.说几个与JVM内存相关的核心参数?

-Xms Java堆内存的大小;

-Xmx Java堆内存的最大大小;

-Xmn Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小;

-XX:MetaspaceSize 元空间大小;

-XX:MaxMetaspaceSize 元空间最大大小;

-Xss 每个线程的栈内存大小;

-XX:SurvivorRatio=8 设置eden区 和survivor 区大小的比例,默认是8:1:1;

-XX:MaxTenuringThreshold=5 年龄阈值;

-XX:+UseConcMarkSweepGC 指定CMS垃圾收集器;

-XX:+UseG1GC 指定使用G1垃圾回收器

–查看默认的堆大小及默认的垃圾收集器

java -XX:+PrintCommandLineFlags -version

39.堆为什么要分成年轻代和老年代?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y9tGk7N6-1676628015832)(jvm(动力节点)].assets/image-20230208110910726.png)

因为年轻代和老年代不同的特点,需要采用不同的垃圾回收算法;

年轻代的对象,它的特点是创建之后很快就会被回收,所以需要用一种垃圾回收算法;

老年代的对象,它的特点是需要长期存活,所以需要另外一种垃圾回收算法 ;

所以需要分成两个区域来放不同的对象;

1、绝大多数对象都是朝生夕灭的;

如果一个区域中大多数对象都是朝生夕灭,那么把它们集中放在一起,每次回收时只关注如何保留少量存活对象,而不是去标记那些大量将要被回收的对象,就能以较低的代价回收到大量的空间;

2、熬过越多次垃圾收集的对象就越难以回收;

如果是需要长期存活的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用;

3、JVM划分出新生代、老年代之后,垃圾收集器可以每次只回收其中某一个或者某些部分的区域

,同时也有了"Minor GC"“Major GC”"Full GC"这样的回收类型的划分;

Minor GC/Young GC :新生代收集

Major GC/Old GC:老年代收集

**Full GC:**整堆收集,收集整个Java堆和元空间/方法区的垃圾收集;

**MixedGC:**混合收集,收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为;

4、针对不同的区域对象存亡特征采用不同的垃圾收集算法:

(1)复制算法

(2)标记-清除算法

(3)标记-整理算法

40.JVM堆的年轻代为什么要有两个Survivor区?

1、如果没有Survivor区会怎么样?

此时每触发一次Minor
GC,就会把Eden区的对象复制到老年代,这样当老年代满了之后会触发Major
Gc/Full GC(通常伴随着MinorGC),比较耗时,所以必须有Survivor区;

2、如果只有1个Survivor区会怎么样?

刚刚创建的对象在Eden中,一旦Eden满了,触发一次Minor
GC,Eden中存活的对象就会被移动到Survivor区,下一次Eden满了的时候,此时进行Minor
GC,Eden和Survivor各有一些存活对象,因为只有一个Survivor,所以Eden区第二次GC发现的存活对象也是放入唯一的一个Survivor区域中,但此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化问题,并且由于不连续的空间会导致再分配大对象的时候,由于没有连续的空间来分配,会导致提前垃圾回收;

如果将Survivor中的所有存活对象进行整理消除碎片,然后将所有的存活对象放入其中,这样做会降低效率;

如果把两个区域中的所有存活对象都复制转移到一个完全独立的空间中,也就是第二块Survivor中,这样就可以留出一块完全空着的Eden和Survivor了,下次GC的时候再重复这个流程,所以我们便要有两个Survivor区;

41.Eden区与Survivor区的空间大小比值为什么默认是8:1:1?

一个eden区 ,新生代对象出生的地方;

两个survivor区,一个用来保存上次新生代GC存活下来的对象还有一个空着,在新生代GC时把eden+survivor中存活对象复制到这个空的survivor中;

统计和经验表明,90%的对象朝生夕死存活时间极短,每次gc会有90%对象被回收,剩下的10%要预留一个survivor空间去保存

42.请介绍下JVM中的垃圾回收算法?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q5wj8kSZ-1676628015832)(jvm(动力节点)].assets/image-20230208113830755.png)

标记-清除算法

标记-清除算法是最基础的收集算法,后续的很多垃圾回收算法是基于该算法而发展出来的,它分为’
标记 ‘和’ 清除 '两个阶段;

1、标记

标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记出所有存活的对象,在标记完成后,统一回收所有未被标记的对象,标记过程就是对象是否属于垃圾的判定过程,基于可达性分析算法判断对象是否可以回收;

2、清除

标记后,对所有被标记的对象进行回收;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fp5VxbMR-1676628015832)(.//media/image5.png)]{width=“4.9319444444444445in”
height=“2.345833333333333in”}

优点:基于最基础的可达性分析算法,实现简单,后续的收集算法都是基于这种思想实现的;

缺点:

  1. 执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;

    2、内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集;

复制算法

复制算法是标记-复制算法的简称,将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AuNo8LvI-1676628015833)(.//media/image6.png)]{width=“4.822916666666667in”
height=“2.2916666666666665in”}

优点:实现简单,效率高,解决了标记-清除算法导致的内存碎片问题;

缺点:

  1. 代价太大,将可分配内存缩小了一半,空间浪费太多了

  2. 对象存活率较高时就要进行较多的复制操作,效率将会降低

    一般虚拟机都会采用该算法来回收新生代,但是JVM对复制算法进行了改进,JVM并没有按照1:1的比例来划分新生代的内存空间,因为通过大量的统计和研究表明,90%以上的对象都是朝生夕死的,所以JVM把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor,发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间,HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有另外一个Survivor空间即10%的新生代会被"浪费";

    当然,90%的对象可被回收仅仅是大部分情况下,我们无法百分百保证每次回收都只有不多于10%的对象存活,因此JVM还有一个空间担保机制的安全设计,当Survivor空间不足以容纳一次Minor
    GC之后存活的对象时,就需要依赖其它内存区域(实际上就是老年代)进行空间分配担保(Handle
    Promotion,也就是冒险Minor GC一下);

标记-整理算法

标记-整理算法是根据老年代的特点而产生的

1、标记

标记过程与上面的标记-清理算法一致,也是基于可达性分析算法进行标记;

2、整理

和标记-清理不同的是,该算法不是针对可回收对象进行清理,而是根据存活对象进行整理,让存活对象都向一端移动,然后直接清理掉边界以外的内存

而标记-清除算法不移动存活对象,导致有大量不连续空间,即内存碎片,而老年代这种每次回收都有大量存活对象的区域,移动存活对象并更新所有引用这些对象的引用,这是一种比较耗时的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,像这样的停顿我们也称为"Stop
The World"即STW;

但是即便是移动存活对象是耗时的操作,但是如果不这么做,那么在充满内存碎片的空间中分配对象,又影响了对象的分配和访问的效率,所以JVM权衡两者之后,还是采用了移动存活对象的方式,也就是对内存进行了整理;

另外像cms垃圾收集器平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间,所以像基于标记-清除算法的CMS收集器面临空间碎片过多时就会进行一次整理;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aD58mi1R-1676628015833)(.//media/image7.png)]{width=“4.75in”
height=“2.2604166666666665in”}

优点:

  1. 不会像复制算法那样划分两个区域,提高了空间利用率;

    2、不会产生不连续的内存碎片;

缺点:效率问题,除了像标记-清除算法的标记过程外,还多了一步整理过程,效率变低;

分代收集算法

现在一般虚拟机的垃圾收集都是采用" 分代收集 "算法;

根据对象存活周期的不同将内存划分为几块,一般把java堆分为新生代和老年代,JVM根据各个年代的特点采用不同的收集算法;

新生代中,每次进行垃圾回收都会发现大量对象死去,只有少量存活,因此采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;

老年代中,因为对象存活率较高,采用标记-清理、标记-整理算法来进行回收;

43.请介绍一下JVM垃圾收集器?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sPx9fIlD-1676628015833)(.//media/image8.jpeg)]{width=“4.41875in”
height=“3.9340277777777777in”}

如上图,一共有7种作用于不同分代的垃圾收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用,垃圾收集器所处区域表示它是属于新生代收集器还是老年代收集器;

新生代收集器:Serial、ParNew、Parallel Scavenge [ˈpærəlel]
[ˈskævɪndʒ]

老年代收集器:CMS、Serial Old、Parallel Old

整堆收集器: G1

垃圾收集器的最前沿成果:ZGC 和 Shenandoah

Serial收集器

新生代收集器,最早的收集器,单线程的,收集时需暂停用户线程的工作,所以有卡顿现象,效率不高,致使java语言的开发团队一直在改进垃圾收集器的算法和实现,但Serial收集器简单,不会有线程切换的开销,是Client模式下默认的垃圾收集器,-client,
-server;

参数: -XX:+UseSerialGC

java -XX:+PrintFlagsFinal -version 打印jvm默认的参数值;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9nEeQ9S3-1676628015834)(.//media/image9.png)]{width=“4.239583333333333in”
height=“1.3854166666666667in”}

ParNew收集器

它是新生代收集器,就是Serial收集器的多线程版本,大部分基本一样,单CPU下,ParNew还需要切换线程,可能还不如Serial;

Serial和ParNew收集器可以配合CMS收集器,前者收集新生代,后者CMS收集老年代,

“-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代垃圾收集器;
“-XX:+UseParNewGC”:强制指定使用ParNew;
“-XX:ParallelGCThreads=2”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FKEWK56Z-1676628015834)(.//media/image10.png)]{width=“4.947916666666667in”
height=“1.9270833333333333in”}

Parallel Scavenge收集器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xL4AZsUQ-1676628015835)(.//media/image8.jpeg)]{width=“2.953472222222222in”
height=“2.629861111111111in”}

简称Parallel,它是新生代收集器,基于复制算法,并行的多线程收集器(与ParNew收集器类似),侧重于达到一个可控的吞吐量,虚拟机运行100分钟,垃圾收集花1分钟,则吞吐量为99%,有时候我们也把该垃圾收集器叫吞吐量垃圾收集器或者是吞吐量优先的垃圾收集器;而且这个垃圾收集器是jvm默认的新生代的垃圾收集器;

它提供一个参数设置吞吐量:

-XX:MaxGCPauseMillis
该参数设置大于0的毫秒数,每次GC的时间将尽量保持不超过设置的值,但是这个值也不是设置得越小就越好,GC暂停时间越短,那么GC的次数会变得更频繁;

-XX:+UseAdaptiveSizePolicy
自适应新生代大小策略,默认这个参数是开启的,当这个参数被开启之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间获得最大的吞吐量,这种调节方式称为垃圾收集的自适应的调节策略(GC
Ergonomics);

如果我们不知道怎么对jvm调优,我们可以使用ParallelScavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPauseMillis参数(最大停顿时间)给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成,自适应调节策略也是ParallelScavenge收集器区别于ParNew收集器的一个重要特性;

参数:-XX:+UseParallelGC 指定使用Parallel Scavenge垃圾收集器

java -XX:+PrintCommandLineFlags -version
打印jvm默认初始堆和最大堆大小以及垃圾收集器

java -XX:+PrintFlagsFinal -version 打印jvm所有的默认的参数值;

-XX:+PrintGCDetails

-XX:+PrintGCDateStamps

-Xloggc:d:/dev/gc.log

Parallel
Scavenge垃圾收集器中的Ergonomics负责自动的调节gc暂停时间和吞吐量之间的平衡,自动优化虚拟机的性能;

Serial Old收集器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KMuRKNnr-1676628015835)(.//media/image8.jpeg)]{width=“2.953472222222222in”
height=“2.629861111111111in”}

它是Serial收集器的老年代版本,同Serial一样,单线程==,可在Client模式下使用==,也可在Server模式下使用,采用标记-整理算法,Serial
Old收集器也可以作为CMS
收集器发生失败时的后备预案,在并发收集发生Concurrent Mode
Failure时使用;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q8S70Int-1676628015836)(.//media/image9.png)]{width=“4.239583333333333in”
height=“1.3854166666666667in”}

Parallel Old收集器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QWwrexS7-1676628015836)(.//media/image8.jpeg)]{width=“2.953472222222222in”
height=“2.629861111111111in”}

是ParallelScavenge的老年代版本,多线程,标记整理算法,它是在jdk1.6开始才提供

在注重吞吐量和CPU资源的情况下, Parallel Scavenge新生代+ Parallel
Old老年代是一个很好的搭配;

参数:-XX:+UseParallelOldGC 指定使用Parallel Old收集器;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-So3Z2fYT-1676628015836)(.//media/image11.png)]{width=“5.083333333333333in”
height=“1.9583333333333333in”}

CMS收集器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Axd6DiVY-1676628015837)(.//media/image8.jpeg)]{width=“3.1798611111111112in”
height=“2.83125in”}

CMS全称Concurrent MarkSweep,是一款老年代的垃圾收集器,它是追求最短回收停顿时间为目标的收集器,互联网B/S结构的服务器端特别适合此收集器;

我们知道垃圾回收会带来Stop theWorld(stw)的问题,会导致系统卡死时间过长,很多响应无法处理,所以CMS垃圾回收器采取的是垃圾回收线程和系统工作线程尽量同时执行的模式来处理的,基于标记-清除算法;

参数:-XX:+UseConcMarkSweepGC 指定使用CMS垃圾收集器;

-XX:+PrintGCDetails

-XX:+PrintGCDateStamps

-Xloggc:D:/dev/gc.log

CMS垃圾收集器的运作过程,分为4个阶段:

1、初始标记(stw,标记一下GCRoots能直接关联到的对象,那么这些对象也就是需要存活的对象,速度很快);

2、并发标记(不会stw,追踪GC Roots的整个链路,从GCRoots的直接关联对象开始遍历整个对象引用链路,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行)

3、重新标记(stw,修正并发标记期间,因用户程序继续运行而导致标记产生变化的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短,它其实就是对在第二阶段中被系统程序运行变动过的少数对象进行标记,所以运行速度很快的)

4、并发清除(不会stw,清理删除掉标记阶段判断的已经死亡的对象,这个阶段其实是很耗时的,但由于不需要移动存活对象,并且这个阶段也是与用户线程同时并发执行的)

其中初始标记和重新标记需要暂停用户线程(Stop TheWorld),其它阶段都是并发执行,所以总体上暂停时间更短;

CMS垃圾收集器的缺点:

1、并发收集会占用CPU资源,特别是cpu数量小的服务器下,会占用用户线程,导致性能下降,CMS默认启动的回收线程数是(处理器核心数量+3)/ 4;

2、会产生浮动垃圾,因为你并发清除的时候用户线程可能还在产生垃圾,这些垃圾没有清除,而且你不能让老年代填满了再清除,你要给用户线程留一定空间,所以jdk1.5默认是老年代68%了就触发回收,jdk1.6则提升到92%;

通过-XX:CMSInitiatingOccupancyFraction参数设置;

默认现在是取 -XX:CMSTriggerRatio 的值,默认是80%;

-XX:CMSInitiatingOccupancyFraction设置得更高,可以降低内存回收频率,获取更好的性能,但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次并发清除失败"并发失败"(Concurrent
Mode Failure),如果预留老年代不够用户线程使用,则启用Serial
Old收集,这就会暂停用户线程,导致性能下降;

3、CMS基于标记-清除算法,清理后会产生碎片空间,空间碎片过多时,将会导致大对象无法分配,往往会出现老年代还有很多剩余空间,但没有足够大的连续空间来分配当前对象,而不得不提前触发一次FullGC,CMS垃圾收集器有一个

-XX:+UseCMSCompactAtFullCollection开关参数(默认是开启的),用于在CMS收集器进行FullGC时开启内存碎片的整理过程,由于这个内存整理必须移动存活对象,整理过程是无法并发的,就导致停顿时间变长,因此虚拟机设计者们还提供了另外一个参数

-XX:CMSFullGCsBeforeCompaction,即CMS收集器在执行过若干次(数量由参数值决定)不整理空间的FullGC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full
GC时都进行碎片整理)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mZ3oqrJ7-1676628015837)(.//media/image12.png)]{width=“4.767361111111111in”
height=“1.7840277777777778in”}

java -XX:+PrintCommandLineFlags -version
打印jvm默认初始堆和最大堆大小以及垃圾收集器

java -XX:+PrintFlagsFinal -version 打印jvm所有的默认的参数值;

G1收集器

G1垃圾收集器的基本介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f9677tZJ-1676628015838)(.//media/image8.jpeg)]{width=“3.282638888888889in”
height=“2.9229166666666666in”}

G1全称GarbageFirst,G1垃圾回收器可以同时回收新生代和老年代,不需要两个垃圾回收器配合起来使用;

G1垃圾收集器是目前可用于生产环境的最前沿最先进的垃圾收集器,从JDK1.6u14开始试验,到JDK1.7u4达到成熟,直到JDK1.8u40才正式完成,开始可以使用;

JDK 9发布时,G1宣告取代Parallel Scavenge加Parallel
Old组合,成为服务端模式下的默认垃圾收集器,而CMS则被声明为不推荐使用(Deprecate)的收集器,如果对JDK9及以上版本的HotSpot虚拟机使用参数-XX:+UseConcMarkSweepGC来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃,CMS后续将退出历史舞台;

G1垃圾收集器的基本原理

G1是一款可以让我们设置垃圾回收的预期停顿时间的垃圾收集器,设置参数是-XX:MaxGCPauseMillis,默认值是200ms;

其实我们对内存合理分配,优化jvm参数,就是为了尽可能减少新生代(Minor
GC),或者是整个老年代(Major GC),或者是整个Java堆(Full
GC),尽量减少GC带来的系统停顿,避免影响系统处理请求,G1可以指定垃圾回收导致的系统停顿时间不能超过多久,不管垃圾的多与少,垃圾回收的时间都不要超过我们设置的值(并不是绝对的),G1全权给你负责,保证达到这个目标,这相当于我们就可以直接控制垃圾回收对系统性能的影响了;

所以G1垃圾收集器是尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象,这就是G1垃圾收集器的核心原理;

G1垃圾收集器如何做到可预测的停顿时间?

1、这与G1垃圾收集器独特的设计有关,它最大的特点就是把Java整个堆内存拆分为多个大小相等的Region [ˈridʒən] ;区域、分区

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l3ZavW1l-1676628015838)(.//media/image13.png)]{width=“6.766666666666667in”
height=“2.629861111111111in”}

2.G1它会追踪每个Region的回收价值,即它会计算每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾?

3、G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为垃圾回收的最小单元,即每次可以选择一部分Region进行收集,避免在整个Java堆中进行全区域的垃圾收集,让G1收集器去跟踪各个Region里面的垃圾的"回收价值",然后根据用户设定的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),然后在后台维护一个优先级列表,优先处理回收价值大的那些Region,这也是"GarbageFirst"名字的由来,这种使用Region划分堆内存空间,基于回收价值的回收方式,保证了G1收集器在有限的时间内尽可能收集更多的垃圾;

比如:G1通过追踪发现,1个Region中的垃圾对象有10MB,回收它需要耗费500毫秒,另一个Region中的垃圾对象有20MB,回收它需要耗费100毫秒,那么G1垃圾收集器基于回收价值计算会选择回收20MB只需要100毫秒的Region;

G1垃圾收集器是否还有年代的划分?

G1也有新生代和老年代的概念,但只不过是逻辑上的概念,也就是说一个Region此时是属于新生代的Eden空间,过一会儿可能就属于老年代空间,也就是一个Region在运行过程中动态地扮演着新生代的Eden空间、Survivor空间,或者老年代空间,每个Region并不是固定属于某一个空间,另外新生代、老年代也不一定是连续空间,可能是分开的;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KL7IPnMx-1676628015839)(.//media/image14.png)]{width=“4.893055555555556in”
height=“2.6729166666666666in”}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hDvb0tsZ-1676628015839)(.//media/image13.png)]{width=“6.766666666666667in”
height=“2.629861111111111in”}

刚开始Region是空的,可能谁都不属于,然后系统创建对象就分配给了新生代,这个Region被新生代对象放满之后,后续垃圾回收了这个Region,然后下一次同一个Region可能又被分配了老年代,用来放老年代的长时间需要存活的对象,所以Region随时会属于新生代也会属于老年代;

新生代和老年代各自的内存区域在不停地变动,由G1自动控制,也就是Region动态分配给新生代或者老年代,按需分配,然后触发垃圾回收的时候,可以根据设定的预期系统停顿时间,来选择最少回收时间和最多回收对象的Region进行垃圾回收,保证GC对系统停顿的影响在可控范围内,同时还能尽可能回收最多的对象;

G1垃圾收集器中的大对象?

Region中有一类特殊的Humongous [hjuːˈmʌŋgəs]区域,专门用来存储大对象;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8dX2QuHQ-1676628015839)(.//media/image13.png)]{width=“6.766666666666667in”
height=“2.629861111111111in”}

G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象,每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous
Region之中,G1的大多数行为都把HumongousRegion作为老年代的一部分来进行看待;

G1垃圾收集器内存大小如何设置?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iF7hBEWb-1676628015840)(.//media/image13.png)]{width=“6.766666666666667in”
height=“2.629861111111111in”}

每个region = 1m ~32m,最多有2048个region;

G1对应的是一大堆的Region内存区域,最多可以有2048个Region,比如说堆大小是4G(4096MB),那么每个Region的大小就是2MB,Region的取值范围是1M-32M,可以通过参数"-XX:G1HeapRegionSize"指定每个Region是多少兆;

比如说堆大小是4G(4096MB),刚开始,默认新生代对堆内存的占比是5%,也就是占据200MB左右的内存,对应大概是100个Region,可以通过"-XX:G1NewSizePercent"来设置新生代初始占比,一般默认值即可,因为在系统运行中,JVM会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过"-XX:G1MaxNewSizePercent"设置,并且一旦Region进行了垃圾回收,此时新生代的Region数量就会减少,这些都是动态的;

新生代 :老年代 = 60% :40%

G1垃圾收集器新生代还有Eden和Survivor吗?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NHWUbud5-1676628015841)(.//media/image13.png)]{width=“6.766666666666667in”
height=“2.629861111111111in”}

G1垃圾收集器依然有新生代、老年代的概念,新生代里依然有Eden和Survivor的划分,G1是从CMS发展过来的,以后是要完全取代CMS垃圾收取器的,从jdk9开始G1已经是默认的垃圾收集器,之前的很多技术原理在G1中依然可用,我们知道新生代有一个参数"-XX:SurvivorRatio=8",所以G1还是可以区分出来属于新生代的Region里哪些属于Eden,哪些属于Survivor;

比如新生代刚开始初始化时有100个Region,那么可能有80个Region是Eden,10个Region分别是两个Survivor,所以G1中依然有Eden和Survivor的概念,它们会各自占据不同的Region;

只不过随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加;

5% – 60%

G1垃圾收集器的新生代垃圾回收

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mXX2lKDf-1676628015841)(.//media/image13.png)]{width=“6.766666666666667in”
height=“2.629861111111111in”}

G1的新生代也有Eden和Survivor,其触发垃圾回收的机制也是类似的,随着不停在新生代Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%;

假设堆 4G,最大2048个region,每个region为2M,新生代最大60%=2.4G;

一旦新生代达到了设定的占据堆内存的最大大小60%,按照上面的数据大概就是有1200个Region,里面的Eden可能占据了1000个Region,每个Survivor是100个Region,而且Eden区满了,此时触发新生代的GC,G1就会依然用复制算法来进行垃圾回收,进入一个"Stop
theWorld"状态,然后把Eden对应的Region中的存活对象复制到S0对应的Region中,接着回收掉Eden对应的Region中的垃圾对象;

但这个过程与之前是有区别的,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过"-XX:MaxGCPauseMills"参数来设定,默认值是200ms,那么G1就会通过对每个Region追踪回收它需要多少时间,可以回收多少对象来选择回收一部分Region,保证GC停顿时间控制在指定范围内,尽可能多地回收对象;

G1垃圾收集器的老年代垃圾回收

1、初始标记,需要Stop the World,不过仅仅标记一下GC
Roots直接能引用的对象,这个过程速度很快,而且是借用进行Minor
GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿;

2、并发标记,不需要Stop the World,这个阶段会从GC
Roots开始追踪所有的存活对象,初始标记阶段仅仅只是标记GC
Roots直接关联的对象,而在并发标记阶段,就会进行GC Roots追踪,从这个GC
Root对象直接关联的对象开始往下追踪,追踪全部的存活对象,这个阶段是很耗时的,但可以和系统程序并发运行,所以对系统程序的影响不大;

3、重新标记(最终标记),需要Stop the*World,用户程序停止运行,最终标记一下有哪些存活对象,有哪些是垃圾对象;

4、筛选回收,需要Stop the*World,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间,这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的;

从整体上看,G1垃圾收集像是一种标记-整理算法,它不存在内存碎片问题,实际上它是一种复制算法,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,
所以它并不是纯粹地追求低延迟,而是给它设定暂停目标,使其在延迟可控的情况下获得尽可能高的吞吐量;

G1垃圾收集器的混合垃圾回收?

混合垃圾收集即mixed gc,它不是一个old gc,除了回收整个young
region,还会回收一部分的old
region,是回收一部分老年代,而不是全部老年代,可以选择部分old
region进行收集,从而可以对垃圾回收的耗时时间进行控制;

G1有一个参数,是"-XX:InitiatingHeapOccupancyPercent",它的默认值是45%,即如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段;

比如堆内存有2048个Region,如果老年代占据了其中45%的Region,也就是接近1000个Region的时候,就会开始触发一个混合回收;

G1回收失败时的Full GC

在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,把各个Region中存活的对象复制到其他空闲的Region中;

如果万一出现复制时没有空闲Region可以存放存活对象了,就会停止系统程序,然后采用单线程进行标记清除和压缩整理,空闲出来一批Region,这个过程很慢;

与CMS中的"Concurrent Mode Failure"失败会导致Full
GC类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫暂停用户线程,导致FullGC而产生长时间Stop The World;

1、新生代垃圾回收

2、老年代垃圾回收

3、混合回收

4、Full GC

什么时候使用G1垃圾收集器?

-XX:+UseG1GC

  1. 针对大内存、多处理器的机器推荐采用G1垃圾收集器,比如堆大小至少6G或以上;

  2. 超过50%的堆空间都被活动数据占用;

3、在要求低延迟的场景,也就是GC导致的程序暂停时间要比较少,0.5-1秒之间;

4、对象在堆中分配频率或者年代升级频率变化比较大,防止高并发下应用雪崩现象的场景;

ZGC收集器

-XX:+UseZGC

ZGC(Z Garbage
Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器,它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器,在JDK
11新加入,还在实验阶段,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms;

Shenandoah收集器

Shenandoah作为第一款不由Oracle(包括Sun)公司的虚拟机团队所领导开发的HotSpot垃圾收集器,Oracle官方明确拒绝在
OracleJDK12 中支持 Shenandoah
收集器,它是一款只有OpenJDK才会包含的收集器,最初由RedHat公司创建的,在2014年的时候贡献给了OpenJDK,Shenandoah收集器能实现在任何堆内存大小下都把垃圾停顿时间限制在10ms以内;

高薪必备JVM面试题解惑

主讲:Cat 老师

北京动力节点教育科技有限公司
2021 • 北京

动力节点•版权所有•禁止传播

什么是内存泄漏?什么是内存溢出?

**内存溢出:**OutOfMemory

它是指程序在申请内存时,没有足够的内存空间供其使用,抛出OutOfMemory错误;

比如申请了一个8MB空间,但是当前内存可用空间只有5MB,那么就是内存溢出;

即:OutOfMemoryError,是指没有空闲内存,垃圾收集器回收后也不能提供更多的内存空间;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sUG0j1oT-1676628015842)(.//media/image3.png)]{width=“3.348611111111111in”
height=“1.4430555555555555in”}

**内存泄露:**Memory Leak

它是指程序运行后,没有释放所占用的内存空间,一次内存泄漏可能不会有很大的影响,但长时间的内存泄漏,堆积到一定程度就会产生内存溢出;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dOyiWmpo-1676628015842)(.//media/image4.png)]{width=“3.245138888888889in” height=“1.63125in”}

(1)单例对象,生命周期和应用程序一样长,如果单例对象持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会产生内存泄露;

(2)一些资源未闭也会导致内存泄漏,比如数据库连接,网络连接socket和IO流的连接都必须在finally中close,否则不能被回收的;

你们线上环境的JVM都设置多大?

线上:4核8G机器;

JVM:栈、堆、元空间;

1、栈1m,xss512k,一个线程是1m,一个线上项目Tomcat可能有300个线程,300m;

2、堆:大概把机器的一半内存给堆,4G(新生代、老年代);

CMS:1/3 、2/3 G1: 6:4

3、元空间: 一般512M肯定够了

此时JVM参数如下:

-Xms4096M -Xmx4096M -Xss1M -XX:MetaspaceSize=512M
-XX:MaxMetaspaceSize=512M -XX:+UseG1GC

线上Java服务器内存飙升怎么回事?

jmap -histo pid

jmap -heap pid

jmap -dump:format=b,file=heap.hprof pid

线上Java项目CPU飙到100%怎么排查?

top

top -H -p pid

printf ‘%x’ tid 8ef

jstack pid

JVM堆溢出后,其他线程是否可以继续工作?

线上Java项目OOM了,怎么回事?

-Xms256M

-Xmx256M

-XX:+PrintGCDetails

-XX:+PrintGCDateStamps

-Xloggc:d:/gc.log

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=d:/heap.hprof

某一天晚上,线上Java服务器宕机了,怎么回事?

亿级流量的订单系统JVM优化

  1. 系统预估

    每个用户访问20次/天,500万日活用户,流量 = 500万 * 20 = 10000万 =
    1亿;

    购买率15%,每人1单,每天订单量 = 500万 * 15% * 1 = 75万订单/天;

    二八原则,下单集中在一天4小时内,洪峰下单量 = 75万 / 4小时 =
    18.75万单/小时

    = 18.75万单/60分/60秒 = 52单/秒;

    52k*20*10=52*200=10400 = 10MB/秒,每秒52单基本上JVM没有压力;

  2. 流量洪峰场景

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MrAdefla-1676628015843)(.//media/image5.jpeg)]{width=“5.6618055555555555in”
height=“3.185416666666667in”}

普通4核8G服务器,一台机器抗300-400并发下单请求比较合理;

583000 / 300 = 1943台机器

300KB * 20 * 10 = 60MB的内存开销,一秒后60MB对象就成为垃圾;

  1. 内存分配

    4核8G的机器,JVM给4G,剩下几个G会留给操作系统;

    堆3G(新生代1.5G,老年代1.5G)

    栈1MB,JVM里大概会有300-500个线程,大概300-500MB;

    元空间/永久代512MB;

"-Xms3072M -Xmx3072M -Xmn1536M -Xss1M

-XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M"

-XX:+PrintGCDetails

-XX:+PrintGCDateStamps

-Xloggc:d:/gc.log

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=d:/heap.hprof

  1. 内存占用动态推算

    每秒处理300个订单,占据新生代60MB的内存空间,新生代总共有1.5G的内存空间;

    1.5G * 1024MB / 60MB = 25秒 新生代Eden占满,触发Minor GC;

    一般情况下一次可以回收掉90%的新生代对象,存活对象 = 1.5G *
    1024MB * 10% = 150MB;

    如果"-XX:SurvivorRatio"参数默认值为8,那么:

    新生代Eden=1.2GB、S0 = 150MB、S1 = 150MB

  2. 如何调优?

    (1)、1次MinorGC后,可能Survivor不足或者触发动态年龄判断,对象进入老年代,明显是Survivor空间不足

    新生代调整为2G,老年代为1G,此时Eden:1.6G,每个Survivor:200MB;

    解决可能的Survivor不足或者触发动态年龄判断降低新生代对象进入老年代的概率;

    此时JVM参数:

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=512M
-XX:MaxMetaspaceSize=512M”

(2)、一般系统里的@Service、@Controller之类的注解需要长期存活,这些对象一般也不会很多,可能几十兆,应该让它们尽快进入老年代;

此时JVM参数:

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5”

(3)、
一般情况下,大对象可能需要长期存活和使用,让它直接进入老年代;(根据项目实际情况来确定)

此时JVM参数如下:

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M”

(4)指定合适的垃圾回收器;

此时JVM参数 :

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC”

没有使用G1垃圾收集器;

  1. 大概每隔几分钟Minor
    GC之后有大概200MB左右对象进入老年代,推算可能差不多1小时后,才会有接近1GB的对象进入老年代,触发Full
    GC,然后高峰期一过,可能需要几个小时才会一次Full GC;

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:-UseCompressedClassPointers
-XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction=0

  1. 优化思路
<!-- -->
  1. 尽可能让对象在新生代里分配和回收,避免对象频繁进入老年代导致老年代频繁垃圾回收

  2. 给系统充足的内存空间,避免新生代频繁的垃圾回收;

  3. 指定合适的垃圾收集器;
    =,降低新生代对象进入老年代的概率;

    此时JVM参数:

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=512M
-XX:MaxMetaspaceSize=512M”

(2)、一般系统里的@Service、@Controller之类的注解需要长期存活,这些对象一般也不会很多,可能几十兆,应该让它们尽快进入老年代;

此时JVM参数:

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5”

(3)、
一般情况下,大对象可能需要长期存活和使用,让它直接进入老年代;(根据项目实际情况来确定)

此时JVM参数如下:

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M”

(4)指定合适的垃圾回收器;

此时JVM参数 :

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC”

没有使用G1垃圾收集器;

  1. 大概每隔几分钟Minor
    GC之后有大概200MB左右对象进入老年代,推算可能差不多1小时后,才会有接近1GB的对象进入老年代,触发Full
    GC,然后高峰期一过,可能需要几个小时才会一次Full GC;

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:-UseCompressedClassPointers
-XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction=0

  1. 优化思路
<!-- -->
  1. 尽可能让对象在新生代里分配和回收,避免对象频繁进入老年代导致老年代频繁垃圾回收

  2. 给系统充足的内存空间,避免新生代频繁的垃圾回收;

  3. 指定合适的垃圾收集器;

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值