深入jvm 07. 垃圾回收(一)

1、GC是什么?为什么要有GC?

垃圾指的是运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。GC(Garbage Collector) 是指回收堆中死亡对象所占据的空间。

如果只分配内存空间,而不进行回收,那么内存迟早会被消耗完。另外,垃圾回收还可以清理内存碎片,将占用的堆内存移到堆的一端,这样在堆中空出连续的空闲内存空间。

2、GC判定的方法

方式一:引用计数法
它的做法是为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。对于一个对象A,如果有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。

缺点是:①需要额外的空间来存储引用计数器;②每次赋值都要更新计数器,增加时间开销;③无法处理循环引用问题(致命问题),比如对象A和对象B相互引用,除此之外没有任何其他引用指向A和B,这两个对象实际已经死亡,但各自的引用计数器都不为0,使用引用计数法会将它们都判断为存活对象,无法回收循环引用对象所占据的空间,从而造成内存泄露。

方式二:可达性分析算法
可达性分析算法是java虚拟机主流垃圾收集器所采用的算法。它的做法是将一系列 GC Roots 作为初始的存活对象集合,然后从该集合出发,探索所有能够被该集合引用到的对象,然后将其加入该集合,这个过程称之为标记。最后未被探索到的对象,则可以被回收。

GC Roots 可以包含:①java方法栈帧中的局部变量;②已加载的类的静态变量;③本地方法栈内JNI(本地方法)引用的对象;④所有被同步锁持有的对象;⑤字符串常量池里的引用。

3、GC算法有哪些?

使用可达性分析算法,将垃圾对象标记完成后,需要进行后续清理工作。有如下三种GC算法:
1)标记-清除算法:把垃圾对象所占据的内存标记为空闲内存,并记录在一个空闲列表中。当需要新建对象时,会从空闲列表中寻找一块可以容纳该对象的空闲内存,分配给这个对象。

缺点:①会造成内存碎片。因为堆中的对象必须是连续分布的,所以可能会出现总的空闲内存足够,但是无法分配新对象的极端情况;②分配效率较低。如果是一块连续地空闲内存,那么通过指针加法( pointer bumping )即移动指针,就可以进行分配,而对于空闲列表,则需要逐个访问列表中的项,以寻找到足够容纳该新建对象的空闲内存。

2)标记-压缩算法:把存活的对象压缩整理到内存区域的起始位置,那么剩余的内存就是连续的了,这样解决了内存碎片化问题。

缺点:需要进行额外的内存碎片的压缩整理。

3)复制算法:把内存区域两等分,分别用from指针和to指针来维护,始终在from指针所指区域进行内存分配,to所指的区域是空白的。当发生垃圾回收时,将from区域的存活对象全部复制到to区域,然后清空from区域,最后交换from指针和to指针。这同样解决的内存碎片化问题。

缺点:只有一半的内存空间被使用,内存利用率很低。

4、GC分代回收思想

一般来说,大部分的java对象具有"朝生夕死"的特征。基于这个特征,出现了java虚拟机分代回收思想。堆空间被划分为两代,分别是新生代和老年代。新生代又被划分为Eden区和两个大小相同的Survivor区(也称from区和to区),Eden区和Survivor区的比例是动态调整的,根据Survivor区的使用情况动态调整。

4.1、如何选择合适的垃圾收集算法

1)新生代中的大部分对象存活时间较短,很多是"朝生夕死"。采用复制算法,只需要复制少量存活对象,就可以完成垃圾回收。新生代的GC称为Minor GC。

2)老年代中的对象存活时间较长,也没有额外的空间进行分配担保,因此需要采用标记-整理或标记-清除算法。老年代的GC称为Major GC,也称Full GC(即整堆的GC,因为老年代GC伴随新生代的GC)。

4.2、TLAB
当调用new指令时,会在Eden区分配一块内存来存储对象。由于堆空间是线程共享的,所以在这里边分配空间是需要进行同步操作的,否则有可能出现两个对象共用一段内存的情况。

