JVM及GC

满足下面3个条件方法区里的类会被回收:
1、该类的所有实例对象都已从java堆里被回收
2、加载这个类的ClassLoader已经被回收
3、对该类的class对象没有任何引用

tomcat需要破坏双亲委派模型的原因:
1、tomcat中的需要支持不同web应用依赖同一个第三方类库的不同版本,jar类库需要保证相互隔离;
2、同一个第三方类库的相同版本在不同web应用可以共享
3、tomcat自身依赖的类库需要与应用依赖的类库隔离
4、jsp需要支持修改后不用重启tomcat即可生效,为了上面类加载隔离和类更新不用重启,定制开发各种的类加载器

类的加载时机
1、创建类的实例。
2、使用类的静态变量或静态方法。
3、使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。
4、初始化某个类的子类。
5、直接使用java.exe命令来运行某个主类。

类的初始化时机
1、当虚拟机启动,先初始化main方法所在的类
2、使用new关键字创建一个类的对象
3、调用该类的静态变量(final的常量除外)和静态方法
4、使用java.lang.reflect包的方法对类进行反射调用
5、当初始化一个类时,如果其父类没有被初始化,则先会初始化他的父类

不会发生类的初始化的情况:
1、引用静态常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了),如: public static final int NUM = 10; 代码中使用Son.NUM
2、当访问一个静态成员时,只有真正声明这个静态成员的类才会被初始化,当通过子类引用父类的静态变量,不会导致子类初始化
3、某类型数组的动态初始化,不会触发此类的初始化,如:
Person[] people = new Person[10];

Java程序初始化顺序
1、父类的静态变量
2、父类的静态代码块
3、子类的静态变量
4、子类的静态代码块
5、父类的非静态变量
6、父类的非静态代码块
7、父类的构造方法
8、子类的非静态变量
9、子类的非静态代码块
10、子类的构造方法

加载、验证、准备、解析、初始化

哪些对象不会被回收?
被方法的局部变量、类的静态变量引用的对象。

强引用、软引用、弱引用、虚引用
强引用:被强引用对象就不会被回收
软引用:进行回收之后发现内存还是不够存放新对象,则会对软引用对象进行回收
弱引用:一定会被回收

虚引用:在任何时候都可能被垃圾回收器回收,主要用来跟踪对象被垃圾回收器回收的活动。在对象被收集器回收时收到一个系统通知。 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,在其关联的虚引用出队前,不会彻底销毁该对象。 所以可以通过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收了。

使用虚引用的目的就是为了得知对象被GC的时机,可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。这个虚引用对于对象而言完全是无感知的,有没有完全一样,但是对于虚引用的使用者而言,就像是待观察的对象的把脉线,可以通过它来观察对象是否已经被回收,从而进行相应的处理。

事实上,虚引用有一个很重要的用途就是用来做堆外内存的释放,DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。

finalize()的作用
没有GC Roots引用的对象一定会被回收吗?
不是的,对象有个finalize()方法可以拯救自己,如果对象重写了Object类中的这个方法,在对象要被回收的时候会尝试调用,看是否把自己的实例对象给了某个GC Roots变量,比如下方代码

重写让对象引用了自己,那么就不会被垃圾回收。

-XX:TraceClassLoading -XX:TraceClassUnloading
这两个参数用于追踪类加载和类卸载的情况

动态对象年龄判断
要转移存活对象进入s0或s1之前,会判断s0或s1中存活的对象所占比例是否大于默认的50%,这个规则实际运行的逻辑:年龄1+年龄2+年龄n的多个年龄对象总和超过survivor区域的50%,会把n及以上的对象都放入老年代,不用等15次GC。

Minor GC后对象太多无法放入Survivor,此时会把这些对象直接放入老年代。

在执行任何一次Minor GC之前,JVM都会先检查一下老年代可用的内存空间,是否大于新生代所有对象的总大小。因为最极端的情况下,可能Minor GC过后,所有对象都存活下来。

如果老年代可用内存小于新生代所有对象大小,会看一下
"-XX:-HandlePromotionFailure"参数是否设置,如果有,则会判断老年代剩余内存大小是否大于之前每一次Minor GC后进入老年代的对象平均大小。

