首先JVM的内存结构包括五大区域: 程序计数器、虚拟机栈、本地方法栈、方法区、堆区。其中程序计数器、虚拟机栈和本地方法栈3个区域随线程启动与销毁, 因此这几个区域的内存分配和回收都具有确定性,不需要过多考虑回收的问题。而Java堆区和方法区则不一样,这部分内存的分配和回收是动态的,正式垃圾回收需要关注的部分。
垃圾回收在堆内存进行回收前, 要先确定区域的哪些对象是可以被回收的、那些对象暂时还不能回收,下面谈一谈判断对象是否存活的算法。
判断对象是否存活的算法
1.引用计数算法
引用计数算法:堆中的每个对象实例都有一个引用计数器,当一个对象被创建时,就将该对象实例分配给一个变量,该引用计数器设置为1,当任何其他变量被赋值为这个对象的引用时,计数加1,当一个对象实例的某个引用超过了生命周期或被赋为一个新值时, 引用计数减1。
任何引用计数器为0的对象实例都可以进行垃圾回收。当一个对象实例被垃圾回收时,它引用的所有对象实例引用计数器减1.
优点:引用计数器可以很快的执行,对程序不需要长时间的打断
缺点:无法检测出循环引用。如对象A有对象B的引用,对象B又有对象A的引用,这样他们的引用计数永远都不为0
2.可达性分析算法
可达性算法:将所有的引用关系看作一张图,从一个节点GC Root开始,寻找对应的引用节点,找到后继续寻找这个节点的引用节点,当所有引用节点寻找完毕后,剩余的节点就被认为是没有被引用的节点,即无用节点,无用节点被判定为可回收对象。
Java中可以作为GC Root的包括下面几种:
- 虚拟机栈中的引用对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
对于Java中的引用类型可以看这篇文章Java 控制类的引用类型,合理使用内存
常用的垃圾回收算法
1.标记-清除算法
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行垃圾回收
这种算法实现起来比较容易,但是会造成内存碎片
2.标记-复制算法
复制算法是为了解决标记-清除算法的缺陷而提出的。
它将内存划分为大小相等的两块,每次只使用其中的一块。当这A快内存用完了,就将还存活的对象复制到B块上面,然后把A块的内存空间一次性清理掉
这种算法虽然实现简单,运行高效且不易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能使用的空间缩减为原来的一半。很显然,复制算法的效率跟存活对象的数量有很大关联,若存活对象很多,那么效率将大大降低
3.标记-整理算法
该算法是为了解决复制算法的缺陷,充分利用内存空间而提出的。
该算法与标记-清除算法一样,但是在完成标记后,不直接清理可回收对象,而是将存活对象全部向一端移动,接着清理掉边界以外的内存。
4.分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。其核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。
将其分为年轻代、老年代和永久代。然后根据不同的区域采用合适的收集算法。
Java一般将堆区分为年轻代和老年代,将方法区划为永久代。
下面对不同的年龄代进行简单说明
年轻代:新创建的对象都存放在这里。因为年轻代会频繁的进行GC清理,JVM在年轻代采用的是标记-复制算法,先标记出存活的实例,然后清除掉无用实例,将存活的实例根据年龄(每个实例被经历一次GC后年龄会加1)拷贝到不同的年龄代。
老年代:老年代中是经历了N此垃圾祸首后仍然存活的对象,其中的N由JVM的参数决定。这块内存区域一般大于年轻代。GC发生的次数也比年轻代要少。
永久代:用于存放静态文件,如Java类、方法等。为方法区。
方法区主要回收的内容有:废弃的常量、无用的类,对与废弃常量可以同过引用的可达性判断,但是对于无用类需要同时满足以下3个条件:
- 该类的所有实例都已经被回收了
- 加载该类的 ClassLoader 已经被回收了
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
GC在什么时候触发
GC在优先级最低的线程中运行,一般在应用程序空闲时被调用。当内存不足时才会主动调用
因为对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有如下两种:
1.Scavenge GC
一般情况下,当新对象生成,并且在年轻代申请空间失败时,会触发Scavenge GC, 对年轻代进行垃圾回收。这种方式的GC不会影响到老年代。因为大部分对象都是年轻代开始的,同时年轻代内存不会分配的很大,所有年轻代的GC会频繁的进行。所以在这里要使用速度快、效率高的算法,使其空间尽快空出来。
若GC一次后仍不能满足内存分配,JVM会进行二次GC,若仍无法满足,则报“out of memory"的错误,Java应用将停止
2.Full GC
对整个内存进行整理,包括年轻代、老年代和永久代,所以Full GC比Scavenge GC要慢, 因此应该尽量减少Full GC的次数。以下可能引发Full GC的原因:
- 老年代被写满
- 永久代被写满
- System.gc()被显示调用
- 上一次GC后堆的各域分配策略动态变化。
Java的垃圾回收介绍到这,下面在说说如何在程序中减少GC的开销的几个建议:
- 不要显式调用System.gc()。此函数建议JVM进行GC,虽然只是建议,但是大多数情况下会触发GC,增加了间歇性停顿的次数,大大影响系统的性能
- 尽量减少临时对象的使用。也就是减少Scavenge GC执行的机会
- 对象不用时最好显式置为null。将不用的对象置为null,有利于GC收集器判定,从而提高GC的效率
- 尽量减少静态对象变量。静态变量属于全局变量,不会被GC祸首。
- 能有基本类型的就不要用包装类。基本类型变量栈用的内存资源比对应的包装类要少的多
- 使用StringBuffer 而不是String类累加字符串。因为堆String类型进行加的时候,会创建新的String对象,而StringBuffer是可变长的,在原有基础上进行扩增,不会产生中间对象
- 分散对象创建或删除的时间。集中在短时间内大量创建新对象,特别是大对象,会突然需要大量内存,JVM在面临这种情况时只能进行GC,以回收内存或整合内存碎片,从而增加GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。