【JVM】--对象是如何被定义为垃圾及回收算法

GC

  • Garbage
    垃圾,指内存中已经不再被使用的空间就是垃圾
  • Garbage Collector
    垃圾收集器

要进行垃圾收集,如何判断一个对象是否可以被回收?

引用计数法

Java中引用和对象是有关联的,如果要操作对象则必须用引用进行。显然一个简单的办法就是通过引用计数来判断一个对象是否可以回收。

简单来说就是给对象添加一个引用计数器,

  • 每当有一个地方引用它,计数器值加一;
  • 每当有一个引用失效时,计数器值减一。

任何时刻计数器值为零的对象就是不能被使用的,则这个对象就是可回收对象。
在这里插入图片描述

public class ReferenceCountingGC {
    public Object instance = null;

    public static final int _1MB = 1024 * 1024;
    private byte[] bigSize = new byte[2 * _1MB];

    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();

        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;
        
		// 手动GC
        System.gc();
    }
}

运行结果:

[GC (System.gc()) [PSYoungGen: 7373K->728K(37888K)] 7373K->728K(123904K), 0.0644247 secs] [Times: user=0.05 sys=0.00, real=0.07 secs] 
[Full GC (System.gc()) [PSYoungGen: 728K->0K(37888K)] [ParOldGen: 0K->679K(86016K)] 728K->679K(123904K), [Metaspace: 3465K->3465K(1056768K)], 0.0903428 secs] [Times: user=0.08 sys=0.00, real=0.09 secs] 
Heap
 PSYoungGen      total 37888K, used 328K [0x00000000d6400000, 0x00000000d8e00000, 0x0000000100000000)
  eden space 32768K, 1% used [0x00000000d6400000,0x00000000d6452030,0x00000000d8400000)
  from space 5120K, 0% used [0x00000000d8400000,0x00000000d8400000,0x00000000d8900000)
  to   space 5120K, 0% used [0x00000000d8900000,0x00000000d8900000,0x00000000d8e00000)
 ParOldGen       total 86016K, used 679K [0x0000000082c00000, 0x0000000088000000, 0x00000000d6400000)
  object space 86016K, 0% used [0x0000000082c00000,0x0000000082ca9f48,0x0000000088000000)
 Metaspace       used 3472K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

那么为什么主流的Java虚拟机中没有使用这种算法呢?

  • 每次对象赋值时都要维护引用计数器,计数器本身也有一定的消耗
  • 最主要原因是它很难解决对象之间相互循环引用的问题。

可达性分析

为了解决上面引用计数法的循环引用问题,Java使用了可达性分析算法来判断对象是否存活。

可达性分析就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象不可达时,则说明这个对象是不可能再被使用的。
在这里插入图片描述

  • 仍然存活
    Object1、Object2、Object3、Object4
  • 可回收对象
    Object5、Object6、Object7,虽然三者互相关联,但是到GC Roots不可达。

GC Root

有哪些可以当做GC Root对象呢?

  • 虚拟机栈(栈帧中本地变量表)中引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象,比如字符串常量池里的引用
  • 本地方法栈中JNI(Native方法)引用的对象
  • 所有被同步锁(synchronized)持有的对象

引用

无论是通过引用计数法还是可达性分析,判断对象是否存活都和“引用”离不开关系。

JDK1.2以前 ,Java 里的引用是很传统的定义:如果reference类型的数据中存储的数值代表的是另外一块内存的其实地址,就称该reference数据时代表某块内存的、能够个对象的引用

这种定义下对象只有 “被引用”、“未被引用” 两种状态,对于描述一些“鸡肋型”对象显得无能为力。比如我们希望描述一类对象:当内存空间足够时能保留在内存中,当内存空间在进行垃圾手机后仍非常紧张,那就抛弃这些对象。

JDK1.2 以后,Java对引用进行扩充,将引用分为:强引用、软引用、弱引用、虚引用,这四种引用强度递减:

  • 强引用
    • 就是传统的“引用”定义,是指在程序代码中普遍存在的引用赋值,即类似Object obj = new Object();
    • 无论任何情况下,只要强引用关系还在,垃圾收集就永远不会回收掉被引用的对象,会造成Java内存泄漏