如果判断失败,或者参数没有设置,则会直接触发一次"Full GC",对老年代进行垃圾回收,腾出一些空间,然后再执行Minor GC。

如果Full GC过后,老年代还是没有足够的空间存放Minor GC过后剩余的对象,则会导致OOM内存溢出。

Minor GC过程中会STW,CMS和G1回收过程中,初始标记和重新标记会STW。

ParNew垃圾回收器在执行Minor GC时,就会把系统的工作线程全部停掉,禁止程序继续运行创建新的对象,然后自己用多个垃圾回收线程去进行垃圾回收,默认使用的线程数量和CPU的核数一致,采用复制算法。

CMS垃圾回收
1、初始标记。进入STW,标记GC Roots直接引用的对象,这个过程速度很快
2、并发标记。系统线程继续运行,垃圾回收线程对老年代所有对象进行GC Roots追踪,同时对对象做出的修改进行记录,比如哪些对象被新建,哪些对象失去了引用,这个过程最耗时
3、重新标记。并发标记阶段过程中,系统同时运行,会产生很多存活对象和垃圾对象。进入STW,重新标记第二阶段新创建的对象和已有对象可能失去引用变成垃圾的对象。这个过程速度很快
4、并发清理。系统线程继续运行,垃圾回收线程清理之前标记为垃圾的对象。这个过程也很耗时

CMS相关问题
1、消耗CPU资源,默认启动线程为(cup核数+3)/4。在回收过程中耗时长,占用线程影响应用程序运行
2、Concurrent Mode Failure问题。
在并发清理过程中,可能新生代会触发Minor GC部分对象进入老年代,但是短时间内又没有引用这些对象,这种对象就是老年代的"浮动垃圾"。CMS只回收之前标记的垃圾对象,并不会回收他们,需要等到下次GC。

为了保证CMS垃圾回收期间,还有一定的内存让一些对象可以进入老年代,一般会预留一部分空间。CMS垃圾回收触发时机,其中一个就是当老年代的内存比例达到设定值就会自动执行GC。

参数:"-XX:CMSInitiatingOccupancyFaction",设置老年代占用多少比例就进行GC。默认为92%。
参数:"-XX:UseCMSInitiatingOccupancyOnly",和上面参数配合使用,指定每次CMS进行回收的阈值都是设定值,不然则第一次GC为设定值,后续会自动调整

如果CMS回收期间,系统程序要放入老年代的对象大于可用内存空间,此时就会发生Concurrent Mode Failure,就是并发垃圾回收失败,一边回收,一边放新对象到老年代,内存不够用了。此时会自动用"Serial Old"垃圾回收器代替CMS,直接强行把系统程序STW,重新进行长时间的GC Roots追踪标记出全部垃圾对象,然后一次性回收,完事之后再恢复系统线程运行。

3、内存碎片问题。CMS采用标记清理算法,标记出来的对象清理过后,会导致大量内存碎片产生,如果内存碎片太多会导致后续对象进入老年代找不到可用的连续内存空间触发Full GC。

CMS有一个参数"-XX:+UseCMSCompactAtFullCollection",默认打开,在Full GC之后要再次进行STW,进行碎片整理。
还有一个参数"-XX:CMSFullGCsBeforeCompaction",意思是执行多少次Full GC之后再执行一次内存碎片整理,默认是0,每次回收之后都会进行一次内存整理。

-XX:+CMSScavengeBeforeRemark:在CMS GC 重新标记前启动一次 minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段

-XX:+CMSParallellnitialMarkEnabled:在初始标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW
-XX:+DisableExplicitGC:禁止显式执行GC

为什么Full GC比Minor GC慢很多
1、新生代存活对象少,直接从GC Roots追踪对象,然后把对象放到survivor中,就一次性直接回收eden和之前使用的survivor。
2、老年代存活对象多,并发清理阶段不是一次性回收一大片内存,而是找到零散的各个地方的垃圾对象进行回收,最后要执行内存碎片整理。万一并发清理期间剩余的内存空间不足以存放要进入老年代的对象,还会引发Concurrent Mode Failure问题,改用Serial Old垃圾回收器,STW重新回收对象,更加耗时。

