【学习JVM】内存垃圾回收与调优

新生代的垃圾回收过程

需要参考的准备数据:

《深入理解JAVA虚拟机》
《Oracle技术网/java/G1 GC调优》

需要参考的知识点:
字节码结构
java内存区域
对象的引用
理论上的GC算法和实际落地的GC收集器

知识的记录方式:
 - 查看博客,把没有遇见过的或者觉得比较经典的博文段落摘录
 - 自己的理解以条目的形式展示
 - 知识误解标记
 - 知识盲区标记
 - JAVA内存模型这个知识点基本上每本书都会讲解,最好的方式将每本的书的这一章都读一下,然后摘录重要的知识点,通过反复和串联达到效果。

重要笔记:
  • java中内存对象按照创建方式分类3种“数组类对象创建” 和 “java.lang.Class对象”、“普通java堆对象”。

  • 对象创建内存分配涉及两个问题“分配方式”和“并发问题”

  • 关于分配问题,最简单的内存分配方案就是“指针碰撞”,“指针碰撞”即在一块被分割成“已使用区”和“未分配区”的规整内存上通过移动指针来实现空闲内存分配的方式。如果在一块“已使用”和“未使用”参差的不规整内存上分配,需要借助一个“空间列表free list”,这种分配方式就叫“Free List”。其中带compact过程的Serial与ParNew收集器采用的就是“指针碰撞”,而采用“Mark-Sweep”算法的CMS算法采用是“Free List”分配方式。

  • 关于并发问题,可以采用同步加锁的方法来解决并发问题。还可以使用线程本地分配缓存(Thread Local Allocation Buffer TLAB)来实现,让各个线程只在自己的内存区域分配内存从而免除资源竞争问题。

  • 对象在内存中的存储布局分为3个部分:“对象头”、“实例数据”、“对齐填充”。其中对象头分为“Mark Word“和“Class类型指针”; “实例数据”包括了所有父类和子类的对象数据; 由于有些JVM的内存管理器要求对象以8个字节为单位来分配内存,所以需要对齐填充。

  • 对象的访问方式有两种:"灵活的句柄池"和“快速的直接指针(hotspot用这种)”

  • 堆用于存储创建的对象,只要不停创建可以被GC Roots路径关联的对象就会触发java heap oom,因为被GC Roots路径关联的对象不会被GC回收。堆内存溢出可以通过-xx:+HeapDumpOnOutOfMemory获取Dump文件,利用MAT(内存分析工具)分析dump文件,查找出引起OOM的原因是“内存溢出”还是“内存泄漏”。如果是内存泄漏可以分析“GC Roots”之间的引用路径找出内存泄漏的代码。如果是内存溢出,则只能通过-xmx与-xms来提高堆空间大小。

  • 虚拟机栈用于分配线程的JVM方法调用栈空间,而本地方法栈则用于分配Native方法调用栈的空间,而-Xoss则用于配制Native方法调用栈的空间。但是在Hotspot中将这两个栈合二为一,所以-Xoss参数无效。-Xss用于配置虚拟机栈的空间大小吗?错误!-Xss设置的是JVM线程每个线程的方法栈大小,而非虚拟机栈区的大小,也就是说虚拟机栈区受限于进程内存资源(由所在操作系统决定,这是一个硬顶),而非JVM参数所能设定的。

  • 创建线程和调用JAVA方法太深都可能会触发OOM异常。但是StackOverflowError异常只可能在调用JAVA方法太深才会触发(超过-Xss)

  • java 1.7开始(包括1.7)已经去掉了永久代的概念以及 永久代的限制-XX:PermSize&-XX:NewPermSize参数。

  • String.intern()方法可以利用方法区的常量池的缓存功能来节省内存,但是不同平台的表现不一致,最好别乱用。

  • 动态类技术的产生会创建很多类,比如Groovy和函数式编程的兴起。这类技术会导致方法区存在很多类数据,从而导致这个方法区很臃肿容易引发OOM。

  • Unsafe.getUnsafe()方法只有BootstrapClassloader才可以返回实例,但是可以通过反射越过这个限制。

  Field unsafeField =       Unsafe.class.getDeclaredFields()[0];
  unsafeField.setAccessible(true);
  // 因为是静态字段,所以传入null即可。
  Unsafe unsafe = (Unsafe)unsafeField.get(null)
  unsafe.allocateMemory(1024*1024);
  • JAVA程序通常用new DirectByteBuffer()来创建直接内存。如果创建失败他会抛出OOM,但是他的OOM可能来自不同地方抛出,一种是通过static void reserveMemory(long size, int cap)手工抛出throw new OutOfMemoryError(“Direct buffer memory”);另一种则是通过unsafe.allocateMemory(size);由系统抛出。不管怎么样这类型OOM的特点就是Dump文件都会很小。
    reserveMemory()方法是用来预测是否内存分配是否有超过JVM的MaxDirectMemory设置上限,这是一个软顶。而allocateMemory抛出的是硬顶。

  • GC算法需要解决3个核心问题:where, when, how ?那些内存可以回收,什么时候回收?怎么回收?

  • JAVA判断对象是否可以被回收的依据是可达性分析(Reachability analysis),这个算法涉及2个概念,GC Roots对象和引用链(Reference Chain)。

  • GC Roots对象包括:所有非堆引用所指向的对象,非堆的内存区域包括了,虚拟机栈区,方法区,本地方法区。

    • 虚拟机栈区栈帧中的本地变量表slot所引用的对象
    • 方法区中静态变量和静态常量所引用的堆对象
    • 本地方法区的变量所引用的堆对象
  • 可达性分析与finalize调用过程:
    在GC时,可达性分析会扫描内存,找出GC Roots之间没有路径的堆对象。然后对它进行判断需要不需要被加入finalize队列(F-Queue)当对象满足条件加入F-queue后,回收器会启动一个finalizer线程去执行下一个回收动作(创建新的线程时出于安全考虑,因为下一个回收动作会执行可能由用户定义的finalize函数,为了避免用户在finalize函数里面陷入死循环等恶意代码,所以另开辟线程时明智的)。下一个回收动作分两个步骤“执行finalize方法”与“扫描标记”(这次只扫描F-Queue队列)。然后再对被标记的对象进行回收。

  • 在执行finalize方法的时候,如果对象重新给对象建立强引用,那么在扫描标记时就会将对象移出F-queue队列,否则标记并
    等待最后的回收。

  • 加入F-queue的条件是: 覆盖了Finalize()方法且该重写方法没有被系统执行过。不满足这个条件的垃圾对象在可达性分析结束后将被回收。

