JVM知识点
文章目录
- JVM知识点
- 1. JVM的主要组成部分?及其作用?
- JVM基本结构
- 1.1. 类加载字系统
- 1.2. Java堆
- 1.3. 直接内存
- 1.4. 垃圾回收系统
- 1.5. Java栈
- 1.5.1. 函数调用-出入JAVA栈
- 1.5.1.1. 局部变量表
- 1.5.1.2. 操作数栈
- 1.5.1.3 帧数据区
- 1.5.1.4 栈上分配
- 1.6. 方法区
- 1.7. 本地方法栈
- 1.8. PC(program Counter)寄存器
- 1.9. 执行引擎
- 2.说一下 JVM运行时数据区?
- 线程私有的数据区
- 程序计数器
- 3.说一下堆栈的区别?
- 4.队列和栈是什么?有什么区别?
- 5.什么是双亲委派模型?
- 6.说一下类加载的执行过程?
- 7.怎么判断对象是否可以被回收?
- 引用计数法
- 可达性算法
- 8.Java 中都有哪些引用类型?
- 9.说一下 JVM有哪些垃圾回收算法?
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法
- 10.说一下 JVM有哪些垃圾回收器?
- 11.GC是什么时候触发的
- Scavenge GC
- Full GC
- 12. 详细介绍一下 CMS 垃圾回收器?
- CMS如何执行?
- 1. 初始标记(STW)
- 2. 并发标记
- 3. 并发预清理
- 4. 重标记(STW)
- 5. 并发清理
- 6. 重置
- CMS有什么问题
- 13.新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
- 新生代垃圾回收器
- Serial收集器
- ParNew收集器
- Parallel Scavenge收集器
- 老生代垃圾回收器
- Serial Old
- Parallnel Old
- CMS
- 新生代和老年代垃圾收集器
- G1
- 总结
- 14.简述分代垃圾回收器是怎么工作的?
- 15.说一下 JVM调优的工具?
- 16.常用的 JVM调优的参数都有哪些?
1. JVM的主要组成部分?及其作用?
JVM基本结构
1.1. 类加载字系统
类加载子系统负责从文件或者网络中加载Class信息,加载的类信息存放与方法区的内存空间。方法区还可能存放运行时产量信息,包括字符串与数字常量。(这部分常量信息是Class文件中常量池部分的内存映射)
1.2. Java堆
Java堆在JVM启动时建立,几乎所有的Java对象实例都存放于Java堆中。堆空间是所有线程共享的。根据垃圾回收机制的不同,Java堆可能有不同的结构。比如JDK 1.7和JDK 1.8。
如以下代码:
public class SimleHeap {
public int id;
public SimleHeap(int id) {
this.id = id;
}
public void show() {
System.out.println("My ID is " + id);
}
public static void main(String[] args) {
SimleHeap s1 = new SimleHeap(1);
SimleHeap s2 = new SimleHeap(2);
s1.show();
s2.show();
}
}
SimpleHeap类的信息存放在方法区,主函数中的s1和s2存放在JAVA栈中,并且指向堆中的两个实例。
1.3. 直接内存
Java的NIO允许Java程序使用直接内存,直接内存是在Java堆外的,直接向系统申请的系统空间。通常来讲,访问直接内存的速度要大于Java堆,直接内存适合于频繁读写场景,直接堆在Java堆外,因此它的大小不会受到Xmx指定的最大堆大小的限制,但Java堆和直接内存依然受限于系统的最大内存。
1.4. 垃圾回收系统
垃圾回收器可以对方法区、Java堆和直接内存进行回收
1.5. Java栈
每个JVM线程都有一个私有的Java栈,Java栈是线程创建的时候创建。Java栈中保存着局部变量,方法参数、同时和Java方法的调用、返回密切相关。
1.5.1. 函数调用-出入JAVA栈
栈是线程私有的内存空间,线程执行的基本行为是函数调用,每次函数调用的数据都是通过JAVA栈传递的。JAVA栈是一块先进后出的数据结构,只支持出栈入栈两种操作。在JAVA栈中保存的主要内容为栈帧。每一次函数调用,都会有一个对应的栈帧被压入JAVA栈,每一个函数调用结束,都会有一个栈帧被弹出JAVA栈。JAVA方法有两种返回函数的方式,一种是正常的return,一个是抛出异常,不管哪种方式,都会导致栈被弹出。在一个栈中,至少要包含局部变量表、操作数栈和帧数据区几个部分。JVM提供了-Xss指定线程栈的最大空间,这个参数决定了函数调用的最大深度。
JAVA栈的结构
1.5.1.1. 局部变量表
它用于保存函数的参数以及局部变量,局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也会被销毁。由于变量表在栈帧中,如果函数的参数与局部变量较多,会使得局部变量表膨胀,从而每一次函数调用就会占更多的内存空间。
1.5.1.2. 操作数栈
主要用于保存计算过程中的中间结果,同时作为计算过程中变量的临时存储空间。操作数栈也是一个先进后出的数据结构,只支持入栈和出栈两种操作。
1.5.1.3 帧数据区
帧数据区保存着返回常量池的指针,方便程序访问常量池。当函数返回或出现异常时,虚拟机必须恢复调用者函数的栈帧,并让调用者函数继续执行下去,虚拟机必须有一个异常处理表,方便发生异常时找到处理异常的代码,因此,异常处理表也是帧数据区的重要一部分
1.5.1.4 栈上分配
栈上分配是JVM优化的一项技术,对于那些线程的私有对象,可以将他们打散分配在栈上,而不是堆上。当函数调用结束后可以自行销毁,不需要GC介入,从而提高系统性能
1.6. 方法区
和Java堆一样,方法区是一块线程共享的内存区域。它用于保存系统的类信息,比如类的字段、方法、常量池等。方法区的大小决定了系统可以保存多少个类。
在JDK 1.7中,方法区可理解为永久代,在JDK 1.8中方法区可以理解为元数据区。
1.7. 本地方法栈
本地方法栈和Java栈非常相似,最大的不同在于Java栈用于Java方法的调用,本地方法栈则用于调用本地方法。
1.8. PC(program Counter)寄存器
PC寄存器是每个线程的私有空间,JVM会为每个Java线程创建PC寄存器。在任意时刻,一个Java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前在背执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined。
1.9. 执行引擎
负责执行虚拟机的字节码
2.说一下 JVM运行时数据区?
线程私有的数据区
程序计数器
作用: 用于记录当前线程执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
意义: JVM的多线程是通过线程轮换来分配处理器来实现的,对于我们来说,并行事实上一个处理器也只会执行一条线程中的指令。所以为了保证各线程指令的顺利执行,每条线程都有独立的程序计数器。
存储内容:
当线程中执行的是一个Java方法时,程序计数器中记录的是正在执行的线程的虚拟机字节码指令的地址。
当线程中执行的是一个本地方法时,程序计数器中的值为空。
可能出现异常: 此内存区域是唯一一个在JVM上不会发生内存溢出异常(OutOfMemoryError)的区域。
3.说一下堆栈的区别?
4.队列和栈是什么?有什么区别?
5.什么是双亲委派模型?
当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委托给它的父类加载器去执行,每一层的类都采用相同的方式,直至委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行加载。
使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。
使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委托给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种加载器环境中都是同一个类。相反,如果没有使用双亲委托模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。
6.说一下类加载的执行过程?
javascript:void(0)
7.怎么判断对象是否可以被回收?
引用计数法
简单的说就是给对象添加一个计数器,每当有一个地方引用它时,计数器就加1;当引用失效,计数器就减1;任何时刻计数器为0的对象,就是不可能再使用的。
**优点:**效率高,实现简单
**缺点:**无法解决对象之间循环引用的问题
可达性算法
算法的基本思想是通过一系列的成为“GC Roots”的对象作为起点,从这些起点开始向下搜索,搜索的路径就成为引用链(Reference Chain),当一个对象到GC Roots没有任何的引用链相连的话,也就是该对象不可达,则证明该对象是不可用的
在Java中GC Roots对象包括以下几个:
- 虚拟机栈(栈帧的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈JNI引用的对象
在可达性的算法中,至少要标记两次,才会真正宣告一个对象的死亡:如果对象在进行可达性分析发现没有GC Root相连的引用链,那么将会被第一次标记并进行一次筛选,筛选的条件是此对象是否必要执行Finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,虚拟机都将这两种情况视为“没有必要执行”
如果这个对象被判定有必要执行finalize()方法,那么这个对象会被放置在一个叫做F-Queue的队列中。并在稍后有一个虚拟机自动建立的,底优先级的Finalize线程去执行他。
这里所谓的执行是指虚拟机触发这个方法,并不会承诺等待他允许结束。这样的原因是:如果一个对象的finalize()方法执行缓慢,或者发生死循环,可能会到导致F-Queue队列中的其他对象永远处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,如果对象要在finalize()方法中拯救自己–只需要重新与引用链上的任何一个对象建立关联即可(这种自救的机会只能有一次),譬如把自己的this关键字赋值给某各类的变量或者类的成员变量即可,那在第二次标记时,就会被移除回收集合。如果对象在这个时候还没有逃脱,基本上就要被回收了。
8.Java 中都有哪些引用类型?
强引用、软引用、弱引用、虚引用
9.说一下 JVM有哪些垃圾回收算法?
标记-清除算法
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
复制算法
复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
标记-整理算法
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。具体流程见下图:
分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation)
老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
10.说一下 JVM有哪些垃圾回收器?
- Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC
来强制指定。 - Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。 - ParNew收集器(停止-复制算法)
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。 - Parallel Scavenge收集器(停止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC
来强制指定,用-XX:ParallelGCThreads=4
来指定线程数。 - Parallel Old收集器(停止-复制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。 - CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。
11.GC是什么时候触发的
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
a) 年老代(Tenured)被写满;
b) 持久代(Perm)被写满;
c) System.gc()被显示调用;
d) 上一次GC之后Heap的各域分配策略动态变化;
12. 详细介绍一下 CMS 垃圾回收器?
CMS全称 Concurrent Mark Sweep
,是一款并发的、使用标记-清除算法的垃圾回收器,如果老年代使用CMS垃圾回收器,需要添加虚拟机参数-”XX:+UseConcMarkSweepGC”。
CMS以获取最小停顿时间为目的。在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景。
CMS如何执行?
1. 初始标记(STW)
初始标记阶段需要STW
该阶段进行可达性分析,标记GC ROOT能直接关联到的对象。
注意是直接关联间接关联的对象在下一阶段标记。
2. 并发标记
并发标记阶段是和用户线程并发执行的过程
该阶段进行GC ROOT TRACING,在第一个阶段被暂停的线程重新开始运行。
由前阶段标记过的对象出发,所有可到达的对象都在本阶段中标记。
3. 并发预清理
并发预处理阶段做的工作还是标记,与重标记功能相似
CMS是以获取最短停顿时间为目的的GC。重标记需要STW(Stop The World),因此重标记的工作尽可能多的在并发阶段完成来减少STW的时间。此阶段标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。
4. 重标记(STW)
暂停所有用户线程,重新扫描堆中的对象,进行可达性分析,标记活着的对象
有了前面的基础,这个阶段的工作量被大大减轻,停顿时间因此也会减少
注意这个阶段是多线程的
5. 并发清理
用户线程被重新激活,同时清理那些无效的对象
6. 重置
CMS清除内部状态,为下次回收做准备
CMS有什么问题
CMS这三个字母就隐含了问题所在。并发+标记-清除算法 是问题的来源。
先说并发,并发意味着多线程抢占CPU资源,即GC线程与用户线程抢占CPU。这可能会造成用户线程执行效率下降。
并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。这些垃圾有个专业名词:浮动垃圾。
由于垃圾回收阶段用户线程仍在执行,必需预留出内存空间给用户线程使用。因此不能像其他回收器那样,等到老年代满了再进行GC。
CMS 提供了CMSInitiatingOccupancyFraction参数来设置老年代空间使用百分比,达到百分比就进行垃圾回收。
这个参数默认是92%,参数选择需要看具体的应用场景。
使用标记-清除算法可能造成大量的空间碎片。空间碎片过多,就会给大对象分配带来麻烦。
往往老年代还有很大剩余空间,但无法找到足够大的连续空间来分配当前对象,不得不触发一次Full GC。
CMS的解决方案是使用UseCMSCompactAtFullCollection参数(默认开启),在顶不住要进行Full GC时开启内存碎片整理。
这个过程需要STW,碎片问题解决了,但停顿时间又变长了。
虚拟机还提供了另外一个参数CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认为0,每次进入Full GC时都进行碎片整理)。
总结一下:
CMS采用了多种方式尽可能降低GC的暂停时间,减少用户程序停顿。
停顿时间降低的同时牺牲了CPU吞吐量 。
这是在停顿时间和性能间做出的取舍,可以简单理解为"空间(性能)"换时间。
13.新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
新生代垃圾回收器
Serial收集器
单线程收集器,在它进行收集时,必须暂停其他所有的工作线程,直到它收集结束。虽然“暂停时间”给用户带来很差的用户体验,但是在Client下,回收内存较小,完全可以忍受,是Client模式下的默认新生代收集器
ParNew收集器
它其实就是单线程收集器的多线程版本,是Server模式下的默认新生代收集器
Parallel Scavenge收集器
它也是使用复制算法,同时也是并行的多线程收集器。它的特别之处是关注点与其他的收集器不懂,CMS等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目的是达到一个可控的吞吐量。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短越适合于用户交互的程序,良好的相应速度能提升用户体验,而高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
Parallel Scavenge收集器提供了两个参数用于精确的控制吞吐量,分别是控制最大垃圾收集时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
收集器尽可能使回收时间不大于设定值[减少回收的内存大小],使得垃圾收集更频繁一些,停顿时间下降,但吞吐量也下降了。GCTimeRatio参数的值是吞吐量的倒数=垃圾收集时间/(垃圾收集时间+运行用户代码时间)
Parallel Scavenge收集器还有一个参数:——XX:+UseAdaptiveSizePolicy,这是一个开关参数,当着参数打开后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态得调整这些参数一体同最合适的停顿时间或者最大的吞吐量,这种调节方式成为GC自适应的调节策略。自适应的调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
老生代垃圾回收器
Serial Old
Serial Old是Serial算法的老年代版本,同样是一个单线程收集器。该收集器主要是给Client模式下的虚拟机使用的。
分代收集算法:新生代单线程采用复制算法,并暂停所有用户线程;老年代单线程采用标记-整理算法,并暂停所有用户线程。
Parallnel Old
Parallnel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
CMS
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。该收集器是基于”标记-清除“算法实现的。
CMS收集器的收集过程分为以下4个步骤:
1、初始标记(Stop the World,标记GC Roots能直接关联到的对象)
2、并发标记(进行GC Roots Tracing的过程)
3、重新标记(Stop the World,休整并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录)
4、并发清除(并发清除无用的对象)
缺点:
a、CMS收集器对CPU资源非常敏感,并发阶段占用的线程资源较多。
b、CMS收集器无法处理浮动垃圾。因为CMS并发清理阶段用户线程还在运行着,所以也会有相应的垃圾产生,这部分垃圾CMS无法在此次的收集中处理掉它们。
c、CMS收集器由于是基于“标记-清除”算法,故会产生较多的内存空间碎片。
分代收集算法:新生代采用复制算法,并暂停所有用户线程;老年代采用标记-整理算法,并暂停所有用户线程。
新生代和老年代垃圾收集器
G1
G1(Garbage-First)收集器所具备的特点:
1、并行和并发:使用多个CPU来缩短Stop-The-World的时间,部分垃圾收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
2、分代收集
3、空间整合:标记-整理算法。
4、可预测的停顿。追求低停顿,并建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,达到了实时Java的垃圾收集器。
G1收集器分代策略:
G1收集器将整个Java堆划分为多个大小相等的独立区域(Region)。G1收集器之所以可以有计划地避免在整个Java堆中进行全区域的垃圾收据,是因为G1收集器跟踪各个Region里面的垃圾堆积的价值大小(回收获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。即Grabage-First。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是通过
Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set。在新建对象时,JVM会将相关的引用信息记录到被引用对象所属的Region的Remembered Set中。当进行回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对堆进行扫描也不会有遗漏。
G1收集器的手机阶段也分以下几个步骤:
1、初始标记(只是标记一下GC Roots能直接关联到的对象,并修改可以得Region中创建新对象,这阶段需要停顿线程,但耗时很短)
2、并发标记(从GC Roots开始对堆中对象进行可达性分析,找出存活对象)
3、最终标记(修正在并发标记期间因月洪湖程序继续运行而导致标记产生变动的那一部分标记记录)
4、筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)
总结
分类 | 所属分代 | 使用线程 | 使用算法 |
Serial | 新生代 | 单线程 | 复制(新)、标记-整理(老) |
ParNew | 新生代 | 多线程 | 复制(新)、标记-整理(老) |
Parallel Scavenge | 新生代 | 多线程 | 吞吐量优先算法 |
Serial Old | 老生代 | 单线程 | 复制(新)、标记-整理(老) |
Parallel Old | 老生代 | 多线程 | 复制(新)、标记-整理(老) |
CMS | 老生代 | 多线程 | 标记-清除算法(初始标记、并发标记、重新标记、并发清除) |
G1 | 新生代&&老生代 | 多线程 | 标记-整理算法(初始标记、并发标记、最终标记、筛选回收) |
14.简述分代垃圾回收器是怎么工作的?
15.说一下 JVM调优的工具?
16.常用的 JVM调优的参数都有哪些?