Q什么是垃圾:
运行程序中,没用任何指针指向的对象。
Q为什么需要垃圾回收?
内存只分配,不整理回收,迟早会被消耗完。
内存碎片的整理,为新对象腾出空间
没有GC程序无法正常进行。
Q 哪些区域有GC,哪些区域会有OOM异常(错误)?
堆和方法区是线程共享的,存在GC 和OOM
堆有新生代和养老代,默认比例1:2 ,
新生代分为Eden :s0:s1 默认比例 8:1:1 (实际发现JDK8是6:1:1,一度怀疑是自适应策略,结果不是)
新生代中有YGC/MinorGC, 当Eden区满的时候触发,并使用复制算法,和分代策略,将Eden区和from区的存活对象 放到to区,如果存不下,就直接晋升老年代。
其余对象在对象头的分代阈值为15时晋升到老年代。
老年代垃圾回收为MajorGC,一般是老年代满的时候会被触发。为全堆回收
方法区若发生GC,为FUllGC,此时会对全堆以及方法区进行垃圾回收。
简单一句话:频繁收集年轻代,较少收集老年代,基本不动元空间。
PC寄存器 即没用GC 也没用OOM
虚拟机栈,无GC,有OOM 当栈的大小可以被动态扩容时,申请扩容的大小已经超过了内存可以支配空间,发生OOM,
StackOverFlowError,当栈空间大小是固定的,当前栈帧没有足够空间入栈了,此时方式 SOFERROR
Q:垃圾回收相关算法
标记阶段:引用计数和可达性分析算法。目的:判断对象的存活。
引用计数:
一个对象A,有一个引用计数器。当A被任何一个对象引用了,则A的计数器加1。引用失效,引用计数器则减1。
PS:什么叫A被一个对象引用了?举个例子。
class ReferenceClass{
//static filed
public static A a =new A();
}
对象(new A()),被对象 ReferenceClass的静态变量引用,我们知道类变量的初始化是在类加载三部曲的初始化阶段<clint里>,随着类卸载而消亡。
若ReferenceClass类的生命周期不结束。对象(new A())就会一直被类变量a 引用着。
这里再发散:静态变量在逻辑上是存放在方法区的,从JDK7以后,静态变量和字符串常量池就存放在了堆空间中。
引用计数法有个缺陷:循环引用问题。
注意:java在标记阶段并没有使用引用计数算法。
在分析引用、对象等问题的时候。
一定要注意一个问题,这个引用到底是在方法中(局部变量),还是在类内的成员变量上
因为从内存解构上,局部引用是在虚拟机栈的局部变量表中的,而类内的成员变量引用,是在堆内的。
比如:
class InstanceA{
//此引用的位置是在对象内存解构中的
Object ref =null;
public static void main(String[] args){
//这个引用,a1,是在局部变量表中的
Instance a1 =new Instance();
//这个ref 是在堆中,对象体内的
a1.ref =a1;
//操作数栈指向堆内对象的指针断开。a1.ref 是在堆内又指向自己。
a1 =null;
}
}
Python使用的就是引用计数:解决循环引用的两个方法:
手动解除引用。
使用弱引用。
可达性分析算法:(追踪性垃圾收集)
首先要搞清楚,什么是GC Roots
GC roots 是一组集合,它包括:
1、虚拟机栈中的引用的对象
比如 各个线程中被调用的方法中使用到的参数,局部变量等
2、本地方法栈引用的对象。
3、方法区静态属性引用的引用的对象。如上面的例子,A是引用类型的静态类型变量,它就是一个典型的GC root
class ReferenceClass{
//static filed
public static A a =new A();
}
4、方法区中常量引用对象。
class Demo{
String s ="abc";
public void foo(){
String ddd="XXXYYY"; //局部变量表最大slot深度为2,ddd为局部变量表中变量,XXXYYY在常量池中
}
}
5、所有被同步锁持有的对象 同步监视器
6、java虚拟机内部的引用:
各种常驻对象,比如NUllPointerException,OOM,还有系统类加载器。基本数据类型的Class对象
关于Class对象的内存模型:
7、根据不同的垃圾收集器以及当前回收区域不同,也会有一些临时性的GC roots对象加入。
比如使用G1回收器时,新生代的region里的对象,被老年代的某些对象所引用。此时,老年代的引用,就是临时的GC Roots
即指向某一堆内存中的对象的引用(指针),其本身与被引用对象不在一个回收的逻辑区内,它就是GC roots
为了保证GC roots的准确性,就需要在可达性分析时,内存是一个快照状态,而非运行时。保证其一致性。
此时就会产生STW stop the world。
补问:Q 对象的finalization机制
对象终止机制:系统进行垃圾回收之前,会调用该对象的finalize()方法。
该方法是Object类所有,允许被子类所重写。用于在对象回收时进行资源释放,清理等操作。
但是注意,不要主动去调用某个对象的finalize方法,而是交给垃圾回收机制去调用(GC的finalizer守护线程)。
对象可能有三种状态:
可触及的:从根节点开始,可以到达这个对象
可复活的:无引用的对象,可以在 finalize()中复活。
不可触及的:对象的finalize()被调用,但是没用复活,此时对象为不可触及状态,finalize只能被调用一次
只有对象处于不可触及状态,才能被回收。
清除阶段:
标记-清除(mark-sweep)
注意,标记的不是垃圾,而是非垃圾(可达对象)。
两次遍历:
1、标记阶段,从根节点依次向下逐一遍历,找到所有的引用链。(递归遍历)
2、清除阶段 对堆内存从头至尾线性遍历,找到没用标记的对象,进行回收。
缺点:清理出来的空闲空间不连续,在新分配对象时候,内存分配采用空闲列表
复制算法:
原理和思路,就像我们理解的YGC的回收策略,Eden from to 来回倒腾。
注意点: 复制算法适合存活对象比较少的内存空间,如果对象过多,复制成本是很大的。
一般用在新生代回收
标记-压缩 mark-compact
在mark-sweep之后,进行了一次 压缩整理。
可以理解为mark-sweep-compact
其特点是 对象在发生了移动。
整理后,空闲区是规整的,新对象进行内存分配时候,可以进行指针碰撞,不再需要维护一个空闲列表
整体来说,复制算法最快,但是要移动对象,且浪费内存。
标记压缩速度最慢,且移动对象,但是空间开销很少,且没用内存碎片
标记清除速度中等,不需要移动对象,空间开销小,但是会产生一些内存碎片。
分代收集:
对不同生命周期的对象采取不同的收集方式,提高回收效率。
比如我们现在用的Hotspot虚拟机将对象分为:
年轻代
老年代
增量式收集:用户线程与GC线程并发执行,尽可能减少STW
其实仍是给予标记清除和 复制算法。允许垃圾收集线程以分阶段的方式,完成标记、清理或复制。
但是这样频繁进行线程和上下文切换,增大系统开销,降低系统吞吐量,而且并发执行,要处理好一致性问题,对垃圾与非垃圾要做进一步的修正标记。