方法区的垃圾回收

方法区的垃圾回收对象主要是 “无引用的常量” “无用的类”

“无引用的常量” 的回收条件跟对象一样。
“无用的类” 的回收条件要求比较苛刻,有一下三点(按出现的概率高到低排序):

  1. 无对象实例
  2. 无classloader
  3. java.lang.class无任何引用。

分析:如果classloader存在,那么java.lang.class一定会被classloader引用。如果有堆对象存在那么classloader也一定存在,类的java.lang.class对象也一定会被引用。

如果classloader只被java.lang.class引用,且没有被其他GC Roots所关联,在没有堆对象的情况下,classloader将会被回收。java.lang.class不是GC Roots对象。

java.lang.class可能还会被反射引用。

经常加载类的OSGi框架,动态代理框架,反射代码,容易造成方法区溢出,所以经常需要使用带有方法区回收功能的虚拟机。

Note: 未标记对象等于死 对象Unmarked objects == Dead Objects

  • Mark-Sweep扫描标记算法,存在两个问题:一个是效率问题,因为每次扫描都需要遍历整块内存,另一个是空间碎片问题,造成空间的得不到充分利用而造成频繁的执行GC。这个算法是最基础的算法,后面算法也是针对这两个缺点在这个基础上进行优化。

  • Copying复制算法,则是将内存分成两块区域,第一块区域只负责分配内存和回收内存,第二块区域只负责临时存放内存。每次GC都将第一块内存的存活对象按顺序Copy到第二块区域,剩余在第一块区域的垃圾将被一次性清除。这个算法在效率上比较好,而且没有碎片存在。但它是一种牺牲空间的做法以及如果存活对象较多复制性能会变得很差。这也是一个最基础的算法。

  • 新生代采用的就是Copying复制算法。新生代为了降低空间的浪费,做了一个假设的大前提:“大部分的对象都会被GC回收”。在这个假设下,新生代把内存划分为3块分别是“伊甸区Eden”和“Servivor1”、“servivor2”。其中Eden区和Servivor1区当做第一个区用于分配内存。servivor2作为第二个区。第二区不参与内存分配,也就意味着将被当作辅助内存(即不可用)。为了减少不可用的内存大小,新生代默认假设“每次GC都有超过89%的对象会被会回收”,由此我们可以得出结果,“伊甸区Eden”和“Servivor1”存活的对象只有不到10%,那么第一区和第二区的比例就是9:1;如果存活对象万一超过10%会怎么办呢?即溢出如何处理,在Hotspot的设计中,将溢出的部分对象copy到老年区。

  • 为什么将第一个区分出一个和servivor2等大小的servivor1区呢?因为复制Copying算法中把第一区的存活对象复制到第二个区后就马上将第一区全部清理干净,然后让第一二区交换身份。新生代的复制算法一样需要这个交换身份的步骤,只不过这个步骤不需要eden区参与,因为存活的对象只有Servivor2的大小,所以只需要让servivor1和servivor2通过改变指针简单交换身份就可以了。

  • 新生代Copying复制算法为了处理溢出,将溢出的对象存放到老年代。而老年代如果采用Copying复制算法溢出将无法处理,因为老年代之后没有其他了,加之老年代的对象都比较持久,不经常回收,采用复制算法效率会极其低效。所以老年代采用另一种类似的算法 “标记整理(Mark-Compact)”,他是基于"Mark-Sweep标记清除法"的优化版本,即在完成标记清除后为了避免存在内存碎片,将剩余的对象向一段平移。(之后采用“指针碰撞”就可以舒服的进行内存分配了)

  • 根据对象存活周期将堆内存划分为“新生代”“老年代”并采用不同的GC算法的做法叫做 “分代收集算法Generational Collection”

  • 垃圾回收的第一步是获取所有GC Roots对象,然后再进行多轮标记(标记可达,没有被标记的就默认不可达了),然后对没有标记(不可达)的进行对象清除。

  • GC算法相对很多细节和设计上的差异,但是主流程基本一致。所以在算法学习上采用问题驱动,会显得更有针对性。

  • 对Classic VM的初略理解:采用单一执行子系统,要么使用内置的解析器(高频指令多会导致解析执行耗时),要么只使用外挂的JIT编译器(低频指令多会导致编译耗时)。而且采用了引用句柄池技术(句柄池有利于拓展,但是寻址性能低),不利于GC的可达性标记。(早期的技术)

  • 对Exact VM的初略理解:采用混合执行子系统,低频指令采用解析器,高频指令则采用JIT编译器,性能优化灵活。采用了快速的直接寻址法,新增OopMap缓存技术优化GC可达性分析效率。(现代VM技术)

  • GC的第一步就是初始标记,找到需要GC的区域上所有关联的GC Roots对象,这些对象大部分位于方法区以及线程的栈空间上(出于表达严谨,这里强调是大部分,因为部分特殊情况,问了不影响理解先不考虑)。而这个过最核心的问题是如果在大块内存里较短时间内快速地识别出哪些内存是该区域上GC Roots呢?想要快速,有个直接了当的方式便是牺牲空间,采用缓存技术,在内存分配的时记录需要的信息,类似的技术有:OopMap技术。

  • 大部分的GC Roots都位于线程的栈帧中,如何在JVM并发环境下高效的从线程堆栈中标记GC Roots也是GC算法中最难的一部分。

  • 栈帧的数据栈是一个slot数组,存放着方法调用过程中需要的本地变量,这些变量就是一堆二进制数字,没有类型表示,有可能是常量,也有可能是堆对象引用指针。

  • 在GC的时候,我们需要枚举所有堆对象引用指针。如果栈帧的数据栈没有标明类型,我们需要设计一个方案去识别并收集这些对象引用指针。收集的方案有3种:

    1. 通过对象边界检测,对齐检测(引用指针能被4字节整除)这些简单方法去推测(由于内存数据含义对无法GC程序来说是不可知的,也无法反编译,所以这种检测是模糊的,只要可能是引用,就会被认为是引用),这种古老的方式被称为“保守式GC”
    2. 当对象在内存被创建时由运行时在堆内存的数据中特定位置标明数据的类型,当GC的时候就可以很快速的进行判断,如果是引用对象数据就标记为GC Roots,这种不需要JIT编译器支持。
    3. 在编译时生成一个标记栈帧数据栈类型的映射表,通过扫描映射表枚举GC Roots, 这种称为“准确式GC”
  • 枚举GC Roots对象除了需要注意效率问题,还有另一个问题,即并发问题。我们知道引用在作用域内有效,一旦过了作用域便可能失效(比如slot复用技术会将无效本地变量移除),也就是说引用关系是动态变化的。我们在多线程的时候,一旦GC开始运行,我们希望所有线程的引用关系都不能被修改,这个时候JIT编译器会在代码中指定一些片段为“安全区”,只要程序计数器位于安全区内,那么这时候的引用关系就不会有任何改变。同时规定所有线程都必须执行到最近的安全点(带有OopMap的指令行)挂起,等待GC枚举GC Roots对象。

  • 理论上任何指令行都可以作为安全点,如果到处都是安全点,那么安全点用于存储操作栈类型的OopMap就会占有很大空间。大部分指令都是秒执行的,但有些指令会花很长时间执行(比如:循环挑战,方法调用,异常跳转等),这类指令程序会停留比较久,所以引用关系比较稳定,所以这类指令会被作为安全点。

  • 我们可以把安全点和安全区区域看做是线程为了配合GC程序枚举GCRoots 刻意选择在引用关系稳定的地方挂起。

  • 可以把oopMap简单理解成是调试信息。在源代码里面每个变量都是有类型的,但是编译之后的代码就只有变量在栈上的位置了。oopMap就是一个附加的信息,告诉你栈上哪个位置本来是个什么东西。 这个信息是在JIT编译时跟机器码一起产生的。因为只有编译器知道源代码跟产生的代码的对应关系。 每个方法可能会有好几个oopMap,就是根据safepoint把一个方法的代码分成几段,每一段代码一个oopMap,作用域自然也仅限于这一段代码。 循环中引用多个对象,肯定会有多个变量,编译后占据栈上的多个位置。那这段代码的oopMap就会包含多条记录。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vP5Fyw0j-1579505082472)(https://i.loli.net/2019/04/26/5cc2cb9302cec.png)]


新生代的Serial收集
缺点:

回收时Stop the world,导致工作线程无法进行。

优点:

没有复杂线程交互开销,简单高效。

适合场景:

新生代空间小,内存申请不频繁(数据稳定性高),GC频率低的客户端程序。

可搭配使用:

可以配合 老年代的CMS收集器 以及 老年代的Serial Old收集器(MSC)

参数

-XX:+UseSerialGC


新生代的ParNew收集
缺点:

会stop the world,在单CPU等情况下表现并没有Serial收集器好。

优点:

可以和CMS一起工作,适合现代多CPU的系统环境。

适合场景:

新生代空间小,内存申请不频繁(数据稳定性高),GC频率低的多CPU服务端程序。

可搭配使用:

可以配合 老年代的CMS收集器 以及 老年代的Serial Old收集器(MSC)

相关参数:

-XX:UseParNewGC 强行指定新生代的GC收集器类型。
-XX:ParallelGCThreads 多CPU环境下限制新生代GC最大线程数。


新生代的Parallel Scavenge收集

parallel Scavenge收集器和ParNew收集器基本一致,他最大的特点是 通过动态的控制GC条件实现控制VM吞吐量(Throughput),即控制CPU用于工作线程的使用率。

缺点:

会stop the world,在单CPU等情况下表现并没有Serial收集器好。不适合用户交互高的应用。不可以和CMS一起工作。

优点:

适合现代多CPU的系统环境。能够自动管理新生代内存比例,达到最大化吞吐量(新能调优的目的就是在包装垃圾正常回收的情况下,最大化吞吐量)。免除用户调优麻烦。

适合场景:

多CPU环境。对吞吐量又敏感要求,手工优化新生代内存比例的困难时适用。

可搭配使用:

可以配合 老年代的Serial Old收集器 以及 老年代的Parallel Old收集器(MSC)

相关参数:

-XX:+UseParallelGC 打开该收集器后,将使用Parallel Scavenge(年轻代)+Serial Old(老年代)的组合进行GC。

-XX:MaxGCPauseMillis 指定最大GC停顿时间,如果不设置,GC为了实现高吞吐量会导致停顿时间很长,所以必须配合吞吐量参数-XX:GCTimeRatio来实现。单位是毫秒。如果这个值设置太小,停顿时间缩短,导致新生代空间减少(新生代是动态自适应的,缩小牺牲,新生代的GC时间会减少。),导致GC频率增加和吞吐量减少(小空间吞吐量也小),无默认值。
-XX:GCTimeRatio 这个参数的设计原则是“吞吐量优先”,体现在他值的含义,和限制的含义。这个值表示工作线程的时间与GC时间的比例,比如99表示线程与GC的时间比是99:1,65就是65:1。GC Time ratio的翻译应该是吞吐量与GC时间的比值。这个限制的含义,他限制的吞吐量必须比少于这个比例,即吞吐量要比这个值越大越好。
-XX:+UseAdaptiveSizePolicy 这个参数与新生代密切相关,我们知道新生代是一个相对复杂的而且与程序堆内存分配直接相关的一块区域。如果采用了具有自适应能力的吞吐量收集器Parallel Scavenge收集器,那么可以开启这个参数,就收集器就会自适应(以吞吐量优先)的调节新生代的比例了,无需再设置-Xmn、-XX:ServivorRatio、-XX:PretenureSizeThresHold等参数。只需要这样写-XX:+UseAdaptiveSizePolicy就可以开启了,其实默认就是开启了。如果要关闭它就这样写-XX:-UseAdaptiveSizePolicy。

MaxGCPauseMillis和GCTimeRatio同时设置时,GC线程会优先考虑MaxGCPauseMillis

UseAdaptiveSizePolicy默认是开启的

GCTimeRatio默认值为99,即吞吐量优先原则


  • 3种新生代收集器都采用了复制GC算法

老年代的Serial Old收集器

它是采用了标记-整理算法,单线程。适合client端等小内存单CPU的环境下使用。在Server端它的主要用途是作为CMS的后备方案使用。

缺点:

单线程,stop the world。

优点:

采用Mark-Compact无内存碎片,可以和Parallel Scavenge新生代傻瓜收集器配合使用。

适合场景:

适合内存小VM,比如client

可搭配使用:

三种新生代收集器,Parallel Scavenge收集器,Serial收集器,ParNew收集器。以及老年代收集器CMS(作为辅助备胎使用)

相关参数:

-XX:+UseSerialGC 默认新生代是Serial GC,老年区是Serial Old GC

老年代的Parallel Old收集器

它是Parallel scavenge的老年代算法,1.6开始提供。采用Mark-Compact算法

缺点:

必须和Parallel Scavenge配合使用。

优点:

多线程。

适合场景:

适合傻瓜版的多线程吞吐量优先的 server端使用。

可搭配使用:

Parallel Scavenge收集器

相关参数:

-XX:+UseParallelOldGC 只能在-XX:UseParallelGC开启Parallel scavenge新生代收集器的时候使用。

老年代的Concurrent Mark Sweep 收集器

CMS的算法核心Mark and Sweep,Mark是一个复杂的过程也是耗时最长且可以拆解的过程。而Sweep过程没有涉及到位置移动,所以允许并发进行。有了这个理论基础,CMS算法由此产生,从此靠别了一GC就从头到尾StopTheWorld的做法。

CMS分为4个核心步骤,前3个步骤是标记,最后一个核心步骤是并发清除:
初步标记(initial mark):采用简单快速的Stop the World 单线程标记所有GC Roots对象。
并发标记(concurrent mark):以第一步骤的GC Roots为基础,尽最大可能的标记活跃对象,这个步骤耗时最长,但是他是并发的。
重新标记(remark):这个步骤类似ParallelOldGC,但是,前面两个步骤已经标记了大部分对象,剩下就是一些在并发情况下无法标记的对象,或者过程中发生引用改变的对象。通过stop the world 多线程标记补漏。
并发清除(concurrent sweep):并发清除,这个过程也是耗时相当长,但是也是并发。

  • 由于停顿的时间都是短暂的,大部分耗时的步骤都是并发,所以CMS收集器总体表现是并发的,往后的性能瓶颈和调优也是在这地方下手。

  • 由于CMS的并发线程数的公式:p为CPU个数

(p  + 3) / 4

从这个公式得知,当p=4时, 吞吐量为25%,当p=3时,吞吐量为30%,当p=2时,吞吐量50%左右,基本上就是串行了(这个很要命)。
也就是说GC需要不少于25%CPU资源。总的来说,在CPU少于3个时CMS基本上不可用。

  • CMS时牺牲实施吞吐量获得实时停顿短暂的老年代算法。(Parallel Scavenge是新生代的算法,也可以通过设置MaxGCPauseMillis实现牺牲吞吐量换取停顿时间,不过这两个算法不可共用)

  • 在并发清除阶段,工作线程还会继续产生垃圾(Floating Garbage),如果在触发CMS预留给并发工作线程的空间不多,即FloatingGarbage太多,会引起Concurrent Mode Failure.之后系统会启动低效的SerialOldGC,这个将对系统性能造成很大影响。这个硬伤可以弥补,通过XX:CMSInitiatingOccupancyFraction调节合适的CMS启动时机(太早启动会GC频繁,太晚启动引发浮动垃圾太多)。

  • CMS还有一个很重要的事情就是内存碎片问题,CMS使用一段时间后就会面临严重的碎片问题,从而触发CMS的Full GC模式,这个模式下的CMS默认会进行一次Compact。虽然默认每次Full GC会CMS都会compact,但其实没必要,只需要适当的compact几次就可以了,作为完美主义,可以通过XX:CMSFullGCsBeforeCompact设置多次不带Compact的CMS FullGC触发一次带Compact FullGC。

缺点:

在CPU少于3得不到好的性能,存在内存碎片,如果Old区增长太快(浮动垃圾多)容易触发CMS,要求能灵活使用CMSFullGCsBeforeCompact
与CMSInitiatingOccupancyFraction参数调优

优点:

低停顿,高并发。

适合场景:

Server端,多CPU,大内存。

可搭配使用:

虽然可以Serial搭配,但是坚决别这么做,因为无论CPU个数如何,都无法到达好的性能。
唯一建议组合:ParNew+CMS+SerialOld

相关参数:

-XX:UseConcMarkSweepGC 使用ParNew+CMS+SerialOld组合收集器
-XX:ParallelGCThreads 限制新生代并行线程数。
-XX:CMSInitiatingOccupancyFraction CMS的触发时机,即设置已用内存达到百分之多少时触发CMS(非FullGC)
-XX:CMSFullGCsBeforeCompact 调节FullGC的Compact的触发频率,即多少次不带Cimpact的FullGC后才触发一次带Compact的FullGC
-XX:UseCMSCompactAtFullCollection 设置是否在FullGC时开启Compact,默认是开启的。(强烈建议使用默认开启。)

G1收集器
描述

它的目的是未来替换CMS,即CMS能用的多CPU多内存场景他也能用。
他的特点是“并发”“独立使用”“可控停顿时间(M毫秒内限制最多停顿N毫秒,适合实时操作系统RTS)”

  • G1分为4个步骤:

    1. 初始标记:单线程STW标记各个Region的GC Roots以及Remembered Set中对象。
    2. 并发标记:并发标记所有的活跃对象
    3. 最终标记(Final Marking):并行STW标记所有并发过程中有改动的引用,并将改动log到RememberedSet Logs中,然后同步到RememberSet.
    4. 筛选回收(Live Data Counting and Evacuation):
      按照回收垃圾耗时和可回收空间加权评分排序,将满足“停顿要求”的region按排序先后进行清除。
  • G1 采用并发的标记优点类似CMS,但它是针对整块堆空间的一个活动。

  • G1 强调的是可控制的停顿时间,所以对吞吐量有所牺牲。

缺点:

因为对低停顿的追求,导致GC频率增加,其对吞吐量的控制相对较差。

优点:

可控制的低停顿,有很好的实时交互性。

适合场景:
老年代数据比较多,内存比较大,停顿相对比较久的多CPU后端系统。

  • G1无组合收集器, 相关调优参数另外解答。

常见的GC方案:

parNew+CMS+SerialOld : 适合大部分数据都会延迟老年代的业务系统,特别是多CPU多内存的后端服务器系统,这些系统的老年区内存大,容易堆积很多垃圾,容易造成长时间停顿,采用CMS可以很好的控制停顿时间,理论上低停顿会造成吞吐量下降,但是老年代的内存一般都比较大,所以这个影响并不明显。使用的参数

XX:UseConcMarkSweepGC,
XX:CMSInitiatingOccupancyFraction,XX:CMSFullGCsBeforeCompact,
XX:ParallelGCThreads

Parallel Scavenge:
适合大部分对象都是临时的(比如适合函数式编程比较多的场景),大部分对象分配到新生代之后就会被回收,新生代为性能瓶颈,并且新生代相对不大的场景。比如一些应用性强的,基于回话的应用型后端业务系统。这类系统的吞吐量很关键,而新生代空间不大,响应速度应该不成问题。所以Parallel Scavenge可控的吞吐量特性正好合适这类系统。使用参数:
XX:+UseParallelGC
XX:+UseParallelOldGC
XX:MaxGCPauceMillis
XX:GCTimeRatio
XX:UseAdaptiveSizePolicy(默认开启,所以忽略)

G1
应用场景和CMS基本一致,只是比CMS多了一个可控的停顿时间的机制。

其他与垃圾回收相关的虚拟机参数:
ServivorRatio: eden区与servivor区的比例

PretenureSizeThresHold: 设置新生代可容纳的最大对象大小阈值(大于这个值,会直接进入老年区),单位是B

MaxTenuringThresHold: 设置最大新生代对象年龄,每一次minor GC所有对象加一岁,当达到这个设定年龄后晋升到老年代

HandlePromotionFailure: 设置是否允许晋升失败。1.6之后默认就是允许失败,即true。由于每次minorGC前都会把之前每次晋升的平均值和当前Old区的可用空间进行比较,如果大于它且不允许失败,那么MinorGC前会进行一次fullGC,确保不会晋升失败。如果大于它且允许失败,那么会冒险进行一次minorGC,万一失败了,依然可以通过FullGC一次。这样的做目的就是为了减少FullGC的次数,FullGC每次都会compact很伤性能,所以这个参数有必要默认为true.

注意:OutOfMemoryException,是指系统99%的时间用于GC,而回收的内存却小于2%所发出的告警。

  • 无论是内存分配,还是内存回收,还是分代规划,还是GC算法,甚至GC日志,都是理论,需要有具体的垃圾收集器来具体讨论,尽管很多垃圾收集器理论上很多共同点,但是不应该混为一谈。区分分析才是理解他们的正确方式。

  • 当minorGC触发时,采用SerialNew/ParNew的复制算法,从最小年龄开始检测,当该年龄的对象大小和超过servivor空间的一半的时候,会自动启动“Old区担保”机制,将大于等于这个年龄的对象全部直接复制到Old区(不管是否达到MaxTenuringThreshold以及PretenureSizeThreshold,都会被复制到Old区.)

G1 详解

监控调优分析处理的工具篇

jps 可以查看远程(通过RMI协议)或者本地系统的所有JVM进程
jstat 可以查看JVM运行数据(类加载状况,gc分代状况,JIT编译状态)
jinfo 可以显示JVM各方面properties等配置信息(读取和修改虚拟机启动参数,以及System.getProperties()的参数值)

jmap 获取heapdump快照信息(还可以通过kill -3 获取),比如堆,永久代、finalize的F-Queue的对象统计信息(注意,这个主要是分析内存对象,而jstat分析的是GC状况,粒度不同)。
jhat 分析heapdump文件
jstack 查看线程快照信息(线程堆栈,线程ID,线程状态,锁状态),可以通过Thread.getAllStackTrace获取

JConsole JAVA监听管理控制台

这个东西相当于拥有了jps jstat jstack的功能
可以用来分析线程状态和GC分代情况,以及类加载情况。

一个涉及Integer.value对[-128,127]对象缓存引发的死锁程序,很经典,很有意思:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EcsRGWVR-1579505082474)(https://i.loli.net/2019/04/26/5cc2cb9214ab5.png)]

VisualVM AllInOne虚拟机监控管理工具

它是一个需要外接插件才可以使用的工具,它对性能的影响很小,可以在生产上使用。所有强大的功能都是通过插件支持,比如加上JConsole后就等同于拥有了JConsole的功能了,而且还有用于性能分析的Profiler功能(例如JProfiler).
BTrace插件可以在不停机的情况下加入log代码,它利用的是Hotswap技术实现动态代理功能(可以利用字节码工程或者Proxy接口实现,这里目测是用字节码工程实现)。

调优经验摘录:

  1. 32位的服务器的最大内存4G,而64位系统的内存通常很大,如果内存太大FullGC将会面临dump文件太大不好分析,以及GC停顿大的问题。
    2.利用JDNI共享连接池
    3.NIO等很消耗直接内存,直接内存通过频繁调用system.gc()来达到FullGC的效果,但是该效果会被-XX:+DisableExplicitGC影响。而且system.gc会造成STW,且GC线程优先级低。想要回收直接内存,只能通过sun.misc.cleaner.
    4.直接内存的默认回收方式就是,等直接内存的引用对象被回收时,通过PhantomReference的通知调用unsafe.freememory方法回收的。但是这个不靠谱,因为经常出现堆外快要爆炸了,堆内的引用对象依然好好的。
    5.最好的回收直接内存的方法就是主动手工调用sun.misc.cleaner(它继承了幽灵引用)的freememory方法。
    原理如下:
class DirectByteBuffer extends MappedByteBuffer  implements DirectBuffer
{
    ....
    //构造方法
    DirectByteBuffer(int cap) {                   // package-private
    
           ...省略无关代码
    
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));//注册钩子函数,释放直接内存
        ....省略无关代码
    
    }
      ....
}

