垃圾回收机制



垃圾回收

垃圾回收(Garbage Collecting ,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡或者长时间没有使用的对象进行清除和回收。垃圾回收机制就是java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据内存空间的一种机制。

判断对象是否存活的算法

1、引用计数法

引用计数法的做法是为每个对象添加一个引用计数器,用来统计指向该对象的引用个数,当有地方引用该对象时计数器加1,当引用失效时计数器减1,用对象计数器是否为0来判断对象是否可被回收。一旦某个对象的引用计数器为0,则说明该对象已经死亡,便可以回收了。

(1)具体实现

如果有一个引用,被赋值为某一对象,那么该对象的引用计数器 + 1 。如果一个指向某一对象的引用,被赋值为其他值,那么将该对象的引用计数器 - 1 。也就是说,我们需要截获所有的引用更新操作,并且相应的增减目标对象的引用计数器。

(2)缺点

除了需要额外的空间来存储计数器,以及繁琐的更新操作,引用计数法还有一个重大的漏洞——无法处理循环引用对象。

举个例子,假设对象 a 与 b 相互引用,除此之外没有其他引用指向 a 或者 b 。在这种情况下,a 和 b 实际上已经死了,但由于它们的引用计数器皆不为0,所以这些循环引用对象所占据的空间将不可回收,从而造成了内存泄露。

【JVM】垃圾回收算法_老年代

2、可达性分析

目前Java虚拟机的主流垃圾回收器采取的是可达性分析算法。可达性分析算法的实质在于将一系列GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程也称之为标记(Mark)。最终,未被探索到的对象便是死亡的,是可以回收的。

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200707181157852.png =500px)

可达性分析可以解决引用计数法不能解决的循环引用问题。还拿上面例子说明,即便对象 a 和 b 相互引用,只要从GC Roots出发无法到达 a 或者 b ,那么可达性分析便不会将它们加入存活对象合集中。

(1)GC Roots

暂时理解为由堆外指向堆内的引用,一般来说,GC Roots包括以下几种:


  • Java方法栈帧中的局部变量
  • 已加载类的静态变量
  • JNI handles
  • 已启动且未停止的Java线程

(2)缺点

在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为null),或者漏报(将引用设置为未被访问过的对象)。

如果发生了误报,Java虚拟机最多损失了部分垃圾回收的机会。漏报比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则很有可能会直接导致Java虚拟机崩溃。

常用垃圾收集(GC)算法

1、标记-清除算法

分为两个步骤:第一就是标记,也就是标记所有需要回收的对象;第二就是清理,标记完成后进行统一的回收带有标记的对象占据的内存空间。这个算法效率不高,而且在标记清除之后会产生大量的内存不连续的内存碎片,当程序运行过程中需要分配较大对象时,无法找到足够的连续内存而造成内存空间浪费。

【JVM】垃圾回收算法_可达性_02

缺点

(1)造成内存碎片。由于Java虚拟机的堆中对象必须是连续分布的,因此可能出现总空闲内存足够,但是无法分配的极端情况。

(2)分配效率低。如果是一块连续的内存空间,可以通过指针加法来做分配。对于空闲列表,Java虚拟机则需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存。

2、复制算法

复制算法是将内容容量划分成大小相等的两块,每次只使用其中的一块。当一块内存用完之后,就将还存活的对象复制到另一块上面,然后再把已使用的内存空间一次性清理。这样使得每次都对其中一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只是这种算法的代价就是将内存缩小为原来的一半了。

【JVM】垃圾回收算法_引用计数_03

3、标记-整理算法(或叫压缩算法)

标记整理算法和标记清除算法很相似,显著的区别是:标记清除算法只对不存活的对象进行处理,剩余存活对象不做任何处理,所以造成了内存碎片的问题;而标记整理算法对不存活的对象进行清除,还对存活的对象进行重新整理,因此不会产生内存不连续的现象。

【JVM】垃圾回收算法_老年代_04

分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。其核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。然后根据不同的区域采用合适的收集算法,它本身并不是一个新的收集算法。在jdk1.7之前,对JVM分为三个区域:新生代,老年代,永久代。

(1)新生代(复制算法)

新生代的目标就是尽可能快速的收集掉那些生命周期较短的对象,一般情况下新生成的或者朝生夕亡的对象一般都是首先存放在新生代里。

因为新生代会频繁的进行GC清理,所以采用的是复制算法,先标记出存活的实例,然后清除掉无用实例,将存活的实例根据年龄(每个实例被经历一次GC后年龄会加1)拷贝到不同的年龄代。

【JVM】垃圾回收算法_引用计数_05

(2)老年代(标记整理或标记清除算法)

老年代一般存放的是一些生命周期较长的对象,比如是新生代中经历了N次垃圾回收后仍然存活的对象都进入了老年代。

这块内存区域一般大于年轻代,GC发生的次数也比年轻代要少。

在老年代中因为对象存活率较高,没有额外的空间对它分配担保,就必须使用标记清除或标记整理。

(3)永久代

永久代主要存放静态文件,如java类,方法等,永久代对垃圾回收没有显著影响。

方法区主要回收的内容有:废弃的常量,无用的类,对于废弃的常量可以通过引用的可达性分析判断,但是对于无用类需要同时满足以下三个条件:

1、该类的所有实例都已经被回收了

2、加载该类的ClassLoader已经被回收了

3、该类对于的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

何时触发GC(垃圾收集)?


  1. 执行System.gc()的时候
  2. 老年代空间不足,一次Full GC之后,然后不足 会触发Java.outofmemoryError.java heap space
  3. 永久代空间不足,永生代或者永久代,java.outofMemory PerGen Space
  4. minor 之后 survivor 放不下,放入老年代,老年代也放不下,触发FullGC,或者新生代有对象放入老年代,老年代放不下,触发FullGC
  5. 新生代晋升为老年代的时候,老年代剩余空间低于新生代晋升为老年代的速率,会触发老年代回收
  6. new 一个大对象,新生代放不下,直接到老年代,空间不够,触发FullGC

如何避免频繁的GC?


  1. 不要频繁的new对象
  2. 不要显式的调用system.gc()
  3. 不要用String+ ,使用StringBuilder
  4. 不要使用Long ,Integer,尽量使用基本类型
  5. 少用静态变量,不会回收
  6. 可以使用null进行回收