前言:在慕课网上学习剑指Java面试-Offer直通车时所做的笔记
目录
第一章 对象被判定为垃圾的标准
当没有被其它对象引用时,它就是垃圾,其占据的内存就要被释放,同时此对象也要被销毁。
判断对象不被引用的算法有:引用计数算法,可达性分析算法。
1.1 引用计数法
用过判断对象的引用数量来决定对象是否可以被回收。
堆中的每个对象实例都有一个引用计数器,当一个对象被创建时,若该对象实例分配给一个引用变量,该对象实例的引用计数会被设置为1,若该对象又被另外一个对象所引用,则该对象的引用计数器继续加1,变为2.当该对象的实例的某个引用超过了生命周期或者被设置为一个新值时,该对象实例的引用计数便会减1。
任何引用计数为0的对象实例可以被当作垃圾收集。
优点:执行效率高,程序执行受影响较小。
缺点:无法检测出循环引用的情况,导致内存泄漏。
如子对象有对父对象的引用,父对象反过来引用子对象。这样引用计数永远不会为0.
主流的垃圾收集器并未用此种算法。
1.2 可达性分析算法
通过判断对象的引用链是否可达来决定对象是否可以被回收。
此算法来自离散数学中的图论,程序把所有的引用关系看作一张图,通过一系列名为GC Root的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径就被称为引用链即reference chain,当一个对象从GC Root没有任何引用链相连,从图论上来说这个对象到GC Root不可达,那在这时则证明了这个对象是不可用的,它也就被标记为垃圾。
垃圾回收器会对内存中的整个对象图进行遍历,它从GC Root开始,然后是跟对象引用的其它对象,比如实例对象,回收器将访问到的所有对象标记为存活,当标记结束后不可达的对象会被回收器清除。
可以作为GC Root的对象
虚拟机栈中引用的对象(栈帧中的本地变量表)。比如我们在java方法李new了一个Object,并赋值给了一个局部变量,那么在局部变量没有被销毁之前,new出的Object就会成为GC Root。
方法区中的常量引用的对象,比如类里某个常量存储的是某个对象的地址。那么被保存的对象也成了GC的跟对象。当别的对象引用到它就会变成图上的关系。
方法区中的类静态属性引用的对象。
本地方法栈中JNI(Native方法)的引用对象
活跃线程的引用对象
第二章 垃圾回收算法
2.1 标记-清除算法(Mark and Sweep)
标记:从根集合进行扫描,对存活的对象进行标记。
清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存。
经过扫描,发现A,C,D等不可达,是垃圾对象,所以进行扫描清除。
由于标记清除不需要进行对象的移动,并且进队不存活的对象进行处理,因此标记清楚后会产生大量不连续的内存碎片,随便较多可能导致后续无法找到足够的连续内存而不得不触发另一次垃圾回收动作,如果向上图所以情况,有一个需要占三个内存块的对象要存储,会导致vuter(音译,不知道是啥)一直是暂停状态,collector一直在尝试进行垃圾收集,直到outOfMemory。
2.2 复制算法
将可用的内存按容量,按一定比例划分为两块或者多个块,其中一块或者两块作为对象面,其他的作为空闲面。对象主要在对象面上创建,当被定义为对象面的内存用完之后,就将存活的对象从对象面复制到空闲面,然后将对象面所有对象内存清除。
这种算法适用于对象存活率低的场景,比如年轻代,这样每次都会整个半区进行回收也就不用考虑碎片等情况了。推导重建只需要移动堆顶指针然后推导重建即可。
总的来说:
解决碎片化问题,顺序分配内存,简单高效,适用于对象存活率低的场景。
市面上商用的虚拟机都采用此方法回收年轻代。
复制算法在应对存错率较高的情况就有点力不从心了,要进行较多的复制操作,更关键的是,如果不想浪费百分之50的空间,就需要有额外的空间进行担保,以应对所有对象都百分之百存活的极端情况。
2.3 标记-整理算法(Compacting)
标记:从根集合进行扫描,将存活的对象进行标记。
清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。
通过可达性算法标记可回收的对象,清除时会将存活对象压缩到内存一端,之后把存活对象以外的内存空间清除掉。
好处:避免了内存的不连续性,不用设置两块内存互换,适合于存活率高的场景,如老年代的回收。
2.4 分代收集算法
将堆内存进一步划分,将生命周期不同的对象划分到不同的内存区域,让不同的区域以采用不同的垃圾回收算法。
jd6,jdk7时候:
堆内存可以分为年轻代,老年代,永久代这三个区域。
jdk8以后的版本:
永久代被去掉了,只剩下了年轻带和老年代。
年轻带存活率低会采用复制算法,老年代存错率高会采用标记-清除或标记-整理算法.
分代收集算法的GC分为两种
2.4.1 Minor GC
Minor GC:发生在年轻代中的垃圾收集动作,所采用的是复制算法。
年轻代几乎是所有java对象出生的地方,即java申请的内存以及存放都是在这个地方,java中大部分对象不需要长久存活,有朝生夕灭的性质,新生代是垃圾收集的频繁区域。
年轻代:尽可能快速地收集掉那些生命周期短的对象。
年轻代主要分为Eden区和两个Survivor区,对象刚被创建出来的时候,内存空间首先分配在Eden区的,如果Eden区放不下,对象也可能被直接放在Survivor区,甚至是老年代中,两个Survivor区被定义为from区与to区,哪个是from哪个是to不是固定的,会随着垃圾回收的进行相互转换。
每次使用Eden和其中的一块Survivor,当进行垃圾回收时将Eden和Survivor中存活的对象,一次性复制到另一块Survivor空间上,最后清理掉Eden区和刚刚的Survivor空间。当Survivor空间不够用的时候则需要老年代进行分配的担保。
具体步骤:
首先对象在Eden出生被挤满
触发依次Minor GC,清理Eden区域并将其中的存储哦对象转到S0(Survivor)中,并将其年龄加1。
假设Eden区再次被填满,则将Eden和S0中的存活对象都移动到S1中,并代数加1。
Eden又满了,则将Eden和S1中的存活对象都移动到S0中,并代数加1。
如此周而复始,达到某个值时,默认是15,这些对象会成为老年代,当然这也不是固定的,有的较大的对象,Eden或者Survivor装不下的就会进入老年代。
常用的调优参数:
2.4.2 Full GC
老年代:存放生命周期较长的对象
主要采用标记清理或者标记整理方法进行回收。
当触发老年代的垃圾回收时通常新生代也会进行垃圾回收,即对整个堆进行垃圾回收,这便是所谓的Full GC,MajorGC通常与Full GC是等价的。
Full GC比MinorGC慢,但执行频率低。
触发FullGC的条件:
创建一个大对象,Eden区域不足,选择保存在老年代中,老年代空间也不足,触发FullGC。
永久代空间不足(jdk以前)
CMS GC时出现promotion failed,concurrent mode failure。promotion failed在进行Minor GC时Survivor放不下,老年代也放不下,所以触发。concurrent mode failure是在执行CMS GC时同时有对象要放入老年代中,但是老年代空间不足,便会触发。
Minor GC晋升到老年代的平均大小大于老年代的剩余空间。
调用System.gc() (只是提醒,具体是否回收看虚拟机)
使用RMI来进行RPC或管理的JDK应用,每小时执行以此Full GC。
第三章 新生代垃圾收集器
学习垃圾收集器之前要先学习几个名词:
Stop-the-world:意味着JVM由于要执行GC而停止了应用程序的执行,这种情况会在任意一种算法中发生。GC优化很多时候就是指减少Stop-the-world时间,使系统具有高吞吐,低停顿的特点。
Safepoint:分析过程中对象引用关系不会发生变化的点。GC相当于是保洁阿姨打扫卫生,如果阿姨一边打扫卫生,一边有人扔垃圾该怎么办呢?保洁阿姨可以在开始打扫前,就不让其他人扔垃圾,这样就解决了,在可达性分析中,要分析哪个对象没有引用了的时候,必须在一个快照的状态点进行,在这个点线程都被冻结了,不可以出现对象引用关系还再不断变化的情况,因此分析结果需要在某个点具有确定性,节点叫做安全点,一般在方法调用,循环跳转,异常跳转等才会产生安全点,一旦GC发生让所有的线程都跑到安全点在停顿下来,如果发现线程不在安全点,就恢复线程,等线程跑到安全点再说,安全点不能太少也不能太多,太少会让GC等待太长的时间,太多增加程序的负荷。
JVM运行模式: JVM有两种运行模式, Client模式启动较快,Server模式启动较慢, 但是启动进入稳定期运行之后,Server模式的运行速度会比Client要快,因Server模式启动的JVM需要重量级虚拟机,对程序采用了更多的优化,而Client采用的JVM是轻量级的虚拟机.
即使经过长时间的发展,java的垃圾收集器仍在不断地演进中,不同大小的设备,不同特征的运用场景都需要有不同的垃圾收集器来满足特定的要求,因此垃圾收集器不存在哪个好,哪个坏这一说.不同的厂商,不同的JVM,提供的选择也不同.
一些常见的垃圾收集器以及它们之间的关系还有适用范围:
新生代的垃圾收集器在上面.老年代的在下面,它们之间有连线的话表明可以搭配使用.
3.1 Serial收集器
可以通过在程序启动的时候设置+UseSerialGC来启动这个收集器.
Serial收集器是java虚拟机中最基本也是历史最悠久的收集器.
单线程收集,进行垃圾收集时,必须暂停所有工作线程.
优点是:简单高效,Client模式下默认的年轻代收集器.
一次收集不超过100ms,是可以接受的.
3.2 ParNew收集器
多线程收集,其余的行为,特点和Serial收集器一样.默认开启的收集线程数与CPU相同,在CPU数量非常多的情况下,可以使用某参数来限制线程个数.
单核执行效率不如Serial,在多核下执行才有优势.
它能与CMS收集器配合工作,是现在主流的年轻代收集器,使用的是复制算法.
3.3 Parallel Scavenge
系统吞吐量:
使用复制算法,多线程.
之前的是关注停顿时间,现在的是垃圾收集器更关注吞吐量.
在多核下执行才有优势,Server模式下默认的年轻代收集器.
如果对垃圾收集器不了解,我们可以使用此收集器配合UseAdaptiveSizePolicy参数,它会把内存的调优交给虚拟机完成.
第四章 老年代垃圾收集器
4.1 Serial Old收集器
它是Serial的老年代版本.
单线程收集,进行垃圾收集时,必须暂停所有工作线程.
简单高效,Client模式下默认的老年代收集器.
4.2 Parallel Old收集器
Jdk6之后开始提供,在此之前Parallel Scavenge一直处于比较尴尬的状态,如果新生代选择Parallel Scavenge,那么老年代除Serial Old别无选择,由于Serial Old在服务端上的拖累,使用了Parallel Scavenge也未必能达到吞吐量最大的效果.
直到Parallel Old出现,Parallel Scavenge终于有了最终的应用场合,
它是多线程,以吞吐量优先.
4.3 CMS收集器
它的整个垃圾回收过程可以分为以下六步:
1.初始标记 : stop-the-world
在这个过程中需要虚拟机停下正在执行的任务,这个过程从垃圾回收的根对象开始,只扫描到和跟对象直接关联到的对象,并做标记,
2.并发标记:并发追溯标记,程序不会停顿.
在初始标记的基础上,继续向下标记,和用户的程序并发执行.
3.并发预清理: 查找执行并发标记阶段从年轻代晋升到老年代的对象.
通过重新扫描,减少下一阶段重新标记的工作.
4.重新标记:暂停虚拟机,扫描CMS堆中的剩余对象.
扫描从跟对象开始,并向下关联.
5.并发清理:清理垃圾对象,程序不会停顿.
6.并发重置:重置CMS收集器的数据结构.等待下一次垃圾回收.
它采用的是标记清除算法,这样会带来内存空间碎片化的问题,如果碰到需要较大的连续内存空间,则只能触发一次GC.
4.4 Garbage First收集器
Garbage First收集器既用于年轻代,也用于老年代的收集器,hotspot开发团队赋予Garbage First的使命是未来可以替换掉JDK5中发布的CMS收集器,与其它收集器相比,Garbage First有如下的特点.
1.并发和并发,使用多个CPU来缩短stop-the-world的时间,与用户线程并发执行.
2.分代收集.独立管理整个堆,但是能够采用不用的方式处理新创建对象,和熬过多次GC的旧对象.
3.空间整合:基于标记整理算法
4可预测的停顿:可指定在长度n毫秒的时间内,消耗在垃圾收集上的时间不得超过m毫秒.
会将整个java堆内存划分成多个大小相等的独立Region,年轻代和老年代不再物理隔离,新生代和老年代可以是不连续的集合.在JVM启动时不需要再决定哪个是年轻代,哪个是老年代,随着年轻代被收集后,它会变为可用状态,也可以分配成老年代.
G1的年轻代收集和其它的收集器一样,都是回收整个年轻代,但是它的老年代不需要整个老年代进行回收,只有一部分region被调用,Garbage First年轻代由eden region和survivor region组成,当一个JVM分配eden region失败后就会触发一个年轻代回收,G1年轻代收集器会移动所有存储对象,从edne region到survivor region这就是copy出survivor的过程.