private static class Deallocator
    implements Runnable
{
 
    private static Unsafe unsafe = Unsafe.getUnsafe();
 
    private long address;
    private long size;
    private int capacity;
 
    private Deallocator(long address, long size, int capacity) {
        assert (address != 0);
        this.address = address;
        this.size = size;
        this.capacity = capacity;
    }
 
    public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
        unsafe.freeMemory(address);//清除直接内存
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }
}



public class Cleaner extends PhantomReference<Object> {
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
    private static Cleaner first = null;
    private Cleaner next = null;
    private Cleaner prev = null;
    private final Runnable thunk;

    private static synchronized Cleaner add(Cleaner var0) {
        if (first != null) {
            var0.next = first;
            first.prev = var0;
        }

        first = var0;
        return var0;
    }

    private static synchronized boolean remove(Cleaner var0) {
        if (var0.next == var0) {
            return false;
        } else {
            if (first == var0) {
                if (var0.next != null) {
                    first = var0.next;
                } else {
                    first = var0.prev;
                }
            }

            if (var0.next != null) {
                var0.next.prev = var0.prev;
            }

            if (var0.prev != null) {
                var0.prev.next = var0.next;
            }

            var0.next = var0;
            var0.prev = var0;
            return true;
        }
    }

    private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);
        this.thunk = var2;
    }

    public static Cleaner create(Object var0, Runnable var1) {
        return var1 == null ? null : add(new Cleaner(var0, var1));
    }

    public void clean() {
        if (remove(this)) {
            try {
                this.thunk.run();
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }

                        System.exit(1);
                        return null;
                    }
                });
            }

        }
    }
}

