JVM的GC 策略及优化
2023-04-21 01:12:30 0 举报
AI智能生成
JVM的GC 策略及优化
作者其他创作
大纲/内容
减少GC的频率和Full GC的次数
目的
概念
Full GC = Minor GC(Young GC) + Major GC(Old GC)+Metaspace GC
老年代空间被写满
元空间不足
Minor GC后存活的对象晋升到老年代的平均大小大于老年代的剩余空间
CMS GC时出现promotion failed和concurrent mode failure
调用 System.gc() 也会安排一次 Full GC
什么时候会触发FullGC
Full GC
指在执行垃圾回收的过程冻结所有业务线程的运行,直到垃圾回收线程执行结束
为什么要STW?
垃圾收集线程与业务线程一起运行的后果
整理内存碎片
GC Roots根节点枚举
STW的场景
STW(Stop the World)
内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。
大并发情况下
内存泄露导致内存溢出
内存溢出(OOM)的原因
内存泄漏与内存溢出的区别
外框
名词概念
查看版本
-version
检索出所有的标准参数
-help
1. 常见标准参数
解释执行
-Xint
第一次使用就编译成本地代码
-Xcomp
混合模式,JVM自己来决定
-Xmixed
-X参数
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
-XX:[+-]<name>
格式
1. Boolean类型
-XX:MaxGCPauseMillis=500
-XX<name>=<value>
2. 非Boolean类型
2. -XX参数
-Xms1000M等价于-XX:InitialHeapSize=1000M
-Xmx1000M等价于-XX:MaxHeapSize=1000M
-Xss100等价于-XX:ThreadStackSize=100
3. 其他参数
sz flags.txt
java -XX:+PrintFlagsFinal -version > flags.txt
4. 查看参数
a. 开发工具中设置比如IDEA,eclipse
b. 运行jar包的时候:java -XX:+UseG1GC xxx.jar
c. web容器比如tomcat,可以在脚本中的进行设置
d. 通过jinfo实时调整某个java进程的参数(参数只有被标记为manageable的flags可以被实时修改)
5. 设置参数的常见方式
如果设置大于1,虽然编译速度会提高,但是同样影响系统稳定性,会增加JVM崩溃的可能
最大并行编译数
-XX:CICompilerCount=3
简写-Xms100M
初始化堆大小
-XX:InitialHeapSize=100M
最大堆大小
-XX:MaxHeapSize=100M
设置年轻代的大小
-XX:NewSize=20M
年轻代最大大小
-XX:MaxNewSize=50M
设置老年代大小
-XX:OldSize=50M
老年代
比如-XX:Ratio=4,则表示新生代:老年代=1:4,也就是新生代占整个堆内存的1/5
新老生代的比值
-XX:NewRatio
比如-XX:SurvivorRatio=8,也就是(S0+S1):Eden=2:8,也就是一个S占整个新生代的1/10
两个S区和Eden区的比值
-XX:SurvivorRatio
当JVM堆内存发生溢出时,也就是OOM,自动生成dump文件
启动堆内存溢出打印
-XX:+HeapDumpOnOutOfMemoryError
表示在当前目录生成一个heap.hprof文件
指定堆内存溢出打印目录
-XX:HeapDumpPath=heap.hprof
默认值为 15
提升年老代的最大临界值
-XX:MaxTenuringThreshold=6
设置方法区大小
-XX:MetaspaceSize=50M
方法区最大大小
-XX:MaxMetaspaceSize=50M
新生代,吞吐量优先
使用UseParallelGC
-XX:+UseParallelGC
老年代,吞吐量优先
使用UseParallelOldGC
-XX:+UseParallelOldGC
老年代,停顿时间优先
使用CMS
新生代,老年代,停顿时间优先
使用G1GC
-XX:+UseG1GC
可以使用不同的垃圾收集器,对比查看GC情况
打印出GC日志
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:g1-gc.log
经验值是3000-5000最佳
设置每个线程的堆栈大小
-Xss128k
启动并发GC周期时堆内存使用占比
-XX:InitiatingHeapOccupancyPercent
允许的浪费堆空间的占比
-XX:G1HeapWastePercent
暂停时间不能太小,太小的话就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。
G1最大停顿时间
-XX:MaxGCPauseMillis=200ms
默认值随JVM运行的平台不同而不同
并发垃圾收集器使用的线程数量
-XX:ConcGCThreads=n
参数
堆
年轻代
方法区
调用GC器
堆调优参数
6. 常用参数含义
调优参数
全称JVM Process Status Tool
查看java进程
1. jps
全称JVM statistics Monitoring
查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10次
jstat -class PID 1000 10
查看类装载信息
jstat -gc PID 1000 10
查看垃圾收集信息
用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据
2. jstat
全称JVM Memory Map
jmap -heap PID
打印出堆内存相关信息
用于生成heap dump文件
3. jmap
全称JVM Heap Analysis Tool
与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
4. jhat
jstack分析
把打印信息拉到最后可以发现
运行结果
分支主题
举例
jstack PID
查看线程堆栈信息
5. jstack
全称JVM Configuration info
jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID
查看用法
jinfo -flag [+|-] PID
jinfo -flag <name>=<value> PID
参数只有被标记为manageable的flags可以被实时修改
修改
jinfo -flags PID
查看曾经赋过值的一些参数
实时查看和调整虚拟机运行参数
6. jinfo
调优命令行工具有哪些?
JvisualVM(jdk自带)
Jconsole(jdk自带)
查找内存泄漏和减少内存消耗
MAT
阿里Arthas
4种JVM调优工具
0:查看内存当前进程 ps -aux|grep serviceName
:-XX:+HeapDumpOnOutMemoryError
出现OOM时生成堆dump
-XX:HeapDumpPath=/opt/jvmlogs/
生出堆文件地址
使用JvisualVM分析dump文件
2.DUMP文件分析
dump文件分析
JVM内部的优化逻辑
为什么会出现方法内联呢?
方法内联
逃逸分析不是直接的优化手段,而是代码分析手段
逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
什么是逃逸分析?
当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作对象的指针(或对象)的逃逸(Escape)
对象逃逸的本质是对象指针的逃逸
public User doSomething1() { User user1 = new User (); user1 .setId(1); user1 .setDesc("xxxxxxxx"); // ...... return user1 ;}
public void doSomething2() { User user2 = new User (); user2 .setId(2); user2 .setDesc("xxxxxxxx"); // ...... }
什么是“对象逃逸”?
逃逸分析
竞争不可抢占性资源引起死锁
竞争可消耗资源引起死锁
进程推进顺序不当引起死锁
计算机系统中的死锁
如果一组进程中的每一个进程都在等待仅由该进程中的其他进程才能引发的事件,那么该组进程是死锁的
定义
死锁的定义
互斥条件
请求和保存条件
不可抢占条件
如果每个资源只有一个实例,则环路等待条件是死锁存在的充分必要条件
循环等待条件
产生死锁的必要条件
在进程执行前采取的措施,通过设置某些限制条件,去破坏产生死锁的四个条件之一,防止发生死锁。
静态方法
所有进程在开始运行之前,必须一次性地申请其在整个运行过程中所需的全部资源
简单,易行,安全
优点
资源被严重浪费,严重地恶化了资源的利用率
使进程经常会发生饥饿现象
缺点
第一种协议
它允许一个进程只获得运行初期所需的资源后,便开始运行。进程运行过程中再逐步释放已分配给自己的,且已用毕的全部资源,然后再请求新的所需资源
第二种协议
破坏请求和保存条件
当一个已经保存了某些不可被抢占资源的进程,提出新的资源请求而不能得到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请
破坏不可抢占条件
对系统所以资源类型进行线性排序,并赋予不同的序号
例如令输入机的序号为1,打印机序号为2,磁盘机序号为3等。所有进程对资源的请求必须严格按资源序号递增的次序提出。
破坏循环等待条件
预防死锁的策略
预防死锁
安全状态
安全状态之例
由安全状态向不安全状态的转换
系统安全状态
每一个新进程在进入系统时,它必须申明在运行过程中,可能需要每种资源类型的最大单元数目,其数目不应超过系统所拥有的资源总量。当进程请求一组资源时,系统必须首先确定是否有足够的资源分配给该进程。若有,再进一步计算在将这些资源分配给进程后,是否会使系统处于不安全状态。如果不会,才将资源分配给它,否则让进程等待
含义
可用资源向量 Available[m]:m为系统中资源种类数,Available[j]=k表示系统中第j类资源数为k个。
银行家算法中的数据结构
银行家算法
安全性算法
银行家算法之例
利用银行家算法避免死锁
避免死锁的策略
避免死锁
选择一个没有阻塞的进程p
将p移走,包括它的所有请求边和分配边
重复步骤1,2,直至不能继续下去
简化步骤
资源分配图
若一系列简化以后不能使所有的进程节点都成为孤立节点
死锁定理
当进程等待时检测死锁 (其缺点是系统的开销大)
定时检测
系统资源利用率下降时检测死锁
检测时机
死锁检测中的数据结构
死锁的检测
抢占资源
死锁的解除
终止所有死锁进程
进程的优先级的大小
进程已执行了多少时间,还需时间
进程在运行中已经使用资源的多少,还需多少资源
进程的性质是交互式还是批处理的
代价最小
逐个终止进程
终止进程的方法
是使用一个有效的挂起和解除机构来挂起一些死锁的进程
付出代价最小的死锁解除算法
终止(或撤销)进程
死锁的检测与解除
处理死锁的方法
解除死锁
死锁排查
运行时优化
四、JVM调优
面向服务端的回收器
概述
G1能充分利用多CPU、多核环境的优势来缩短STW的停顿时间
并行+并发
G1收集器不需要与其它收集器配合就可以独立管理整个
分代收集堆
可预测的停顿
特点分析
为何设置H区?
Humongous区域
通过哈希表方式(哈希表底层使用数组)来存储
稀疏表
通过数组来存储,每个数组元素指向引用者分区中512直接内存块对本分区的引用情况
细粒度表
通过位图来指示,每1位表示对应的分区有引用到本分区
粗粒度位图
RSet的数据结构
分区内部有引用关系
新生代分区到新生代分区之间有引用关系
新生代分区到老生代分区之间有引用关系
老生代分区到新生代分区之间有引用关系
老生代分区到老生代分区之间有引用关系
需要使用RSet保存引用关系的情况
记忆集RSet(RememberedSet)
垃圾收集的目标范围
概念
回收集CSet(Collection Set)
Mixed GC模式
设定Region大小,取值范围1MB~32MB
-XX : G1HeapRegionSize
Region
扫描根和本地变量
根扫描
更新Rset
检测年轻代指向老年代的对象
处理Rset
年轻代复制到survival/old区
复制对象
处理软引用、弱引用、虚引用
处理引用
Young GC(新生代)
都有Young GC
相同
1. G1会根据预测时间动态地改变新生代的大小
不同
G1与其它JVM垃圾回收器比较
G1为每一个Region设计了两个名为TAMS的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围
TAMS(Top at Mark Start)的指针
需要停顿线程,但耗时很短(STW)
初始化标记
STAB破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,并发扫描结束后,在将这些记录重新扫描一次。
STAB(原始快照)
写屏障
解决并发标记问题
并发标记
用于处理并发阶段结束后仍遗留下来的最后少量的SATB记录
短暂停顿用户线程(STW)
最终标记
3. 把空闲分区放到空闲分区列表中。
暂停用户线程(STW)
筛选回收
全局并发标记(老年代)
工作流程
1.G1在压缩空间方面有优势
2.G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
4.G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
G1相对于CMS的优势
因垃圾收集产生的内存占用(Footprint)
程序运行时的额外执行负载(Overload)
G1相对于CMS的劣势
都比CMS要高
G1的意义
G1回收器
内存占用(Footprint)
吞吐量(Throughput)
延迟(Latency)
衡量垃圾收集器的指标
ZGC(The Z Garbage Collector)是JDK 11(只能在64位的linux上使用)中推出的一款追求极致低延迟的实验性质的垃圾收集器
概述
停顿时间不超过10ms
支持8MB~4TB级别的堆,未来支持16TB
停顿时间不会随着堆的大小,或者活跃对象的大小而增加
设计目标
JDK15已实现
不设分代使得ZGC进行垃圾回收时,就是全量回收,也就是每发生一次垃圾回收就是一次FGC,而每次垃圾回收的停顿时间在10ms以下
基于Region内存布局,不设分代
通过设计不同的标记位区分不同的虚拟空间,而这些不同标记位指示的不同虚拟空间通过mmap映射在同一物理地址;颜色指针能够快速实现并发标记、转移和重定位
颜色指针(color pointer)
实现并发标记和并发转移(整理)的处理
读屏障
尽量把对象分配在访问速度比较快的地方
NUMA
不同垃圾回收器的并发执行
特点
(1)初始标记
(2)并发标记
(3)最终标记和非强根并行标记
4)并发处理非强引用和非强根并发标记
实际上第一次垃圾回收时无须处理这一步
(5)重置转移集合中的页面
在内存充足的情况下不会触发这一步
(6)回收无效的页面
转移集合中就是待回收的页面
(7)并发选择对象的转移集合
在后续重定位(也称为Remap)时需要的对象转移表(Forward Table)就是在这一步初始化的
(8)并发初始化转移集合中的每个页面
该步需要STW
(9)转移根对象引用的对象
(10)并发转移
ZGC
三、不分代垃圾收集器
当Eden区或者S区空间不够用
老年代空间不够用
方法区空间不够用
System.gc()
为什么要有 GC?
要执行的Java代码的线程
JavaThread
执行JIT的线程
CompilerThread
JVM内部使用的线程
NameThread
JVM执行垃圾回收的同步线程,是JVM最关键的线程之一,主要的用途之一是处理垃圾回收
VMThread
并发执行垃圾回收任务的线程
ConcurrentGCThread
涉及垃圾回收的线程
绝大多数对象朝生夕死
活得越久的对象,也就是熬过很多次垃圾回收的对象是越来越难以消亡的
虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的
意义
例子
记忆集(Remembered Set)记录老年代跨代引用的内存块
存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的
分代收集理论
第一步
第二步
原理
解决产生内存碎片时放置对象的分配策略(动态分区分配策略)
存活对象较多的情况下比较高效
适用于老年代
适合场景
扫描了整个空间两次(1.标记存活对象;2.清除没有标记的对象)
缺点
1. 标记-清除算法
划分内存,一分为二将内存划分为两块相等的区域,每次只使用其中一块
概括
当其中一块内存使用完了,会把还存活的对象复制到另外一块上面,复制过去的对象会形成连续的地址空间,所以不会产生内存碎片,最后把已经使用过的内存空间清理一次清除掉
第二步
存活对象较少的情况下比较高效
扫描了整个空间一次(标记存活对象并复制移动)
Eden区和两块Survivor空间,若Survivor空间不足容纳一次Minor GC后存活的对象,由老年代分配担保
适合场景
需要一块空的内存空间
需要复制移动对象
2. 标记-复制算法
第一步
对象的移动方式和它们初始的对象排列及引用关系无关
移动位置但是并不更新标记(引用地址)
第一次遍历
更新标记
第二次遍历
流程
双指针回收算法
随机整理
线性整理(一般不怎么用)
整理前
Free指针是为了留位置,而Scan对象是为了找存活对象
第一次遍历
更新对象地址
移动对象
第三次遍历
Lisq2算法
滑动整理
关键词:偏移量,标记向量以及内存索引号
单次遍历算法的重点在于提前记录我们需要转移的位置
流程
适用于堆内存不完整(内存碎片)的情况,已分配的内存和空闲内存相互交错,JVM通过维护一张内存列表记录可用的内存块信息,当分配内存时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录,最常见的使用此方案的垃圾收集器就是CMS
空闲列表
假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞"
指针碰撞
单次遍历算法
整理算法
第三步
原理
所有现代的标记-整理回收器均使用滑动整理,它不会改变对象的相对顺序,也就不会影响赋值器的空间局部性。复制式回收器甚至可以通过改变对象布局的方式,将对象与其父节点或者兄弟节点排列的更近以提高赋值器的空间局部性。
本质是在标记清除的基础上进行整理
任意顺序算法只能处理单一大小的对象,或者针对大小不同的对象需要分批处理
整理过程需要2次或者3次遍历堆空间;对象头部可能需要一个额外的槽来保存迁移的信息
存活对象太多,移动对象效率极慢,而且对象移动操作必须全程暂停用户应用程序才能进行(Stop The World)
不需要移动对象,产生内存碎片,内存分配复杂
标记-清除算法
需要移动对象,移动操作和内存回收变复杂
标记-整理算法
与标记-清除算法进行比较思考
标记-整理算法
像基于标记-清除算法的CMS收集器则采用:让虚拟机平时多数时间都采用标记-清楚算法,暂时容忍内存碎片的存在,知道内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,来规整内存空间(并发模式失败)
即使不移动对象会使收集器的效率提升一些,但因内存分配和访问相比较垃圾收集频率要高得多,这部分的耗时增加,总吞吐量仍然是下降的
从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但从整个程序的吞吐量来看,移动对象会更划算
一、JVM垃圾回收算法
单线程收集器
只会使用一个CPU或者一个垃圾收集器去完成收集工作
标记复制算法
垃圾收集算法
工作流程
Serial
Serial收集器的多线程版本
多个线程进行垃圾回收
并发
多个垃圾收集线程进行执行的过程叫并行(当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行)
并行
并发与并行区别
目前只有它能够和CMS收集器配合工作(并行)
运行在Server模式下的首选新生代回收器
特点及使用场景
工作流程
ParNew
并行的多线程收集器
吞吐量 = 程序运行时间/(程序运行时间+垃圾收集时间)
在吞吐量优先的场景下使用
特点及使用场景
Parallel Scavenge
新生代的垃圾收集器
单线程回收器
主要用于client的jvm回收
标记整理(压缩)算法
Serial Old
Parallel Scavenge的老年代回收器
Parallel Old
以最短响应为目标的回收器
适用于BS架构的环境
标记清除算法(三色标记算法+增量更新)
优点
并发阶段会降低吞吐量
优点和缺点分析
CMS单线程或者双线程情况下效率很低
CMS会并发失败
CMS可中止的预处理会导致极限5S停顿
CMS吞吐的设计并不是很优秀
JDK8为什么不用CMS做为默认垃圾收集器?
JDK1.7初始化标记阶段是串行;JDK8以后默认是并行
1.初始时,所有对象都在 【白色集合】中;
2.将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
3.从灰色集合中获取对象:3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;3.2. 将本对象 挪到 【黑色集合】里面。3.3重复步骤3.1和3.2,直至【灰色集合】为空时结束
4.结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收
三色标记(遍历过程)
标记直接相关联的第一个对象
(1)初始标记
③
②
①
浮动垃圾(多标)
1.赋值器插入了一条或者多条从黑色对象到白色对象的新引用
2.赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
漏标必须同时满足的两个条件
增量更新(Incremental Update)
方案
只需破坏这两个条件的任意一个即可解决并发扫描时的对象消失问题
对象消失(漏标)
并发标记出现的问题
(2)并发标记
并发标记阶段与用户线程一起运行,所以会产生新的对象之间的关系,新增关系的对象所在Card会被标记为Dirty Card
(3)并发预处理
(4)可中止的预处理
引用的对象是黑色对象,且新的引用的目标对象为灰色或白色,那么我们就把发出引用的对象涂成灰色,将在后续被重新扫描。
增量更新破坏的是第一个条件,在新增一条引用时,将该记录保存。实际的实现中,通常是将引用相关的节点进行重新标记
增量更新:Incremental Update
解决并发标记问题
(5)重新标记
(6)并发清除
为什么并发标记细化之后还会额外有两个流程出现呢?
CMS想到了一种方式,就是先进行新生代的垃圾回收,也就是一次young GC,回收完毕之后。是不是新生代的对象就变少了,那么再进行垃圾回收,是不是就变快了。
CMSScheduleRemarkEdenSizeThreshold 默认值:2MCMSScheduleRemarkEdenPenetration 默认值:50%
只要到了5S,不管发没发生Minor GC,有没有到CMSScheduleRemardEdenPenetration都会中止此阶段,进入remark
CMSMaxAbortablePrecleanTime 默认为5S
CMS提供CMSScavengeBeforeRemark参数,使remark前强制进行一次Minor GC
如果在5S内还是没有执行Minor GC怎么办?
怎么知道young区什么时候发生minor GC?
触发young GC的设置
怎么让回收变快?
当老年代被回收的时候,我们如何判断A对象是存活对象?(如图)
young区策略
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的数据结构
脏卡(dirty card)
写屏障(Write Barrier)
如何在对象赋值(跨代引用)的那一刻去更新维护卡表?(谁来把卡页变脏?)
卡表
实现
记忆集(理论)
old区策略
该值代表老年代堆空间的使用率,默认值为68。当老年代使用率达到此值之后,并行收集器便开始进行垃圾收集,该参数需要配合UseCMSInitiatingOccupancyOnly一起使用,单独设置无效。
-XX:CMSInitiatingOccupancyFraction
该参数启用后,参数CMSInitiatingOccupancyFraction才会生效。默认关闭
-XX:+UseCMSInitiatingOccupancyOnly
例如:当老年代达到: ((100 - 40) + (double) 80 * 40 / 100 ) / 100 = 92 %时,会触发CMS回收
看源码公式:((100 - MinHeapFreeRatio) + (double)( CMSTriggerRatio * MinHeapFreeRatio) / 100.0) / 100.0
调低-XX:CMSInitiatingOccupancyFra
减少并发模式失败ction
参数预防
jvm会放弃使用并发的CMS GC,转而采用单线程的Serial Old GC
已经并发失败
并发模式失败CMS(concurrent mode failure)
CMS
老年代的垃圾收集器
二、分代垃圾收集器
JVM的GC策略及优化
收藏
收藏
0 条评论
回复 删除
下一页