public class ReferenceDemo {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        System.out.println(o1);
        System.out.println(o2);

        o1 = null;
        System.gc();

        System.out.println(o1);
        System.out.println(o2);
    }
}

运行结果:
最后打印不是null,说明并没被回收

java.lang.Object@14ae5a5
java.lang.Object@7f31245a
null
java.lang.Object@7f31245a

Process finished with exit code 0
  • 软引用
    • 描述一些还有用,但非必须的对象。
    • 只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够内存,才会抛出内存溢出异常。
    • SoftReference实现软引用
public class SoftReferenceDemo {
    public static void softRef_Memory_Enough( ) {
        Object o1 = new Object();
        SoftReference<Object> softReference = new SoftReference<Object>(o1);

        System.out.println(o1);
        System.out.println(softReference.get());

        o1 = null;
        System.gc();

        System.out.println(o1);
        System.out.println(softReference.get());
    }

    /**
     * JVM配置,故意产生大对象并配置小内存,产生OOM
     * -Xms5m -Xmx5m -XX:+PrintGCDetails
     */
    public static void softRef_Memory_NotEnough( ) {
        Object o1 = new Object();
        SoftReference<Object> softReference = new SoftReference<Object>(o1);

        System.out.println(o1);
        System.out.println(softReference.get());

        o1 = null;
        try {
            byte[] bytes = new byte[20 * 1024 * 1024];
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println(o1);
            System.out.println(softReference.get());
        }
    }

    public static void main(String[] args) {
//        softRef_Memory_Enough();
        softRef_Memory_NotEnough();
    }

}

运行结果:
最后打印为null,说明软引用已被回收

[GC (Allocation Failure) [PSYoungGen: 1024K->488K(1536K)] 1024K->560K(5632K), 0.0019976 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
java.lang.Object@14ae5a5
java.lang.Object@14ae5a5
[GC (Allocation Failure) [PSYoungGen: 1488K->488K(1536K)] 1560K->704K(5632K), 0.0016943 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 488K->488K(1536K)] 704K->728K(5632K), 0.0026086 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 488K->0K(1536K)] [ParOldGen: 240K->679K(4096K)] 728K->679K(5632K), [Metaspace: 3468K->3468K(1056768K)], 0.0189544 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 679K->679K(5632K), 0.0028049 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 679K->661K(4096K)] 679K->661K(5632K), [Metaspace: 3468K->3468K(1056768K)], 0.0109221 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
null
null
Heap
 PSYoungGen      total 1536K, used 67K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 6% used [0x00000000ffe00000,0x00000000ffe10d40,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 4096K, used 661K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 16% used [0x00000000ffa00000,0x00000000ffaa5440,0x00000000ffe00000)
 Metaspace       used 3499K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 384K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.fjw._10_JVM.SoftReferenceDemo.softRef_Memory_NotEnough(SoftReferenceDemo.java:39)
	at com.fjw._10_JVM.SoftReferenceDemo.main(SoftReferenceDemo.java:50)

Process finished with exit code 1

  • 弱引用
    • 也是描述那些非必须对象,但是它的强度比软引用更弱一些
    • 当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
    • WeakReference实现弱引用
public class WeakReferenceDemo {
    public static void main(String[] args) {
        Object o1 = new Object();
        WeakReference<Object> weakReference = new WeakReference<Object>(o1);

        System.out.println(o1);
        System.out.println(weakReference.get());

        o1 = null;
        System.gc();

        System.out.println(o1);
        System.out.println(weakReference.get());
    }

}

运行结果:
最后打印为null,内存足够也会被回收。

java.lang.Object@14ae5a5
java.lang.Object@14ae5a5
null
null

Process finished with exit code 0
  • 软引用和弱引用使用场景
    有一个应用需要读取大量的本地图片:
    • 如果每次读取都从硬盘,则会严重影响性能
    • 如果一次性全部加载到内存中,又可能造成内存溢出

这是可以使用软引用或弱引用解决问题

public class WeakHashMapDemo {
    public static void main(String[] args) {
        myHashMap();
        System.out.println("=====================");
        myWeakHashMap();


    }

    private static void myWeakHashMap() {
        WeakHashMap<Integer, String> map = new WeakHashMap<>();
        Integer key = new Integer(1);
        String value = "HashMap";

        map.put(key,value);
        System.out.println(map);

        key = null;
        System.out.println(map);

        System.gc();
        System.out.println(map);
    }

    private static void myHashMap() {
        HashMap<Integer, String> map = new HashMap<>();
        Integer key = new Integer(1);
        String value = "HashMap";

        map.put(key,value);
        System.out.println(map);

        key = null;
        System.out.println(map);

        System.gc();
        System.out.println(map);
    }
}

运行结果:

{1=HashMap}
{1=HashMap}
{1=HashMap}
=====================
{1=WeakHashMap}
{1=WeakHashMap}
{}

Process finished with exit code 0
  • 虚引用
    • 也称“幽灵引用”或“幻影引用”,是最弱的一种引用关系
    • 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得对象实例,需要与引用队列(ReferenceQueue)联合使用
    • 设置虚引用的唯一目的就是为了能在对象被回收时收到一个系统通知
    • PhantomReference实现虚引用
public class PhantomReferenceDemo {
    public static void main(String[] args) {
        Object o = new Object();
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        PhantomReference<Object> phantomReference = new PhantomReference<>(o, referenceQueue);

        System.out.println(o);
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());
        System.out.println("===================");

        o = null;
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e){ e.printStackTrace(); }

        System.out.println(o);
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());
    }
}