6.Runtime.getRuntime().exec()这个方法时要命的。因为它会调用系统函数fork(),拷贝当前JVM进程来执行一个shell命令.不用多久系统资源就会被消耗殆尽。
7.java 程序采用异步socket可能会造成socket堆积,直到socket资源耗尽。异步通知等业务还是采用消息队列比较合理。
8.JAVA程序中比较的数据应该重新设计精简的数据结构来存放,而不是采用JAVA原声的对象包装,避免造成资源浪费。比如要把很多对象格式化传输,就不要简单的把对象序列化,而是要重新设置新的精简的专用的序列化格式。
9.可以使用-XX+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -XLoggc来查看GC停顿的规律
10.-Xverify:none通过关闭对字节码的安全检验可以提高很大的类加载和程序启动速度。
11.Hotspot的名字来源于JIT即时编译器,因为JIT会对热点代码编译成本地汇编码。
12.找到垃圾回收的触发原因,是对垃圾回收调优的关键。常用参数jstat -gccause 找到最近一次gc的触发原因。


G1官方材料学习

为解决CMS算法产生空间碎片和其它一系列的问题缺陷,HotSpot提供了另外一种垃圾回收策略,G1(Garbage First)算法,通过参数-XX:+UseG1GC来启用,该算法在JDK 7u4版本被正式推出,官网对此描述如下:

