[JAVA笔记]-基础-Java虚拟机的垃圾回收机制

Java虚拟机的垃圾回收机制

垃圾收集GC(Garbage Collection)是Java语言的核心技术之一, 在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给了JVM来处理。对于Java对象来讲,如果说这个对象没有被其他对象所引用该对象就是无用的,此对象就被称为垃圾,其占用的内存也就要被销毁。

相关问题

什么样的对象会被当做垃圾回收

当一个对象的引用(地址值)没有变量去记录的时候/没有被其他对象引用, 该对象就会成为垃圾对象, 并在垃圾回收器空闲的时候对其进行清扫.

public class Jvm_Gc {
    public static void main(String[] args) {
        //当一个对象的引用(地址)没有变量被记录的时候,该对象 就会成为垃圾对象, 并在垃圾回收器空闲的时候对其进行清扫
        MyObject obj = new MyObject();
        obj = null;//对象被赋空值, 没有记录引用/地址, 或者没有对象引用.这就是垃圾对象.
        
        //实例2
        new MyObject().show();
    }
}

class MyObject{
    public void show(){
        System.out.println("我是方法show");
    }
}

标记垃圾的算法?

Java中标记垃圾的算法主要有两种, 引用计数法可达性分析算法

  • 引用计数法 引用计数法就是给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的,可以当做垃圾收集。这种方法实现起来很简单而且优缺点都很明显。
    • 优点 执行效率高,程序执行受影响较小
    • 缺点 无法检测出循环引用的情况,导致内存泄露 优点我们很好理解,那么什么是循环引用呢?我们举一个简单的例子。
public class MyObject {
	public MyObject childNode;
} 
public class ReferenceCounterProblem {
	public static void main(String[] args) {
		MyObject object1 = new MyObject();
        	 MyObject object2 = new MyObject();
        	 object1.childNode = object2;
       	 	 object2.childNode = object1;
    }
} 

从上述代码中我们可以看出,object1和object2并没有任何价值,但是他们循环引用,造成内存泄露。

  • 可达性分析算法 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。 那么什么对象可以作为GCRoot?
    • 虚拟机栈中的引用对象
    • 方法区中的常量引用对象
    • 方法区中的类静态属性引用对象
    • 本地方法栈中的引用对象
    • 活跃线程中的引用对象 可达性分析算法如下图所示 image 那么不可达的对象是否是必死之局呢?答案也是否定的 在可达性分析法中不可达的对象,它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法。当对象没有覆盖finalize方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

如何效验对象是否被回收?

可以重写Object类中的finallize方法,这个方法在垃圾执行的时候,被回收器自动调用执行, 日常编码不使用. 这里我们吧垃圾对象设多一点才会使jvm自动开始调用finalize处理垃圾,我们才能看到控制台的输出.

public class Jvm_Gc2 {
    public static void main(String[] args) {
        for (int i = 0;i <= 1000000; i++){
            new MyObject2();
        }
    }
}
class MyObject2 extends Object{
    @Override
    public void finalize(){
        System.out.println("这么多垃圾,我来处理一下!");
    }
}

怎样通知垃圾回收器回收对象?

直接通知垃圾回收器来处理垃圾,调用Gc()方法.

public class Jvm_Gc2 {
    public static void main(String[] args) {
        for (int i = 0;i <= 1000000; i++){
            new MyObject2();
            System.gc();//直接通知GC
        }
    }
}
class MyObject2 extends Object{
    @Override
    public void finalize(){
        System.out.println("我又来处理垃圾啦!");
    }
}

如何将垃圾回收?

在Java中存在着**四种垃圾回收算法,标记清除算法、复制算法、标记整理算法以及分代回收算法**。我们接下来会分别介绍他们。
  • 标记清除算法 该算法分为“标记”和“清除”两个阶段:标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。它是最基础的收集算法,效率也很高,但是会带来两个明显的问题: • 效率问题 • 空间问题(标记清除后会产生大量不连续的碎片) 该算法具体流程如下图所示 image
  • 复制算法 为了解决效率问题,我们开发出了复制算法。它可以将内存分为大小相同的两块,每次使用其中的一块。当第一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。 简单来说就是该对象分为对象面以及空闲面,对象在对象面上创建,对象面上存活的对象会被复制到空闲面,接下来就可以清除对象面的内存。 这种算法的优缺点也比较明显 • 优点:解决碎片化问题,顺序分配内存简单高效 • 缺点:只适用于存活率低的场景,如果极端情况下如果对象面上的对象全部存活,就要浪费一半的存储空间。
  • 标记整理算法 为了解决复制算法的缺陷,充分利用内存空间,提出了标记整理算法。该算法标记阶段和标记清除一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。 如下图所示: image
  • 分代收集算法 当前虚拟机的垃圾收集都采用分代收集算法,这种算法就是根据具体的情况选择具体的垃圾回收算法。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

GC的执行机制

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。

Scavenge GC

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC

对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC: 1.年老代(Tenured)被写满 2.持久代(Perm)被写满 3.System.gc()被显示调用 4.上一次GC之后Heap的各域分配策略动态变化

Java有了GC同样会出现内存泄露问题

  • 静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。
	Static Vector v = new Vector(); 
	for (int i = 1; i<100; i++) 
	{ 
	    Object o = new Object(); 
	    v.add(o); 
	    o = null; 
	}

在这个例子中,代码栈中存在Vector 对象的引用 v 和 Object 对象的引用 o 。在 For 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 o 引用置空。问题是当 o 引用被置空后,如果发生 GC,我们创建的 Object 对象是否能够被 GC 回收呢?答案是否定的。因为, GC 在跟踪代码栈中的引用时,会发现 v 引用,而继续往下跟踪,就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。也就是说尽管o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。

  • 各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
  • 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

垃圾回收机制的应用场景

  • 尽量不要创建很大的对象
    • 原因:GC回收算法从来不对大对象(>=85000字节)堆进行内存压缩整理,在堆中大的内存块会浪费太多CPU时间.
  • 不要频繁的new生命周期很短的对象
    • 原因:这样频繁垃圾回收频繁压缩可能导致很多内存碎片.
Java  jvm 
更新时间:2019-10-18 15:09:08

本文由 Alicyu 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.alicyu.com/archives/jvm-gc
最后更新:2019-10-18 15:09:08

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×