说到Java虚拟机不得不提的一-个词就是“垃圾回收”(GC, Garbage Collection)垃圾回收的执行速度则影响着整个程序的执行效率
所以我们需要知道更多关于垃圾回收的具体执行细节
以便为我们选择合适的垃圾回收器提供理论支持.
面试题
如何判断一个对象是否“死亡”?垃圾回收的算法有哪些?
垃圾回收器首先要做的就是,判断一个对象是存活状态还是死亡状态
死亡的对象将会被标识为垃圾数据并等待收集器进行清除
判断一个对象是否为死亡状态的常用算法有两个:
引用计数器算法和可达性分析算法
引用计数算法(Reference Counting)
引用计数算法(Reference Counting)属于垃圾收集器最早的实现算法了
它是指在创建对象时关联一个与之相对应的计数器
当此对象被使用时加1,相反销毁时-1
当此计数器为0时,则表示此对象未使用,可以被垃圾收集器回收
- 优点: 垃圾回收比较及时,实时性比较高 只要对象计数器为0 则可以直接进行回收操作
- 缺点: 无法解决循环弓|用的问题
public class TestUtils {
public static void main(String[] args) {
Test1 test1= new Test1();
Test2 test2= new Test2();
test1.setTest2(test2);
test2.setTest1(test1);
test1=null;
test2=null;
}
}
class Test1{
public Test2 getTest2() {
return test2;
}
public void setTest2(Test2 test2) {
this.test2 = test2;
}
public Test2 test2;
}
class Test2{
public Test1 getTest1() {
return test1;
}
public void setTest1(Test1 test1) {
this.test1 = test1;
}
public Test1 test1;
}
如上代码所示 . 这里的代码如果使用引用计数算法 对象永远不会被回收.
可达性分析算法(Reachability Analysis)
可达性分析算法(Reachability Analysis)是目前商业系统中所采用的判断对象死亡的常用算法
它是指从对象的起点(GC Roots)开始向下搜索
如果对象到GC Roots没有任何引用链相连时,也就是说此对象到GC Roots不可达时
则表示此对象可以被垃圾回收器所回收
- 标记-清除算法
标记-清除(Mark-Sweep) 算法属于最早的垃圾回收算法,它是由标记阶段和清除阶段构成的, 标记阶段会给所有的存活对象做.上标记,而清除阶段会把没有被标记的死亡对象进行回收 而标记的判断方法就是前面讲的引用计数算法和可达性分析算法
缺点:标记-清除算法有一个最大的问题 产生内存空间的碎片化问题, 比如 : 当程序需要给一个大对象进行赋值的时候,而没有足够的内存的时候,就需要进行一次垃圾回收. - 标记-复制算法
标记-复制算法是标记-清除算法的一-个升级,使用它可以有效地解决内存碎片化的问题,它是指将内存分为大小相同的两块区域,每次只使用其中的- -块区域,这样在进行垃圾回收时就可以直接将存活的东西复制到新的内存上然后再把另-块内存全部清理掉.
缺点: 标记-复制的算法虽然解决了内存碎片话的问题,但是又带来了新的问题,此算法导致了内存的可用率大幅降低. - 标记-整理算法
标记-整理算法的诞生晚于标记-清除算法和标记-复制算法
它也是由两个阶段组成的:标记阶段和整理阶段其中标记阶段和标记-清除算法的标记阶段一样,不同的是后面的一个阶段标记一整理算法的后一个阶段不是直接对内存进行清除而是把所有存活的对象移动到内存的一端,然后把另一端的所有死亡对象全部清除.
面试题:Java中可作为GC Roots的对象有哪些?
面试题: 说一下死亡对象的判断细节
当使用可达性分析判断一个对象不可达时, 并不会直接标识这个对象为死亡状态而是先将它标记为“待死亡”状态再进行一次校验.
校验的内容就是此对象是否重写了finalize() 方法,如果该对象重写了finalize()方法,那么这个对象将会被存入到F-Queue队列中,等待JVM的Finalizer线程去执行重写的finalize()方法
在这个方法中如果此对象将自己赋值给某个类变量时,则表示此对象已经被引用了
因此不能被标识为死亡状态,其他情况则会被标识为死亡状态
public class Test {
public static Test result;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("执行了回收方法~~~~");
Test.result=this;
}
public static void main(String[] args) throws InterruptedException {
result = new Test();
result=null;
System.gc();
Thread.sleep(500);
if (Objects.nonNull(result)) {
System.out.println("存活状态~~~~");
}else {
System.out.println("死亡状态~~~~");
}
result=null;
System.gc();
Thread.sleep(500);
if (Objects.nonNull(result)) {
System.out.println("确认存活状态~~~~");
}else {
System.out.println("确认死亡状态~~~~");
}
}
}
//打印结果为
//执行了回收方法~~~~
//存活状态~~~~
//确认死亡状态~~~~
第一次执行了finalize()方法,成功的把自己从待死亡状态拉了回来
第二次同样的代码却没有执行finalize() 方法,从而被确认为了死亡状态,因为任何对象的finalize()方法都只会被系统调用一次.
虽然可以从finalize()方法中把自己从死亡状态“拯救”出来,但是不建议这样做因为所有对象的finalize()方法只会执行一次
因此同样的代码可能产生的结果是不同的,这样就给程序的执行带来了很大的不确定性.