The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with a high probability, while achieving high throughput. The G1 garbage collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collector is designed for applications that:
Can operate concurrently with applications threads like the CMS collector.Compact free space without lengthy GC induced pause times.Need more predictable GC pause durations.Do not want to sacrifice a lot of throughput performance.Do not require a much larger Java heap.

G1垃圾收集算法主要应用在多CPU大内存的服务中,在满足高吞吐量的同时,竟可能的满足垃圾回收时的暂停时间,该设计主要针对如下应用场景:
垃圾收集线程和应用线程并发执行,和CMS一样空闲内存压缩时避免冗长的暂停时间应用需要更多可预测的GC暂停时间不希望牺牲太多的吞吐性能不需要很大的Java堆 (翻译的有点虚,多大才算大?)堆内存结构
1、以往的垃圾回收算法,如CMS,使用的堆内存结构如下:
GC调优-CMS分代.png

新生代:eden space + 2个survivor老年代:old space持久代:1.8之前的perm space元空间:1.8之后的metaspace这些space必须是地址连续的空间。
2、在G1算法中,采用了另外一种完全不同的方式组织堆内存,堆内存被划分为多个大小相等的内存块(Region),每个Region是逻辑连续的一段内存,结构如下:
GC调优-G1分代.png
每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。
Region堆内存中一个Region的大小可以通过-XX:G1HeapRegionSize参数指定,大小区间只能是1M、2M、4M、8M、16M和32M,总之是2的幂次方,如果G1HeapRegionSize为默认值,则在堆初始化时计算Region的实践大小,具体实现如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NVrALFtw-1579505082476)(https://i.loli.net/2019/04/26/5cc2cb921756c.png)]
默认把堆内存按照2048份均分,最后得到一个合理的大小。

GC模式

G1中提供了三种模式垃圾回收模式,young gc、mixed gc 和 full gc,在不同的条件下被触发。
young gc发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc,这种触发机制和之前的young gc差不多,执行完一次young gc,活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。

参数含义
-XX:MaxGCPauseMillis 设置G1收集过程目标时间,默认值200ms
-XX:G1NewSizePercent 新生代最小值,默认值5%
-XX:G1MaxNewSizePercent 新生代最大值,默认值60%

mixed gc当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。

那么mixed gc什么时候被触发?
先回顾一下cms的触发机制,如果添加了以下参数:

-XX:CMSInitiatingOccupancyFraction=80
-XX:+UseCMSInitiatingOccupancyOnly

当老年代的使用率达到80%时,就会触发一次cms gc。相对的,mixed gc中也有一个阈值参数 -XX:InitiatingHeapOccupancyPercent,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次mixed gc.


mixed gc的执行过程有点类似cms,主要分为以下几个步骤:

  • initial mark: 初始标记过程,整个过程STW,标记了从GC Root可达的对象concurrent marking: 并发标记过程,整个过程gc collector线程与应用线程可以并行执行,标记出GC Root可达对象衍生出去的存活对象,并收集各个Region的存活对象信息
  • remark: 最终标记过程,整个过程STW,标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象
  • clean up: 垃圾清除过程,如果发现一个Region中没有存活对象,则把该Region加入到空闲列表中full gc如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的 serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc.
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值