Full GC触发的时机
1、新生代对象大于老年代可用空间,且未开启空间担保参数,此时会触发,所以一般空间担保参数都会打开。"-XX:-HandlePromotionFailure",JDK1.6之后此参数废弃,默认已打开此参数逻辑执行GC。
2、老年代可用空间小于历次Minor GC后进入老年代对象的平均大小,会触发
3、Minor GC后进入老年代的对象大于老年代可用空间,会触发
4、参数"-XX:+UseCMSCompactAtFullCollection",已使用空间大于参数值,自动触发

G1垃圾回收器
1、可以同时回收新生代和老年代对象
2、把java堆内存拆分为2048个大小相等的Region,单个Region随时会属于新生代也会属于老年代,没有给新生代多少内存,给老年代多少内存一说,新生代和老年代的内存区域是不停变动的,由G1自动控制
3、可设置一个垃圾回收的预期停顿时间。如何做到,需要追踪每个Region里的回收价值,即每个Region里回收对象的大小和耗时,尽量把垃圾回收对系统的影响控制在指定的时间范围内,同时在有限的时间内尽量回收更多的垃圾对象。

刚开始的时候,默认新生代对堆内存的占比为5%,可通过参数进行控制。在系统运行中,JVM会不停给新生代增加更多的Regin,最高占比不会超过60%,也可通过参数配置。一旦Region进行了垃圾回收,新生代的Region会减少,是动态变化的。

G1的新生代中也有Eden和Survivor的划分,随着系统运行,不停在新生代的Eden对应的Region区域放入对象,同时会给新生代加入更多的Region,直到达到最大占比60%。G1回收是动态的,会根据设定的GC停顿时间给新生代分配一些Region,然后到达一定程度就触发GC,并且把时间控制在预设范围内。G1会采用复制算法来进行垃圾回收,进入STW状态,然后把Eden+S0中存活对象放入S1中对应Region,接着回收掉Eden和S0的对象。

这个过程和ParNew回收器是有区别的,因为G1设定了GC停顿时间,默认200ms,他会选择部分Region的对象进行回收。

对象什么时候进入老年代?和之前的一致

大对象Region,之前大对象可以直接进入老年代,G1有所改变,他提供了专门的Region来存放大对象,而不是让大对象直接进入老年代的Region。G1中对大对象的判断规则是一个对象的大小超过一个Region大小的50%,如果对象太大,则会横跨多个Region来存放。大对象Region不属于新生代和老年代,在进行新生代和老年代回收的时候,会顺带把大对象Region一起回收。

什么时候触发新生代+老年代的混合回收
参数"-XX:InitiatingHeapOccupancyPercent",默认值为45%。如果老年代占堆内存比例45%的Region时,会触发一个新生代+老年代的混合回收。

回收过程
1、初始标记,同样进入STW,标记GC Roots直接引用的对象
2、并发标记,允许系统程序运行,进行GC Roots追踪所有存活对象,并记录对象的一些修改动作,比如新建对象和失去引用对象
3、重新标记,进入STW,根据并发标记阶段记录的对象修改,最终标记一下哪些是存活对象,哪些是垃圾对象
4、混合回收,会计算新生代和老年代每个Region中存活对象数量,存活对象占比、执行垃圾回收的预期性能和效率。接着会STW尽快进行垃圾回收,根据垃圾回收设置的停顿时间选择部分Region优先进行回收,包括新生代、老年代、大对象

混合回收阶段,G1允许执行多次混合回收,参数"-XX:G1MixedGCCountTarget"
就是在一次混合回收过程中,最后一个阶段执行了几次混合回收,默认值:8次
意味着在最后这个阶段,先停止系统运行,混合回收一些Region,再回复系统运行,接着再STW,混合回收一些Region,反复8次。

为什么要反复回收多次呢?
基于垃圾回收停顿时间设置,让系统运行一会,尽可能让系统不要停顿时间太长,