运行结果:
最后打印仍有值。
创建引用的时候可以指定关联对象,当GC释放对象内存时,会将引用(包括前面的软引用和弱引用)加入到引用队列中,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象内存被回收之前采取必要的行动,相当于是一种通知机制。

java.lang.Object@14ae5a5
null
null
===================
null
null
java.lang.ref.PhantomReference@7f31245a

垃圾收集算法

标记-清除算法

  • 最早出现也是最基础的垃圾收集算法
  • 算法分为:标记 和 清除 两个阶段
    • 首先标记所有需要回收的对象,标记完成后,统一回收所有被标记的对象,也可反过来
    • 标记过程就是对象是否属于垃圾的判定过程

缺点

  • 执行效率不稳定
    如果Java堆中包含大量对象,而且其中大部分需要被回收,这时必须进行大量标记和清除动作,导致标记和清除两过程执行效率随对象数量增长而降低
  • 内存空间碎片化
    标记、清除后会产生大量不连续的内存碎片,空间碎片太多可能导致以后程序运行过程中需要分配较大对象时无法找到足够连续的内存而不得不提前触发另一次垃圾收集动作

标记-复制算法

  • 为了解决标记-清除算法面对大量可回收对象时执行效率低的问题
  • 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存用完了,就将还活着的对象复制到另外一块,然后把已使用过的内存空间一次清理掉
  • 将可用内存缩小一半,空间浪费严重
  • 但是大多数对象(98%)都是“朝生夕死”,活不过第一轮收集
    • 该算法应用于新生代,按容量8:1:1划分为Eden、from、to
    • 只在Eden和from区分配对象,内存满后进行一轮收集,将活着的对象复制到to区

标记-整理算法

标记-复制算法在对象存活率较高时要进行很多复制操作,效率将会降低。更关键的是回有50%的空间浪费,因此在老年代不能使用该算法。

针对老年代对象的存亡特征,提出标记-整理算法,标记过程仍与标记-清除一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界外的内存。

比较

标记-清除标记-整理的本质差异在于前者是一种非移动式回收算法,后者是移动式的,即关键点在于是否移动:

  • 如果移动存活对象,对于老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极繁重的操作,且移动过程中需全程暂停用户应用程序才能进行
  • 如果不移动,像那样标记-清除会导致内存碎片化严重

从上面看来是否移动都存在弊端,于是产生了“和稀泥式”解决方案:让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间碎片化程度严重到无法分配对象时,再采用标记-整理算法收集一次即可。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值