因此,java虚拟机使用了TLAB(Thread Local Allocation Buffer)机制。每个线程可以向java虚拟机申请一段连续地内存,作为线程私有的TLAB。这个操作需要加锁,线程维护两个指针,一个指向TLAB空闲内存的起始位置,一个指向末尾。分配对象时,直接使用指针加法。如果TLAB中没有足够的空间分配给对象,则会尝试在Eden中进行分配。

4.3、记忆集与卡表
Minor GC理论上是不用对整个堆进行垃圾回收,但是却存在老年代的对象引用新生代对象的情况。此时,只需要在新生代建立一个全局的数据结构,称之为"记忆集"(Remembered Set)。这个结构把老年代划分为若干小块,标识出老年代的那一块存在跨代引用。此后,当发生Minor GC时,只有包含跨代引用的小块内存里的对象才会被加入GC Roots进行扫描。

卡表(Card Table)是最常用的记忆集实现形式。该技术将整个堆划分为一个个512字节地卡,并维护一个卡表,卡表中的每一位可以映射到堆中的一个一个卡上。如果某张卡上存在跨代引用,那对应的卡表中的数组元素值标识为1,表明这张卡是脏卡(Dirty Card)。在垃圾收集时,只要将脏卡中的的对象加入GC Roots进行扫描。
在这里插入图片描述

5、什么情况下会触发垃圾回收

1)显式调用System.gc()方法,该方法表示希望进行一次垃圾回收,但不能保证垃圾回收一定会进行。

2)java虚拟机根据当前内存大小,如果内存不够了,会触发垃圾回收。比如新生代满了,新建对象无法在分配内存,会触发垃圾回收。

finalize()方法
一个对象在经历的GC Roots不可达标记后,还需要进行一次筛选,才能判断该对象是否死亡。筛选条件是该对象是否有必要执行finalize()方法。如果该对象没有覆盖finalize()方法,或者已经执行过1次这个方法,则java虚拟机认为没有必要再执行这个方法,会直接回收;如果覆盖了finalize()方法,且还没有执行过,则该对象会被放置在一个由虚拟机自动建立的、低优先级的队列中,GC会对这个队列中的对象进行第二次标记,如果对象在finalize()方法中拯救了自己,即重新与GC Roots建立连接,那么第二次标记时,该对象将不会被回收,否则会被回收。

6、对象内存分配和垃圾回收

1)对象优先的Eden区分配。当Eden区空间耗尽时,会触发一次针对新生代的Minor GC。首先要将Eden区和from区的存活对象复制到To区,然后交互from和To指针。

2)大对象直接进入老年代。对应参数为-XX:PretenureSizeThreshold,对象所占空间大于这个值,直接在老年代中分配。这个参数只对Serial和ParNew两款垃圾收集器有效,Parallel Scavenge不认识这个参数。

3)一个对象在新生代Eden中创建,并第一次经历Minor GC后依然存活,并且能够被Survivor区容纳,此时对象年龄为1岁。此后新生代对象每熬过1次Minor GC,年龄加1。对象年龄到达15岁(默认),则该对象将晋升到老年代。

4)如果from区相同年龄的对象之和大于该区域所有对象之和的一半,大于等于这个年龄的对象进入老年代。

5)空间分配担保原则。如果发生Minor GC时,存在大量存活对象,并且Survivor区放不下这些对象,那么这时候就需要老年代分配担保,让无法放入Survivor区的对象直接进入老年代,前提是老年代可以放入这些对象。由于老年代也不确定,这次GC后从新生代移动过来的对象能否放得下,所以在发生Minor GC时,jvm会计算每次晋升到老年代的所有对象所占空间的平均值,如果这个值大于老年代的当前剩余空间,则改为Full GC。如果小于,则查看 HandlePromotionFailure 设置是否允许担保失败,如果允许,那只会进行一次 Minor GC,如果不允许,则也要进行一次 Full GC。

猜你喜欢

转载自blog.csdn.net/Longstar_L/article/details/107763602