文章原文:https://gaoyubo.cn/blogs/6997cf1f.html
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域 有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是 依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域
线程私有
是一个非常小的内存区域,用于存储当前线程正在执行的字节码指令的地址。每个线程在JVM中都有一个独立的程序计数器。当JVM执行一条字节码指令时,程序计数器会更新为下一条指令的地址。
简而言之,程序计数器存储的是当前正在执行的字节码指令的地址。一旦这条指令执行完毕,程序计数器会立即更新为下一条指令的地址。这样,JVM就可以知道接下来应该执行哪条指令。
需要注意的是,对于那些会导致控制流跳转的指令(如条件跳转、循环等),程序计数器会根据指令的具体行为更新为相应的目标地址,而不是简单地递增到下一个地址。
字节码指令地址
;线程私有
每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧用于存储局部变量表
、操作数栈
、动态连接
、方法出口
等信息。
OutOfMemoryError
(在虚拟机栈可以动态扩展的情况下,扩展时无法申请到足够的内存);*Error
(线程请求的栈深度 > 虚拟机所允许的深度);-Xss
.局部变量表
存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用。
线程私有
线程共享
“几乎”所有的对象实例都在这里分配内存
由于即时编 译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙 的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
OutOfMemoryError
(堆中没有内存可以分配给新创建的实例,并且堆也无法再继续扩展了)。-Xmx
-Xms
常量池
线程共享
OutOfMemoryError
(方法区无法满足内存分配需求时)。永久代
永久代
的概念,改用与JRockit
、J9
一样在本地内存中实现的元空间
方法区的类型信息、静态变量<------>class文件的相对应的表
方法区的运行时常量池<---------->class的常量池表
运行时常量池也是方法区的一部分;
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性
,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String
类的 intern()
方法。
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译器生成的各种字面量(就是代码中定义的 static final 常量)和符号引用,这部分信息就存储在运行时常量池中。
Class文件不会保存各个方法和字段的最终内存布局信息,而是在将类加载到JVM后进行动态链接
的,需要将字段、方法的符号引用经过运行期转换才能正常使用;
NIO
,通过存在堆中的DirectByteBuffer
操作Native内存由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx
指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存
缺点
直接内存大小可以通过MaxDirectMemorySize
设置;如果不指定,默认与堆的最大值-Xmx
参数值一致
参考:JVM系列(九)直接内存(Direct Memory) - 掘金 (juejin.cn)
当Java虚拟机遇到一条字节码new指令时。
常量池
中定位到一个类的符号引用
,<init>()
方法还没有执行,执行构造方法。这其中有两个问题,
线程安全
-XX:+/-UseTLAB
参数设定是否使用 TLAB。对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)
、实例数据(Instance Data)
和对齐填充(Padding)
。
HotSpot VM
要求对象的起始地址必须是8字节的整数倍,所以不够要补齐。Java 程序需要通过虚拟机栈上的 reference 数据来操作堆上的具体对象,reference 数据是一个指向对象的引用,不过如何通过这个引用定位到具体的对象,目前主要有以下两种访问方式:句柄访问和直接指针访问。
句柄访问会在 Java 堆中划分一块内存作为句柄池,每一个句柄存放着到对象实例数据和对象类型数据的指针。
优势:对象移动的时候(这在垃圾回收时十分常见)只需改变句柄池中对象实例数据的指针,不需要修改reference本身。
直接指针访问方式在 Java 堆对象的实例数据中存放了一个指向对象类型数据的指针,在 HotSpot
中,这个指针会被存放在对象头中。
优势:减少了一次指针定位对象实例数据的开销,速度更快。
java.lang.OutOfMemoryError: Java heap space
-Xmx
和 -Xms
)是否可以调大,检查代码中是否有哪些对象的生命周期过长,尝试减少程序运行期的内存消耗。-XX:HeapDumpOnOutOfMemoryError
:让虚拟机在出现内存泄漏异常时 Dump 出当前的内存堆转储快照用于事后分析。OutOfMemoryError
,只会导致 *Error
(栈会比内存先爆掉),一般多线程才会出现 OutOfMemoryError
,因为线程本身要占用内存;OutOfMemoryError
,在不能减少线程数或更换 64 位虚拟机的情况,只能通过减少最大堆和减少栈容量来换取更多的线程;
Spring 框架
(使用 CGLib
字节码技术),方法区溢出是一种常见的内存溢出,要特别注意类的回收状况。-XX:MaxDirectMemorySize
,如果不指定,则和 -Xmx
一样。垃圾收集(Garbage Collection,GC)
,它的任务是解决以下 3 件问题:
其中第一个问题很好回答,在 Java 中,GC
主要发生在 Java 堆和方法区中,对于后两个问题,将在之后的内容中进行讨论,并介绍 HotSpot
的 7 个垃圾收集器。
什么时候回收对象?当然是这个对象再也不会被用到的时候回收。
所以要想解决 “什么时候回收?” 这个问题,我们要先能判断一个对象什么时候什么时候真正的 “死” 掉了,判断对象是否可用主要有以下两种方法。
objA.instance = objB; objB.instance = objA;
,objA 和 objB 都不会再被访问后,它们仍然相互引用着对方,所以它们的引用计数器不为 0,将永远不能被判为不可用。即便如此,一个对象也不是一旦被判为不可达,就立即死去的,宣告一个的死亡需要经过两次标记过程。
在可达性分析中,第一阶段 ”根节点枚举“ 是必须 STW 的,那么为什么因此必须在一个能保障一致性的快照
上才能进行对象图的遍历,而不是同步用户线程进行呢?
引入三色标记作为工具来辅助推导,把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:
关于可达性分析的扫描过程,可以看作对象图上一股以灰色为波峰的波纹从黑向白推进的过程,此时如果用户线程改变了对象的引用关系,会发生两种情况:
如上图所示,b -> c 的引用被切断,但同时用户线程建立了一个新的从 a -> c 的引用,由于已经遍历到了 b,不可能再回去遍历 a(黑色对象不会被重新扫描),再遍历 c,所以这个 c 实际是存活的对象,但由于没有被垃圾收集器扫描到,被错误地标记成了白色,就会导致c被标记为需要回收的对象。
总结下对象消失问题的两个条件:
当且仅当以上两个条件同时满足时,才会产生 “对象消失” 的问题,即原本应该是黑色的对象被误标为白色
对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障现的。在 HotSpot虚拟机
中,增量更新和原始快照这两种解决方案都有实际应用,譬如,CMS是基于增量更新 来做并发标记的,G1、Shenandoah则是用原始快照来实现。
JDK 1.2 后,Java 中才有了后 3 种引用的实现。
Object obj = new Object()
这种,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。SoftReference
。WeakReference
。PhantomReference
。finalize()
方法的判断;
finalize()
方法,或者 finalize()
方法已被执行过(finalize()
只被执行一次);finalize()
方法是对象逃脱死亡的最后一次机会,不过虚拟机不保证等待 finalize()
方法执行结束,也就是说,虚拟机只触发 finalize()
方法的执行,如果这个方法要执行超久,那么虚拟机并不等待它执行结束,所以最好不要用这个方法。finalize()
方法能做的,try-finally 都能做,所以忘了这个方法吧永久代的 GC 主要回收:废弃常量 和 无用的类。
算法描述:
不足:
算法描述:
不足: 可用内存缩小为原来的一半,适合GC过后只有少量对象存活的新生代。
节省内存的方法:
-XX:SurvivorRatio=8
表示 Eden 区大小 / 1 块 Survivor 区大小 = 8
。通过之前的分析,GC 算法的实现流程简单的来说分为以下两步:
想要找到死掉的对象,我们就要进行可达性分析,也就是从 GC Root 找到引用链的这个操作,需要获取所有对象引用。
那么,首先要找到哪些是 GC Roots。
有两种查找 GC Roots 的方法:
OopMap
数据结构来记录 GC Roots
的位置(准确式 GC)。很明显,保守式 GC 的成本太高。准确式 GC 的优点就是能够让虚拟机快速定位到 GC Roots。
但是当内存中的对象间的引用关系发生变化时,就需要改变 OopMap
中的相应内容。可是能导致引用关系发生变化的指令非常之多,如果我们执行完一条指令就改下 OopMap
,这 GC 成本实在太高了。于此,安全点和安全区域就很重要了。
因此,HotSpot
采用了一种在 “安全点”
更新 OopMap
的方法,安全点的选取既不能让 GC 等待的时间过长,也不能过于频繁增加运行负担,也就是说,我们既要让程序运行一段时间,又不能让这个时间太长。
JVM 中每条指令执行的是很快的,所以一个超级长的指令流也可能很快就执行完了,所以 真正会出现 “长时间执行” 的一般是指令的复用,例如:方法调用、循环跳转、异常跳转等,虚拟机一般会将这些地方设置为安全点更新 OopMap
并判断是否需要进行 GC
操作。
此外,在进行枚举根节点的这个操作时,为了保证准确性,我们需要在一段时间内 “冻结” 整个应用,即 Stop The World
,因为如果在我们分析可达性的过程中,对象的引用关系还在变来变去,那是不可能得到正确的分析结果的。即便是在号称几乎不会发生停顿的 CMS 垃圾收集器
中,枚举根节点时也是必须要停顿的。这里就涉及到了一个问题:
主要有以下两种方式:
安全点
,恢复它,让它跑到安全点。安全点
时,检查这个中断标记,选择是否中断自己。除此安全点之外,还有一个叫做安全区域
的东西。
安全区域是指在一段代码片段之中,引用关系不会发生变化,因此在这个区域中的任意位置开始 GC 都是安全的。
一个一直在执行的线程可以自己 “走” 到安全点去,可是一个处于 Sleep
或者 Blocked
状态的线程是没办法自己到达安全点中断自己的,我们总不能让 GC 操作一直等着这些个 ”不执行“ 的线程重新被分配资源吧。对于这种情况,我们要依靠安全区域来解决。
当线程执行到安全区域时,它会把自己标识为 Safe Region
,这样 JVM 发起 GC
时是不会理会这个线程的。当这个线程要离开安全区域时,它会检查系统是否在 GC
中,如果不在,它就继续执行,如果在,它就等GC
结束再继续执行。
为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)
的数据结构,用以避免把整个老年代加进GC Roots
扫描范围。
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了。
采用种称为卡表(Card Table)
的方式去实现记忆集。
卡表最简单的形式只是一个字节数组。
CARD_TABLE [this address >> 9] = 0;
字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个 内存块被称作卡页(Card Page)
。
一般来说,卡页大小都是以2的N次幂的字节数,通过上面代码可 以看出HotSpot中使用的卡页是2的9次幂,即512字节(地址右移9位,相当于用地址除以512)。那如 果卡表标识内存区域的起始地址是0x0000的话,数组CARD_TABLE的第0、1、2号元素,分别对应了 地址范围为0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡页内存块
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代 指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。
在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它 们加入GC Roots
中一并扫描。
如何维护卡表元素?
如何维护卡表《=======》如何在对象赋值的那一刻去更新卡表
解释执行
的字节码,好处理,虚拟机负责每条字节码指令的执行,有充分的介入空间编译执行
的场景中经过即时编译后的代码已经是纯粹的机器指令流了,这就必须找到一个在机器码层面的手段,把维护卡表的动作放到每一个赋值操作之中。在HotSpot虚拟机
里是通过写屏障(Write Barrier)
技术维护卡表状态的。
写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面
,在引用对象赋值时会产生一个环形(Around)通知
供程序执行额外的动作。
在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier)
在赋值 后的则叫作写后屏障(Post-Write Barrier)
void oop_field_store(oop* field, oop new_value) { // 引用字段赋值操作 *field = new_value; // 写后屏障,在这里完成卡表状态更新 post_write_barrier(field, new_value); }
除了写屏障的开销外,卡表在高并发场景下还面临着伪共享(False Sharing)
问题。
伪共享是处 理并发底层细节时一种经常需要考虑的问题,现代*处理器的缓存系统中是以缓存行(Cache Line)
为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。
假设处理器的缓存行大小为64字节,由于一个卡表元素占1个字节,64个卡表元素将共享同一个缓 存行。这64个卡表元素对应的卡页总的内存为32KB(64×512字节),也就是说如果不同线程更新的对 象正好处于这32KB的内存区域内,就会导致更新卡表时正好写入同一个缓存行而影响性能。
为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元 素未被标记过时才将其标记为变脏,即将卡表更新的逻辑变为以下代码所示:
if (CARD_TABLE [this address >> 9] != 0) { CARD_TABLE [this address >> 9] = 0; }
在JDK 7之后,HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark
,用来决定是否开启卡表更新的条件判断
垃圾收集器就是内存回收操作的具体实现。有的属于新生代收集器,有的属于老年代收集器,所以一般是搭配使用的。
查看垃圾收集器种类指令:java -XX:+PrintCommandLineFlags -version
Serial 收集器是虚拟机在 Client 模式下的默认新生代收集器,它的优势是简单高效,在单 CPU 模式下很牛。
ParNew 收集器就是 Serial 收集器的多线程版本,虽然除此之外没什么创新之处,但它却是许多运行在 Server 模式下的虚拟机中的首选新生代收集器,因为除了 Serial 收集器外,只有它能和 CMS 收集器搭配使用。
它们的关注点与其他收集器不同,其他收集器关注于尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目的是达到一个可控的吞吐量。
吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )
因此,Parallel Scavenge 收集器不管是新生代还是老年代都是多个线程同时进行垃圾收集,十分适合于应用在注重吞吐量以及 CPU 资源敏感的场合。
可调节的虚拟机参数:
-XX:MaxGCPauseMillis
:最大 GC 停顿的秒数;-XX:GCTimeRatio
:吞吐量大小,一个 0 ~ 100 的数,最大 GC 时间占总时间的比率 = 1 / (GCTimeRatio + 1)
;-XX:+UseAdaptiveSizePolicy
:一个开关参数,打开后就无需手工指定 -Xmn
,-XX:SurvivorRatio
等参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,自行调整。回收老年代
参数设置:
-XX:+UseCMSCompactAtFullCollection
:在 CMS 要进行 Full GC 时进行内存碎片整理(默认开启)-XX:CMSFullGCsBeforeCompaction
:在多少次 Full GC 后进行一次空间整理(默认是 0,即每一次 Full GC 后都进行一次空间整理)关于 CMS 使用 标记 - 清除 算法的一点思考:
之前对于 CMS 为什么要采用 标记 - 清除 算法十分的不理解,既然已经有了看起来更高级的 标记 - 整理 算法,那 CMS 为什么不用呢?
- 标记 - 整理 会将所有存活对象向一端移动,需要一个指针来维护这个分隔存活对象和无用空间的点,而CMS 是并发清理的,虽然我们启动了多个线程进行垃圾回收,不过如果使用 标记 - 整理 算法,为了保证线程安全,在整理时要对那个分隔指针加锁,保证同一时刻只有一个线程能修改它,加锁的这一过程相当于将并行的清理过程变成了串行的,也就失去了并行清理的意义了。
- CMS关注的是最短停顿时间,标记 - 清除算法的Stop The World最小。
所以,CMS 采用了 标记 - 清除 算法。
在G1收集器出现之前的所有 其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC)
,要么就是整个老年代(Major GC)
,再要么就是整个Java堆(Full GC)
。
而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
G1不再坚持固定大小以及固定数量的 分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域Region
,每一个Region
都可以 根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
-XX:G1HeapRegionSize
设 定,取值范围为1MB~32MB,且应为2的N次幂。Humongous Region
之中,G1的大多数行为都把Humongous Region
作为老年代 的一部分来进行看待具体的处理思路是让G1收集器去跟踪各个Region里面的垃 圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一 个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis
指定,默 认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。
使用记忆集避免全堆作为GC Roots扫描,但在G1收集器上记 忆集的应用其实要复杂很多。
G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更复杂
G1 收集器则是通过原始快照(SATB)算法来实现的
此外,G1为每一个Region设 计了两个名为TAMS(Top at Mark Start)
的指针,把Region中的一部分空间划分出来用于并发回收过 程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。
G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。
总体而言,对于选择垃圾收集器,需要考虑具体的应用场景和需求,并通过实际测试来得出最合适的结论。随着HotSpot对G1的不断优化,G1在不同场景中的表现可能会持续改善。
目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间,当然,以上这些也仅是经验之谈,不同应用需要量体裁衣地实际测试才能得出最合适的结论,随着HotSpot的开发者对G1的不断优化,也 会让对比结果继续向G1倾斜
从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率 (Allocation Rate),而不追求一次把整个Java堆全部清理干净。
这样,应用在分配,同时收集器在收集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。
这种新的收集器设计思路 从工程实现上看是从G1开始兴起的,所以说G1是收集器技术发展的一个里程碑。
Shenandoah和ZGC是两种在Java平台上开发的具有低停顿时间目标的垃圾收集器。它们的目标是减少长时间停顿(Full GC)的发生,以提高Java应用程序的响应性和性能。以下是关于Shenandoah和ZGC的一些关键特点:
这两个垃圾收集器的共同目标是减少垃圾收集期间的停顿时间,使Java应用程序更具响应性。选择哪个收集器取决于应用程序的具体需求和硬件环境。在Java 11及之后的版本中,开发者可以根据性能要求选择Shenandoah或ZGC,以提高应用程序的性能和用户体验。
Epsilon垃圾收集器是一种特殊的垃圾收集器,它在Java中引入了一种不进行垃圾收集的策略。Epsilon垃圾收集器实际上是一种"无操作"的垃圾收集器,它不会执行任何垃圾回收操作,而是允许堆内存不断增长,直到达到操作系统的限制。
Epsilon垃圾收集器的设计目标是用于某些特殊用途,例如:
Epsilon垃圾收集器并不适用于大多数常规Java应用程序,因为它不会回收堆内存中的垃圾,这可能导致内存泄漏。它适用于那些确切了解自己的应用程序行为并且明确知道不需要垃圾收集的情况。
Epsilon垃圾收集器是Java 11中引入的,可以通过命令行参数 -XX:+UseEpsilonGC
启用。但大多数Java应用程序仍然使用其他垃圾收集器,例如G1、ZGC或Shenandoah,以满足它们的垃圾收集需求。
应用程序的主要关注点是什么?
如果是数据分析、科学计算类的任务,目标是能尽快算出结果, 那吞吐量就是主要关注点;
如果是SLA
应用,那停顿时间直接影响服务质量,严重的甚至会导致事务超时,这样延迟就是主要关注点;而如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是不可忽视的。
SLA(Service Level Agreement,服务级别协议)应用通常是指在服务提供者和服务使用者之间制定和遵守的一种协议,其中规定了服务的质量、性能、可用性等方面的标准和承诺。SLA应用通常与服务提供者和客户之间的服务交付和接受有关,特别是在云计算、网络服务、托管服务和其他IT服务领域。
以下是一些SLA应用的示例:
- 云服务提供商:云服务提供商通常与客户签订SLA,以规定云计算服务的性能、可用性、数据备份、安全性等方面的承诺。如果云服务提供商未能满足SLA中的承诺,可能需要提供赔偿或补偿。
- 网络服务:网络服务提供商通常与企业客户签订SLA,以规定网络连接的可用性、带宽、延迟等方面的服务质量。SLA可用于确保网络服务符合业务需求。
- 托管服务:托管服务提供商通常与客户签订SLA,以规定托管服务的性能、可用性、安全性和数据备份等方面的标准。这有助于确保托管的应用程序和数据的可靠性。
- 电子商务:在线商店和电子商务平台可能与物流服务提供商签订SLA,以确保订单交付的时间和质量达到一定标准。
- 移动应用程序和游戏:开发者和移动应用程序平台或游戏服务提供商之间可以签订SLA,以规定应用程序或游戏的性能、稳定性和可用性。
运行应用的基础设施如何?
譬如硬件规格,要涉及的系统架构是x86-32/64、SPARC还是 ARM/Aarch64;
处理器的数量多少,分配内存的大小;选择的操作系统是Linux
、Solaris
还是Windows
等
使用JDK的发行商是什么?版本号是多少?
是ZingJDK/Zulu
、OracleJDK
、Open-JDK
、OpenJ9
或是其他公司的发行版?
该JDK对应了《Java虚拟机规范》的哪个版本?
一般来说,收集器的选择就从以上这几点出发来考虑。举个例子,假设某个直接面向用户提供服 务的B/S系统准备选择垃圾收集器,一般来说延迟时间是这类应用的主要关注点,那么:
如果有充足的预算但没有太多调优经验,那么一套带商业技术支持的专有硬件或者软件解决方案是不错的选择,Azul公司以前主推的Vega系统和现在主推的Zing VM是这方面的代表,这样你就可以使用传说中的C4收集器了。
C4(Continuous Concurrent Compacting Collector)是一种用于Java虚拟机(JVM)的垃圾收集器,它专注于降低垃圾收集引起的停顿时间。C4收集器的目标是在减少停顿时间的同时提供高吞吐量和良好的性能。它是以低停顿时间为特色的垃圾收集器。
以下是C4垃圾收集器的一些关键特点:
- 低停顿时间:C4收集器的设计目标是实现极低的停顿时间。它通过并发标记、并发标记-清除和并发整理等技术,使垃圾收集的大部分工作在应用程序运行时进行,从而降低了停顿时间。
- 适用范围:C4收集器适用于需要快速响应和低延迟的应用程序,如在线事务处理系统、Web应用程序和其他对停顿时间要求较高的应用。
- 全局并发:C4采用全局并发的方式,允许垃圾收集器与应用程序线程并发工作,而不是在停顿期间独占堆内存。
- 分代收集:C4收集器通常使用分代收集策略,将堆内存划分为不同的代。这使得它可以更有效地管理内存,降低了垃圾收集的频率。
- 自适应调整:C4收集器具有自适应调整的能力,可以根据应用程序和硬件环境的变化自动调整其行为。
需要注意的是,C4垃圾收集器通常不是Oracle JDK的默认垃圾收集器,而是一种商业JVM的特性,如Azul Zing。C4垃圾收集器在一些商业JVM中提供,而不是在开源JVM中普遍使用。选择使用C4垃圾收集器通常需要根据具体的商业JVM产品进行配置和许可。
如果没有足够预算去使用商业解决方案,但能够掌控软硬件型号,使用较新的版本,同时又特别注重延迟,那ZGC很值得尝试。
Z Garbage Collector(ZGC)是一种用于Java虚拟机(JVM)的垃圾收集器,旨在降低大型Java应用程序的停顿时间。ZGC是由Oracle开发的,并于Java 11中首次引入。以下是ZGC垃圾收集器的一些关键特点和优势:
- 低停顿时间:ZGC的主要设计目标之一是降低停顿时间。它采用了一种并发的方式来执行垃圾收集,以减少应用程序的停顿时间。通常,垃圾收集过程中的停顿时间在几毫秒到几十毫秒之间,这对需要快速响应的应用程序非常有利。
- 大堆支持:ZGC适用于非常大的堆内存,可以处理几十GB甚至上百GB的堆内存。这使其适合大型数据处理应用和内存密集型应用。
- 并发处理:ZGC的标记、清理和整理阶段是并发进行的,这意味着垃圾收集过程与应用程序线程并行执行。这有助于减少停顿时间。
- 可预测性:ZGC致力于提供可预测的停顿时间,这对于需要满足服务级别协议(SLA)的应用程序非常重要。
- 无需特殊硬件:ZGC不需要特殊的硬件支持,可以在标准的x86架构上运行。
- 多平台支持:ZGC支持多种平台,包括Linux、Windows和macOS。
需要注意的是,ZGC并不适用于所有应用程序。它在大型内存需求和低停顿时间要求的情况下表现最佳。对于小型应用程序,传统的垃圾收集器(如G1或CMS)可能足够了。在选择ZGC时,还需要考虑Java版本的兼容性,因为它是从Java 11开始引入的。
总的来说,ZGC是一种在大型内存应用程序中降低停顿时间的有效垃圾收集器,特别适用于需要可预测性和低延迟的应用程序。
如果接手的是遗留系统,软硬件基础设施和JDK版本都比较落后,那就根据内存规模衡量一 下,对于大概4GB到6GB以下的堆内存,CMS一般能处理得比较好,而对于更大的堆内存,可重点考察一下G1。
新生代和老年代的 GC 操作
Minor GC
Full GC / Major GC
Minor GC
; Minor GC
慢上 10 倍以上。-XX:MaxTenuringThreshold=?
),则将此对象移动到老年区。-Xmx
:Java 堆的最大值;-Xms
:Java 堆的最小值;-Xmn
:新生代大小;-XX:SurvivorRatio=8
:Eden 区 / Survivor 区 = 8 : 1-XX:PretenureSizeThreshold
:单位是字节;
Serial
和ParNew
两款收集器有效。-XX:MaxTenuringThreshold
设定值后,会被晋升到老年代,-XX:MaxTenuringThreshold
默认为 15;我们知道,新生代采用的是复制算法清理内存,每一次 Minor GC,虚拟机会将 Eden 区和其中一块 Survivor 区的存活对象复制到另一块 Survivor 区,但 当出现大量对象在一次 Minor GC 后仍然存活的情况时,Survivor 区可能容纳不下这么多对象,此时,就需要老年代进行分配担保,即将 Survivor 无法容纳的对象直接进入老年代。
这么做有一个前提,就是老年代得装得下这么多对象。可是在一次 GC 操作前,虚拟机并不知道到底会有多少对象存活,所以空间分配担保有这样一个判断流程:
HandlePromotionFailure
参数,看看是否允许担保失败;
Minor GC
;Full GC
;HandlePromotionFailure
参数就没有用了,规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则将进行 Full GC。Java 8 彻底将永久代 (PermGen) 移除出了 HotSpot JVM
,将其原有的数据迁移至 Java Heap 或 Metaspace。
移除 PermGen 的原因:
PermGen
内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen
,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的OOM
;PermGen
可以促进HotSpot JVM
与JRockit VM
的融合,因为 JRockit
没有永久代。移除 PermGen 后,方法区和字符串常量的位置:
Metaspace
; Java Heap
。Metaspace 的位置: 本地堆内存(native heap)。
Metaspace 的优点: 永久代 OOM 问题将不复存在,因为默认的类的元数据分配只受本地内存大小的限制,也就是说本地内存剩余多少,理论上 Metaspace
就可以有多大;
JVM参数:
-XX:MetaspaceSize
:分配给类元数据空间(以字节计)的初始大小,为估计值。MetaspaceSize
的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。-XX:MaxMetaspaceSize
:分配给类元数据空间的最大值,超过此值就会触发Full GC
,取决于系统内存的大小。JVM会动态地改变此值。-XX:MinMetaspaceFreeRatio
:一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最小比例,不够就会导致垃圾回收。-XX:MaxMetaspaceFreeRatio
:一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最大比例,不够就会导致垃圾回收。