前言
面试问到 感觉这块还不熟悉,做一个小总结。
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收.
对象的生命周期 与 堆区的划分
对象会随着生命周期的结束而消亡,根据 IBM 的一个测试,有98% 的对象是朝生息死的。只有部分对象的生命周期会很长。所以将内存按照生命周期的长短划分为两个阶段--- 新生代、老年代。新生代每次经历一次垃圾回收机制后,新生代对象就会自增1,直到age到15之后就会进入老年代。然而这时候却出现了一个问题,在新生区满了进行一次垃圾回收之后,可能造成内存空间的不连续性,下次可能放入一个对象的时候,内存空间虽然是够的,但是由于内存空间的不连续性,导致对象放不下去,被逼无奈,只能放入空间大的老年区了,这时候造成一个问题,老年区因为这种情况,很容易被塞满,然后进行一次full GC , 由于老年区空间非常大,Full gc 就会进行的比较缓慢,也会严重导致Java程序垃圾回收的效率,拖慢程序的运行。。将新生代区分为 Eden 区和 S 区,对新生代进行gc,新生代的Eden区存活下来移到s区,给Eden区留好空间,保证下次new的对象有足够的连续空间。但是问题又出现了 对新生代进行下一次gc的时候,S区又出现了内存的不连续性。JVM 设计者又将S区分为大小相等的两块 from 区和 to 区。以上对新生区进行第二次GC后,Eden区和from区存活的对象就会被复制到s区的to区域,然后清空Eden区和from区,清空Eden区和from区,避免了内存的不连续性的产生,下一轮from 和 to 交换角色,如此往复回收垃圾,如果对象age到达15岁,对象就会进入老年代中。这样设计既减缓了老年代full gc的频率,又解决了垃圾回收导致的内存不连续的问题。
Eden划分为什么要是 8 : 1 : 1 呢
在进行对象分配的的话,如果Eden内存是不够的,JVM 就会进行 Minor GC , gc 是一个线程么,线程会占用cpu资源,也就是说会占用其他线程的cpu资源。Eden 区越小,那么他就会频繁产生Minor GC,又因为 98% 对象是朝生息死的,jvm设计者将Eden区和from to 区 划分为 8 : 1 : 1的区域 保证了 每次存活对象空间与空闲对象空间占比为9:1.每次只会浪费10%的内存。如果存活的对象超过了survior空间的一半,就会触发老年代的担保策略,将对象分配到老年代。
永久代和元空间的区别
JDK在1.8的时候将永久代的实现替换为了元空间。
对于永久代来说,内存占用只能占用jvm内存。而对于元空间来说,则可以占用服务器内存。元空间有可能抢占堆的空间。元空间放的类信息。若项目很大的话,持久代的空间被占满了, 再没有配置 cms gc 的情况下,就会进行full gc。
对象进入老年区的条件
- 满足同年对象达到Survivor空间一半的规则
垃圾回收算法
如何确定一个对象是垃圾?
引用计数法。
存在的问题:循环引用导致内存泄漏,内存泄漏导致内存危机。
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假设在这行发生GC,objA和objB是否能被回收?
System.gc();
}
}
由于两个对象互相引用,引用计数不为0,不会被进行垃圾回收,但是实验结果看到,对象被进行回收了,也说明jvm虚拟机并没有采用此方法判断对象是否已死。
可达性分析算法
通过 一种叫gcRoot的对象开始向下寻找,看某个对象是否可达。
可以作为gc root 的东西 : 静态变量、常量、栈帧中的局部变量、类加载器、Thread、JNI、被同步锁持有的对象。本质是一组活跃的指针。
对象已死的时候,就是没必要执行finalize 方法的时候。
标记-清除算法
简单理解就是 找到 然后 扔掉
- 标记 : 找到所有的GC可达对象。
- 清除 : 递归遍历全堆,把所有没有被标记的对象全部清除掉。
很慢、内存碎片化。若并行执行的话,会出现不该回收被回收了的问题。所以出现了串行化,出现了 STOP THE WORLD(垃圾回收算法性能判断的指标)
标记-复制算法
它将可用 内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
复制对象只需要移动对丁指针,按顺序分配,不需要考虑内存碎片化的问题。 空间浪费
标记-整理算法
整理算法:滑动整理,线性整理
所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
滑动整理
采用双指针算法,一个找空闲位置/垃圾位置,一个找存活对象,覆盖垃圾位置/空闲位置,直到两个指针碰到一起,其余清除。
线性整理
把相邻的存活对象整理在一起,不保证内存碎片是否产生。
分代收集算法
- 新生代 :采用标记-复制算法 复制的操作并不是很多。
- 老年代 :标记-清除(CMS)、标记-整理算法。对象存活率高、没有额外的方法对他进行空间分配担保。使用标记清除或者标记清理。