淘宝的技术团队对Java虚拟机的优化工作其实早已不是停留在简单的参数调整上面,而是充分结合了企业自身的业务特点以及实际的应用场景,在OpenJDK的基础之上通过修改大量的HotSpot源代码,深度定制了淘宝专属的高性能Linux虚拟机TAOBAOVM。从严格意义上来说, 在提升Java 虚拟机性能的同时,严重依赖于物理CPU类型。也就是说,部署有TAOBAOVM 的服务器中CPU ,全都是清一色的Intel CPU,且编译手段采用的是Intel C/CPP Compiler进行编译,以此对GC性能进行提升。除了优化编译效果外,TAOBAOVM 还使用crc32 指令降低 JNI 的调用开销。除了在性能优化方面下足了功夫, TAOBAOVM还在HotSpot 的基础之上大幅度扩充了一些特定的增强实现,比如创新的GCIH (GC invisible heap)技术实现offheap,长生命周期的大对象直接移到offheap,降低gc压力。
引言主要说明,淘宝的技术团队已经需要在JVM层面着手优化自己的性能,可见其对性能的极致追求,而目前自己在实践过程中,关注的还是GC的选择和参数的调优。
简而言之,JVM就是一台执行java字节码的虚拟计算机。其运行的java字节码(可以是二进制文件,也可以来自数据库,甚至网络)未必由java语言编译而成。作为面向对象的语言,将对象的创建、使用与回收过程进行职责分离,应用程序只需关注前者,其执行过程中产生的对象垃圾不再需要编程手动回收,极大的降低了程序开发难度,减少出错,提升开发效率。
一个封闭系统总是朝着熵增的方向发展的,必然越来越混乱,所以对于java来说,GC是必然也必须的。
了解GC必须掌握JVM,而掌握JVM的关键从JMM内存结构,GC算法,垃圾回收器,JVM调优,类加载机制,ASM(字节码)技术逐步深入。
我们开发程序时必须对应用进程可能产生的stop the world(STW)有更好的感知,这要求我们在编写代码时,必须根据业务场景预估虚拟机分配对象和调用对象的行为。如棋者过招,亦如智者看穿。
STW时,GC事件发生过程中,停止所有应用线程的执行。因此,当你的程序突然执行缓慢时,你首先就要考虑和观察内存使用情况。
应用使用GC的架构原则
XXX应用部分进程使用CMS,部分进程使用G1,没有比较,就没有伤害。当然这种比较取决于应用场景。要为真实场景选择更合适的GC机制,将参数调整到最为合适。
使用CMS的应用(管理端应用,客户端应用),内存占用一般设为小于等于8G,调度引擎内存会设置为4G,查询为主进程一般设置为8G,应当关注大对象的分配和使用,也就是关注大数据量的导入和导出操作。为什么要关注大对象下文会介绍。
使用G1的应用,会开启超过8G的内存空间,一般我们会使用16G(计算应用:收益率计算引擎、行情计算引擎,搜索引擎,权限计算引擎等),或者32G(应用数据库:Cassandra,智能推荐平台)。
使用G1时特别注意,选择的jdk版本不同,G1新增特性不同,如果需要针对性的特性,请选择合适的jdk版本。相对来讲,jdk11之后,G1已成熟。但jdk11和jdk8不兼容,不兼容。当前的G1感觉有一统天下之趋势。但是红帽给我们的建议竟然是继续使用CMS,比较无语。
JMM内存结构
JMM由方法区,堆,Java栈,pc寄存器,本地方法栈,执行引擎,类装载器组成。
JVM实例和JVM执行引擎实例区别:JVM实例对应一个独立运行的java程序,是进程级的,JVM执行引擎实例则对应属于用户运行程序的线程,是线程级的。
当程序中所有非守护线程终止时,JVM自动退出,你可以向jvm中注册一个hook,当jvm自动退出之前,会执行这个片段。
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
System.out.println("auto clean temporary file");
}
}));
————————————————
版权声明:本文为CSDN博主「江湖人称小白哥」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/dd864140130/article/details/49155179
主线程:main启动时的线程,作为JVM实例运行的起点。
非守护线程:用户线程,由主线程或者其它线程创建。
守护线程:程序运行时在后台提供通用服务的线程,与主线程同时销毁。一般用于应用或框架创建的监控、调度或清理有关功能的线程。
thread.setDaemon(true) 设置线程为守护线程。
程序计数器看作当前线程所执行的字节码的行号指示器,确保线程切换时能够回到正确的执行位置 。 它是一个很小的空间,是运行最快的区域,java应用程序不能直接控制寄存器。
本地方法栈供C语言函数调用。当java程序调用本地方法native时,就进入一个全新的不再受虚拟机限制的世界。
战帧由三部分组成,即局部变量区( Local Variables)、操作数栈( Operand Stack)和帧数据区( Frame Data)。
栈物理上是事先创建好的内存区域,在jvm中是通过jit使用native stack。
当虚拟机调用本地方法时,会保持本地栈帧不变
堆是所有线程共享的内存区域,是通用内存,用于存放所有对象。
注意堆是逻辑连续的而不是物理连续的
理解JMM堆的关键是理解栈和堆的区别,堆和栈的代码实现可以参考数据结构,非常简单。
在JVM中,静态属性保存在Stack指令内存区,动态属性保存在Heap数据内存区。
1)栈是运行时的单位,而堆是存储的单位。
2)栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
理解堆内存分析中两个重要概念:浅堆和深堆
浅堆(Shallow Heap):对象所消耗的内存。
深堆(Retained Heap) :对象被垃圾回收后可以释放的内存。包含对象及对象引用的全部对象所消耗的内存。特别注意,当一个对象C被对象A,B同时引用时,A对象的深堆不包含对C的引用部分,因为C还被B引用,不能因A直接被回收。
堆中有一个常量池
类文件中常量池(The Constant Pool)
运行时常量池(The Run-Time Constant Pool)
String常量池
字符串常量池则存在于方法区
其工作在编译时,也就是说如果都是静态变量
字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
GC算法
垃圾回收过程是应用性能和垃圾回收器之间的博弈过程。但是程序写的烂,GC再努力博弈,也救不了你。
必知必会的垃圾回收算法:
-
引用计数法
这是最基本想到的算法,其最为经典,最为古老,存在先天缺陷。
如果一个对象A被其它对象引用,就+1,如果取消引用就-1。只要引用为0,该对象就不再使用,可以回收。这种算法会无法解决循环引用这一流氓问题。
-
根搜索算法
为了解决循环引用问题,所以基于根对象的可达性分析思想设计了根搜索算法,如下图
如果对象已经不可达,则回收,显然这种方法更为科学。
那么什么对象才能成为根对象?这里根对象很关键,遍历算法也必须足够高效。
实现时,基于可达性分析,从GCRoots 开始向下搜索引用链,如果不在链中,也就是堆中的对象没有被引用到,则该对象可以被回收。
GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
详解图:
这种回收算法的效率,是存活对象的数量是线性相关的。所以,不要使用大量的小的,存活期长的活动对象,而且一旦不用,立即赋值null,是一个影响效率的好习惯。这中回收算法也为回收性能考量提供了参考,空间越大,存活对象越多,回收效率越低。参考Java GC 内存回收机制详解(二)GC Roots 和 可达链
具体的标记方法:着色标记,并进行三颜色标记,具体标记算法如下:
黑色(black):节点被遍历完成,而且子节点都遍历完成。
灰色(gray): 当前正在遍历的节点,而且子节点还没有遍历。
白色(white):还没有遍历到的节点,即灰色节点的子节点。
- 标记清除算法(Mark-Sweep)
大名鼎鼎的CMS就使用了标记清除算法。可以说奠定了现代垃圾回收算法的思想基础。
简单将就是将垃圾回收分为两个阶段:标记阶段和清除阶段,这个标记清除其实根搜索算法动作是一样的。
这种算法最大问题就是回收后空间不再连续,尤其针对大对象,不连续的空间会降低工作效率。
从此之后的算法设计要解决的两个问题是:
怎样实现更精准清除、更高效清除效率?
怎样确保回收后的空间尽量连续完整?
- 复制算法(Copying)
为了解决上面的问题,自然想到水桶接水的方法。将原有堆空间分为两份,一份使用,一份空闲,回收时,将仍然存活的对象(正常情况下存活对象非常少)复制到空闲的一份中,因存活对象少,效率非常高,然后清空使用那份的内存,标记空闲的空间为使用,使用的为空闲。
如此解决了空间连续性的问题,也提高了清除效率。
但带来了新问题,GC时需要将全部存活的对象从Eden搬到Survivor区,空间利用率低,只能使用一半的空间。
为了解决空间浪费的问题:
在java新生代串行回收器中,使用了改进的复制算法。将新生代分为eden区,survivor from区和survivor to区,默认8:1:1,实际存活的对象一般认为非常少,所以并不需要对半分。
eden和survivor区职责更加明确,不再必须交换。eden的存活对象直接复制到survivor to区,survivor from区的因为接收逃逸的存活对象也直接复制到survivor to区。这样实际垃圾回收的是eden区和survivor from区。回收时其实是survivor from和survivor to区互相交换,这样浪费的内存空间就极大的减少了。大对象直接进入老年代。体现了回收的针对性。
现在似乎大部分问题都解决了,但是如果存活对象非常多,存活时间非常长,那么复制算法的效率就会大打折扣,GC时间就没有必要的被拉长了。
- 标记-压缩算法(Mark-Compact)
标记压缩算法是一种老年代算法。
和标记清除一样,但做了优化,在标记完成之后,将所有存活对象压缩到内存的一端,然后清理边界外的所有空间,完成回收。这样同样保证了连续性,保证了对象访问的效率,但不需要浪费空间,减少复制次数。
到这里我们觉得垃圾回收思想已经非常完整了,但是在实践中,我们会发现垃圾回收暂停的时间可能会比较长,尤其是当开辟的内存空间比较大的时候。现在XXX系统,为了提升吞吐量,我们内存开辟都是8G,16G,32G的初始起步,所以当垃圾到来时,我们需要更强的算法处理。
- 增量算法:增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。简而言之就是少吃多餐,做到GC和应用服务兼顾。其优点是使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。其缺点也很明显,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
一种算法实现TrainGC(火车算法),一种算法实现时G1 GC。
Train GC的“精髓”就是试图通过把若干有相互引用的region串在一起,保证它们总是一起被收集,来避免让每个region都要关联巨大的remembered set。G1 GC则不偷这个懒,确实给每个region都关联上了一个大remembered set,但通过一些压缩技巧来减小这些大remembered set的开销。这样G1就比train有更高的自由度来选择一次增量式GC要收集哪些region及不收集哪些region,从而达到了更好的效果。最终偷懒的死掉了,踏实的得到了更好的发展。
- 分代收集算法(Generational Collecting)
分代算法就是根据对象的特性,将内存区域不同的几个区域,不同区域使用不同的回收算法,以提高回收效率。分代搞定了GC的性能压力,垃圾回收器只需要专注扫描特定区域,不用再整个堆扫描。
新生代:存放新生代对象的堆空间,新生代对象指刚刚创建的或者经历了几次垃圾回收过程的对象,一般都是朝生夕灭。大多数内存块的生存周期都比较短,垃圾收集器应当把更多的精力放在检查和清理新分配的内存块上。新生代对象比较适合使用改进后的复制算法。
老年代:存放老年代对象的堆空间,老年代对象指经历多次垃圾回收仍然存活的对象 ,-XX:MaxTenuringThreshold=数字 参数可以设置对象在经过多少次GC后会被放入老年代(年龄达到设置值,默认为15)。老年代对象回收时存活率非常高,所以适合标记压缩或者标记清除算法。
永久代 :G1已经废弃永久代,这里不再探讨。简单说就是不会被回收的对象放在永久代。
实践中新生代回收频率高,老年代非常低,但消耗更多时间,为了进一步支持高频率的新生代回收,避免新生代扫描时,必须通过新生代对象的引用传递,不得不全量扫描老年代,增加了卡表的数据结构(Card Table),用来标记老年代某一区域是否持有对新生代的引用,如果持有比特位设置为1,否则为0。
到此为止,我们垃圾回收算法已经算非常完整了,但是现在应用为了提高吞吐量和处理的数据量,应用程序往往都走上了简单增加内存空间策略的道路。这时每次Full GC停顿的时间会变得非常长(用户可感知),所以如何降低GC停顿的时间,成为优化的重点方向。
- 分区算法(Region)
分区算法就是将连续的空间分成很多不同的小空间,独立使用,独立回收。
相同条件下,堆空间越大,回收时间越长,停顿时间越长。通过将空间切分,每次只针对部分区域回收,而不是这个空间,从而减少一次GC停顿的时间。
实践中分区算法一定程度改观了STW对应用提供服务能力的消弱。
为啥大小必须相等,可以不等吗?我觉得完全可以不相等啊。每个 Region 都有一 个关联的 Remembered Set (简称 RS), RS 的数据结构是 Hash 表,里面的数据是 Card Table (堆 中每 512byte 映射在 card table 1byte) 。简单地说, RS 里面存在的是 Region 中存活对象的指针当 Region中数据发生变化时,首先反映到 CardTable中的一个或多个 Card上, RS通过扫描内 部的 Card Table 得知 Region 中内存使用情况和存活对象。在使用 Region 过程中,如果 Region 被填满了,分配内存的线程会重新选择一个新的 Region, 空闲 Region 被组织到 一个基于链表的 数据结构 CLinlf:edList)里面,这样可以快速找到新的 Region。
经历了漫长的垃圾回收算法不断迭代优化的心路历程之后,我们必须思考一个深入的问题:垃圾回收算法下一部发展的方向是什么?
- 更智能化:引入AI技术,理解应用场景,对内存和CPU使用趋势做出预测,并针对内存空间做针对性的优化。
- 支持人工定义和标记对象特性,根据对象特性自动分代分区。
- ......目前从java8-java13 G1仍在发展和完善,但垃圾回收算法思想已经相对停滞,我们缺少的可能还是想象力。
垃圾回收器
串行回收指的是在同一时间段内只允许一件事情发生,简单来说,当多个 CPU 可用时,也只能有一个 CPU用于执行垃圾回收操作,井且在执行垃圾回收时,程序中的工作线程将会被暂停,当垃圾收集工作完成后才会恢复之前被暂停的工作线程,这就是串行回收。串行收集器主要有两个特点。首先,它仅仅使用单线程进行垃圾回收。其次,它是独占式的垃圾回收方式。并行收集器是工作在年轻代的垃圾收集器,它只简单地将串行回收器多线程化。串行回收器虽然原始,但可靠,经受住了生产环境的严苛考验。
JVM有如下主流回收器
- Serial垃圾收集器 :单线程垃圾收集器, -XX:UseSerialGC 参数设置开启,新生代和老年代都使用单线程回收。新生代使用改进的复制算法,老年代使用标记压缩算法。
- Throughput垃圾收集器 :多线程并行垃圾收集器,就是将Serial改成多线程版本。Parallel GC根据MinorGC和FullGC的不同分为三种,分别是ParNewGC、ParallelGC和ParallelOldGC。
ParNew回收器,工作在新生代。回收策略和串行一致。
ParallelGC也使用复制算法。但其相比ParNew偏重系统吞吐量,支持自适应GC调节设置。有两个参数控制:MaxGCPauseMillis设置最大垃圾收集停顿时间,GCTimeRatio设置吞吐量大小。
ParallelOldGC是关注吞吐量的老年代垃圾回收器,使用标记压缩算法。
-XX:UseParNewGC :新生代使用ParNew,老年代使用串行回收器
-XX:UseParallelGC:新生代使用ParallelGC,老年代使用串行回收器
-XX:UseParallelOldGC:新生代使用ParallelGC,老年代使用ParallelOldGC回收器。
- 经典CMS并发标记清除垃圾收集器
CMS垃圾回收器设计初衷是为了消除Throughput收集器和Serial收集器Full GC周期中的长时间停顿。也就说CMS是一个非常关注停顿的垃圾回收器,意图提供更好的应用体验,所以一般客户端交互的Server应用建议使用CMS。CMS是老年代垃圾回收器(特别注意),需要配和其它新生代垃圾回收器一起使用。
对应虚拟机GC日志过程如下
CMS-initial-mark
CMS-concurrent-mark-start
CMS-concurrent-mark
CMS-concurrent-preclean-start
CMS-concurrent-preclean
CMS-concurrent-abortable-preclean-start
CMS-concurrent-abortable-preclean
CMS-remark
CMS-concurrent-sweep-start
CMS-concurrent-sweep
CMS-concurrent-reset-start
CMS-concurrent-reset
其在初始标记和重新标记阶段是独占系统资源的(不需要开始结束过程),其它阶段可以和应用线程一起使用。CMS垃圾回收器最大的特点是在垃圾回收阶段不再整体独占,将回收分成6个阶段,只2个阶段独占CPU。
-XX:UseConcMarkSweepGC:新生代使用ParNew回收器,老年代使用CMS
由于CMS执行时是并发的,应用程序和垃圾回收线程会交替执行,所以还会产生新的垃圾,所以垃圾回收时,不是等满了才启动收集,而是老年代到达回收阈值时,就触发回收,通过CMSInitialatingOccupancyFraction指定,默认68%。
CMS是标记清除算法,所以存在内存碎片,离散的可用空间不能分配较大的对象。用UseCMSCompacAtFullCollection参数设置在进行几次垃圾回收之后,进行一次碎片整理。
CMS使用中如果频繁出现concurrent mode failure日志,表明并发收集失败,很可能是老年代不够导致的,那么应当考虑增加老年代空间。
- G1垃圾收集器
新一代垃圾回收器设计初衷是为了尽量缩短超大堆时产生的停顿。G1就是将分代和分区算法思想进行了结合。在G1中堆被平均的分割成若干大小相等的Region。G1同时作用于新生代和老年代。
那么为什么叫G1 (Garbage First Garbage Collector,垃圾优先的垃圾回收器) ? 顾名思义,优先回收垃圾多的region。
G1充分吸收了CMS的并发标记的优点,其工作阶段分为:
- 新生代GC
- 并发标记周期(多个阶段,非常细)
- 混合收集
- 如果需要,会触发Full GC
G1新生代GC会同时回收eden区和survivor区。至少保留一个survivor区(请想一下为什么?前面讲过,survivor from -> survivor to),老年代区可能会增多。
G1的并发标记周期分为如下阶段
初始标记阶段:标记从根节点可直接到达的对象。这个阶段会伴随一次新生代GC,会产生全局停顿。eden清空,存活对象全部到survivor区。
根区域扫描:扫描从survivor可直达老年代的区域,并标记可直达的对象,这个可以和应用程序并行执行,意味着这个阶段会产生新的eden区,但不可和新生代GC并行,所以只有根区域扫描结束,才会进行新生代GC。
并发标记:和CMS类似,从整个堆中找到和标记存活对象。可以并发执行,可以被新生代GC打断。
重新标记: 修正并发标记结果,此过程产生停顿,这一过程使用SATB (Snapshot-At-The-Beginning)算法进行。
具体如何做呢?
参考G1 SATB和Incremental Update算法的理解
如图出现如下情况,我们都知道cmg gc 和g1 gc 都是和程序有并行执行的阶段。既然有并行,那就有可能在并行运行期间之前的标记过的对象的引用关系可能被改变,比如一个白色对象从被灰色的引用变为被黑色的对象引用。如果不做处理,那这个白色的对象会被漏掉,会被错误的回收。会导致程序错误。
这也是cms gc 和g1 gc 都有remark阶段的原因。都需要重新对被修改的card 进行扫描。
cms gc 是Incremental Update算法,g1 gc 是采用的 stab 算法。
- 把一个白对象的引用存到黑对象的字段里,如果这个情况发生,因为标记为黑色的对象认为是扫描完成的,不会再对他进行扫描。只能通过灰色的对象
- 某个白对象失去了所有能从灰对象到达它的引用路径(直接或间接)
SATB 即 Snapshot-at-beginning,satb 算法认为开始标记的都认为是活的对象,如上图所示,引用B到D 的引用改为B到C时,通过write barrier写屏障技术,会把B到D 的引用推到gc 遍历执行的堆栈上,保证还可以遍历到D对象,相对于d来说,引用从B-->A,SATB 是从源入手解决的,即上面说的第2种情况,
这也能理解为啥叫satb 了,即认为开始时所有能遍历到的对象都是需要标记的,即都认为是活的。如果我吧b = null,那么d 久是垃圾了, satb算法也还是会把D最终标记为黑色,导致D 在本轮gc 不能回收,成了浮动垃圾。
Incremental Update write barrier,Incremental Update 算法判断如果一个白色的对象由一个黑色的对象引用,即目的,如上图,D的引用由B-->A,A是目的地址,所以cms 的Incremental Update算法是从目标入手解决的,这是和SATB的第一个区别,发现这种情况时,也是通过write barrier写屏障技术,把黑色的对象重新标记为灰色,让collector 重新来扫描,活着通过mod-union table 来标记,cms 就是这样实现的,这是第二个区别,做法不一样,也是上面讲的防止第一种情况发生。
Incremental Update 和 SATB 的区别。通过上面的分析,我们知道SATB write barrier 是认为开始标记那一刻认为都是活的,所以有可能有些已经是垃圾的对象就会也被扫描,导致 satb 相对 Incremental Update 会更多的开销,g1 gc 扫描的都是选定的固定个数的region,所以这个开销应该可控,但是而且浮动垃圾也更多。
独占清理,计算各个区域存活对象和GC回收比率并排序(回收比),识别可供混合回收的区域(G),这些区域垃圾比较多,更新记忆集(Remembed Set),这个阶段产生停顿。
并发清理阶段,识别并清理完全空闲的区域,这个阶段不停顿。根据独占清理阶段计算出来的每个区域的存活对象,直接回收没有存活对象的区域
GC pause (young)(initial-mark)
GC concurrent-root-region-scan-start
GC concurrent-root-region-scan-end
GC concurrent-mark-start
GC concurrent-mark-end
GC remark
GC cleanup
GC concurrent-cleanup-start
GC concurrent-cleanup-end
- 混合回收
并发标记后,G1明确知道哪些region垃圾比较多。混合回收阶段,就有针对性。因为回收这些高垃圾占必的区域性价比高。这个阶段既进行年轻代回收,也会针对标记的老年代进行回收,Eden区被完全清理,上一步标记为G的区域被清理,被清理区域存活的对象会被移动到其它区域,减少空间碎片。
-XX:UseG1GC 开启使用G1垃圾回收器。
-XX:ConcGCThreads 并行执行GC的线程数,默认1/4核数 ,过大GC时CPU过高,过小回收时间变长
-XX:G1 HeapRegionSize 增大有利于大对象处理。大对象没有按照普通对象方式进行 管理和分配空间,如果增大 Region块的大小,则一些原本走特殊处理通道的大对象就可以被纳 入普通处理通道了。设置的过小,降低灵活性
配置参考
-XX:+PrintGCDetails
-verbose:gc
-Xloggc:gc.log
-XX:+UseGIGC
-XX:+Prin的CApplicationStoppedTirne
-XX:ConcGCThreads=2
-XX:G1HeapRegionSize=32M
如下是垃圾回收器G1针对大对象做的特别优化
如何定义大对象?
假设,当前每个区间的大小定义为2MB, 一个数组大小为1MB 。这个数组会被认定为大对象吗?是的,这是因为数组大小=数组内部对象的头大小+对象大小,即这个数组超过了1MB, 即(大对象)超过了区间大小的 50%,符合大对象的定义要求,属于大对象 。
大对象为何在物理空间上连续?
如果在回收阶段尝试去频繁地回收大对象,存在两个缺点:
- 移动对象需要拷贝数据,大对象较大,拷贝效率终究会成为瓶颈
- 很难找到连续的堆内存用于大对象存储,回收越频繁,越容易出现相互独立的区间
- Region得分配是连续的,空闲的Region加入一个LinkedList队列中。
大对象会在GC的那些阶段回收?
jdk8u40之后,年轻代回收、并行回收循环、 Full GC,它们都会参与到大对象区间的回收工作。
- ZGC最新的垃圾回收器
主要就是解决大数据下垃圾回收的STW问题。ZGC由Oracle开发,承诺在数TB的堆上具有非常低的暂停时间。参考一语道破Java 11的ZGC为何如此高效
这里暂不详述。
JVM调优
暂不表。
jvm会为每个对象创建一个对象头,保留系统信息,其中就包含对象锁的信息。
偏向锁,jdk1.6之后,对象锁使用偏向锁,即一个线程锁住一个对象后,下次再使用这个对象时,不再进行相关的同步操作。
偏向锁依据的原理是如果一个线程最近用到了锁,那么线程下一次执行由统一把锁保护的代码所需的数据可能仍然保存在处理器缓存中。如果这个线程获得这把锁的权利,缓存命中率可能就会增加。
类加载机制
java类加载使用双亲委派模型,这个模型我看代码,觉得没有那么高深晦涩。实现都是比较简单的。
加载 → 连接 → 初始化
连接:验证 → 准备 → 解析
节省内存使用的方法有哪些?
1.减少对象使用。仅定义需要的实例对象,但不要尝试用int替代long,float替代double,意义不大,反倒增加了限制(因为jvm针对对象数组,会做对齐处理)
2.延迟初始化。一个对象引用的对象如果不是时刻必须的,尽量在该存在的生命周期里再初始化。对于非线程安全的对象,引用对象的初始化,要加线程保护机制。
3.使用不可变对象和标准对象。例如Boolean对象,其实应该设计成私有的的,只用Boolean.TRUE和Boolean.FALSE
4.字符串保留,理解intern方法。字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能
5.尽量避免对象重用,避免使用对象池技术,只有在对象的创建成本非常高的情况下(例如数据库连接池),对象池中的对象非常容易进入老年代,反倒增加每次Full GC的成本。需要重用的对象:
线程池
JDBC池
大数组
原生NIO缓冲区
安全相关类MessageDigest Signature
随机数生成器
ZIP解释码器
...
当然对于大部分开发人员,我们根本就不会考虑到这层优化的水准。因为想起来容易,做起来太难了,会极大降低开发的效率,增加工作量,所以更为科学的操作是,关注对象的生命周期
参考资料
深入理解JVM & G1 GC
深入理解java虚拟机
实战java虚拟机
java性能权威指南
Java:JVM垃圾回收(GC)机制
大神教你JVM运行原理及Stack和Heap的实现过程