参数"-XX:G1HeapWastePercent",默认值5%,意思是在混合回收时,对Region回收都是基于复制算法进行,把要回收的Region的存活对象放在其他Region,然后原Region中的垃圾对象全部清理掉,这样在回收过程中会不断出现新的Region,一旦空闲出来的Region数量达到堆内存的5%,就会停止回收。

参数"-XXG1MixedGCLiveThresholdPercent",默认值85%,确定要回收的Region,必须存活对象比例低于85%才可以进行回收。

在进行混合回收时,无论年轻代还是老年代都是基于复制算法进行回收,万一出现复制过程中没有空闲Region可以存放自己的存活对象,就会触发一次失败,一旦失败,就会进入STW,采用Serial Old回收器进行标记、清理、压缩整理操作,空闲出来一批Region,这个过程是很慢的。

为什么转移阶段不能和标记阶段一样并发执行呢?主要是G1未能解决转移过程中准确定位对象地址的问题。

参数"-XX:MaxGCPauseMills",最大GC停顿时间,默认200ms。如果这个参数设置小了,说明每次GC停顿时间会特别短,此时G1发现一旦几十个Region占满,就会触发新生代的GC,然后GC频率特别频繁,虽然每次GC时间很短。

如果这个参数设置大了,G1会允许不停在新生代分配新的对象,累积很多了,再一次性回收好几百个Region,此时可能一次GC停顿时间会达到几百毫秒,但是GC频率很低。

所以我们需要通过这个参数的设置,尽量让系统GC频率别太搞,同时每次GC停顿时间也别太长,达到一个理想的合理值。

mixed GC如何优化
老年代在堆内存占比超过45%会触发,所以要尽量减少对象进入老年代。比较关键的地方,就是新生代GC之后存活对象过多无法放入Survivor区域,已经动态年龄判定规则,这2个条件可能让很多对象快速进入老年代。核心点还是
"-XX:MaxGCPauseMills"参数

如果这个参数设置的很多,导致系统运行很久,新生代可能占了内存60%,此时才触发新生代GC,此时回收后存活下的对象很多,会导致Survivor区域放不下,直接放入老年代中,或者进入Survivor区域后触发动态年龄判定规则,达到Survivor区域的50%,也会快速导致一些对象进入老年代中。

所以要在保证新生代GC别太频繁的同时,要考虑每次GC过后存活对象有多少,避免存活对象快速进入老年代,频繁触发mixed GC

ZGC
与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。

ZGC只有三个STW阶段: 初始标记, 再标记, 初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

ZGC关键技术
ZGC通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理描述如下:并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即着色指针。

频繁Full GC的几种常见原因
1、系统承载高并发,或者处理数据量过大,导致Young GC很频繁,而且每次Young GC过后存活对象太多,内存分配不合理,survivor区域放不下,导致对象频繁进入老年代,触发Full GC
2、系统一次性加载过多数据进内存,搞出来很多大对象,导致频繁大对象进入老年代,频繁触发Full GC
3、系统发生内存泄漏,创建大量对象进老年代,无法进行回收,占用大量空间导致频繁触发Full GC
4、Metaspace因为加载类过多触发Full GC
5、误调用System.gc()触发Full GC

什么情况下会发生Metaspace内存溢出
1、未给系统设置Metaspace大小,直接用的默认值,导致可能才几十mb不够用
2、代码中使用cglib之类的技术动态生成类,没有控制好会导致生成很多类把Metaspace空间塞满

什么情况下会发生栈内存溢出
一个栈的内存大小是有限的,默认1m,一般用不了这么多,建议调成256k就可以了。但是如果一个线程调用方法过多,里面的局部变量占用空间超过设定大小时会发生内存溢出,一般出现在递归方法

什么情况下会发生堆内存溢出
1、系统承载高并发请求,大量对象存活放入内存中,有限的内存空间放不下导致堆内存溢出
2、系统有内存泄漏的问题,大量对象存活没有及时取消引用,导致无法回收占用内存空间

上一篇:《HALCON数字图像处理》第四章笔记


下一篇:HBASE进阶(2):写流程/MemStore Flush