您当前的位置: 首页 >  Java

恐龙弟旺仔

暂无认证

  • 0浏览

    0关注

    282博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Java对象四种引用方式

恐龙弟旺仔 发布时间:2021-09-04 20:09:19 ,浏览量:0

前言:

    我们都知道在java世界中,对象的引用有四种方式。当然,工作中,我们一般都只使用一种,也就是强引用。因为我们一般设置运行时的内存足够大,只要及时的释放对象,GC自动回收不再使用的对象内存,一般情况下,是不会内存溢出的。

    一般我们是怎么释放对象的呢?就是直接将对象设置为null。那么我们先看下如下这个示例:

// -Xmx200m -Xms200m -XX:+PrintGCDetails
@Test
public void test() {
    byte[] key = new byte[100 * 1024 * 1024]; // 100M大小的key
    Object val = new Object();

    Map map = new HashMap();
    // 将key和value设置到map中
    map.put(key, val);

    // 主动触发第一次GC
    System.gc();
    System.out.println("---------------");

    // 好玩的地方在这里,我们主动将key设置为null,后续再主动触发一次GC
    key = null;
    System.gc();

    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

执行以上代码时,我们需要先设置下JVM参数,笔者设置如下

-Xmx200m -Xms200m -XX:+PrintGCDetails

不看执行结果之前,我们先来猜一下两次GC的结果,第一次GC的话,被设置的100M的key肯定是不会被回收的,因为强引用还在;

那么第二次呢?我们主动将key设置为null之后呢,我们会觉得GC会将这100M的key内存回收掉。

来看下实际结果:

[GC (System.gc()) [PSYoungGen: 11382K->1704K(59904K)] 113782K->104112K(196608K), 0.0022167 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 1704K->0K(59904K)] [ParOldGen: 102408K->103975K(136704K)] 104112K->103975K(196608K), [Metaspace: 5245K->5245K(1056768K)], 0.0117054 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
---------------
[GC (System.gc()) [PSYoungGen: 1034K->32K(59904K)] 105009K->104007K(196608K), 0.0012404 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 32K->0K(59904K)] [ParOldGen: 103975K->103525K(136704K)] 104007K->103525K(196608K), [Metaspace: 5248K->5248K(1056768K)], 0.0102300 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
Heap
 PSYoungGen      total 59904K, used 4137K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000)
  eden space 51712K, 8% used [0x00000000fbd80000,0x00000000fc18a420,0x00000000ff000000)
  from space 8192K, 0% used [0x00000000ff800000,0x00000000ff800000,0x0000000100000000)
  to   space 8192K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ff800000)
 ParOldGen       total 136704K, used 103525K [0x00000000f3800000, 0x00000000fbd80000, 0x00000000fbd80000)
  object space 136704K, 75% used [0x00000000f3800000,0x00000000f9d197d8,0x00000000fbd80000)
 Metaspace       used 5511K, capacity 5696K, committed 5888K, reserved 1056768K
  class space    used 632K, capacity 659K, committed 768K, reserved 1048576K

ParOldGen依旧是75%的占用,那100M的key没有被回收掉。

为什么呢?因为HashMap对着100M对象的强引用依然在。

注意:笔者之前会陷入一个误区,本例中key只是对这100M内存对象的一个强引用而已,后续将key=null,只是将key不再指向这100M对象而已,但是这个大对象还是存在的。

那么该如何回收这100M内存呢?很简单,将Map对其的引用也删除,如下所示:

// 原来的方式
key = null;

// 现在的方式
map.remove(key);
key = null;

这样,结果就很明朗了,GC结果如下:

[GC (System.gc()) [PSYoungGen: 11382K->1688K(59904K)] 113782K->104096K(196608K), 0.0035644 secs] [Times: user=0.05 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 1688K->0K(59904K)] [ParOldGen: 102408K->103975K(136704K)] 104096K->103975K(196608K), [Metaspace: 5249K->5249K(1056768K)], 0.0112903 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
---------------
[GC (System.gc()) [PSYoungGen: 1034K->32K(59904K)] 105009K->104007K(196608K), 0.0012414 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 32K->0K(59904K)] [ParOldGen: 103975K->1574K(136704K)] 104007K->1574K(196608K), [Metaspace: 5250K->5250K(1056768K)], 0.0082791 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 59904K, used 3102K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000)
  eden space 51712K, 6% used [0x00000000fbd80000,0x00000000fc087b28,0x00000000ff000000)
  from space 8192K, 0% used [0x00000000ff800000,0x00000000ff800000,0x0000000100000000)
  to   space 8192K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ff800000)
 ParOldGen       total 136704K, used 1574K [0x00000000f3800000, 0x00000000fbd80000, 0x00000000fbd80000)
  object space 136704K, 1% used [0x00000000f3800000,0x00000000f3989ab8,0x00000000fbd80000)
 Metaspace       used 5511K, capacity 5696K, committed 5888K, reserved 1056768K
  class space    used 632K, capacity 659K, committed 768K, reserved 1048576K

ParOldGen 只有1% used,彻底删除了100M大对象的内存

从这个示例中,我们看到了GC回收的困难之处,只有还有一个强引用没被删除,对象就不会被回收。

实际上除了强引用,还有其他几种对象引用方式,在不同的引用方式下,GC对其处理方式也是不同的。

1.强引用

    强引用,是我们使用最多的一个引用方式,方式很简单,就是将一个引用直接执行一个对象。

byte[] key = new byte[100 * 1024 * 1024];
Object val = new Object();

这里的key和val都是对byte[]和Object对象的强引用。

如果一个对象具有强引用,那么在内存回收时,GC是绝不会回收这块内存的,当内存不足时,其会抛出OOM的异常,也不会回收这块内存。

2.软引用

    相对强引用而言,软引用主要用来引用一些有用但不是必须的对象。软引用所关联的对象,只有当内存不足时,才会考虑将这些被软引用所引用的对象回收掉。

2.1 软引用示例
//100M的缓存数据
byte[] cacheData = new byte[100 * 1024 * 1024];
//将缓存数据用软引用持有
SoftReference cacheRef = new SoftReference(cacheData);

这里的100M的数组对象,存在两个引用,一个是cacheData的强引用,另一个是cacheRef的软引用。

当然,我们也可以直接创建一个唯一的软引用对象,如下所示:

SoftReference cacheRef = new SoftReference(new byte[100 * 1024 * 1024]);
2.2 软引用GC示例
// -Xmx200m -Xms200m
@Test
public void test2() throws Exception {
    //100M的缓存数据
    byte[] cacheData = new byte[100 * 1024 * 1024];
    //将缓存数据用软引用持有
    SoftReference cacheRef = new SoftReference(cacheData);
    //将缓存数据的强引用去除
    cacheData = null;
    System.out.println("第一次GC前" + cacheData);
    System.out.println("第一次GC前" + cacheRef.get());
    //进行一次GC后查看对象的回收情况
    System.gc();
    //等待GC
    Thread.sleep(500);
    System.out.println("第一次GC后" + cacheData);
    System.out.println("第一次GC后" + cacheRef.get());

    //在分配一个120M的对象,看看缓存对象的回收情况
    byte[] newData = new byte[120 * 1024 * 1024];
    System.out.println("分配后" + cacheData);
    System.out.println("分配后" + cacheRef.get());
}

// res:
第一次GC前null
第一次GC前[B@6996db8
第一次GC后null
第一次GC后[B@6996db8
分配后null
分配后null

进行这个测试的先决条件是,我们需要对最大最小内存先进行设置,设置为200M

我们直接对结果进行分析:

1)第一次GC后,byte[]还存在一个cacheRef的弱引用,此时200M的内存只占用了100M+,内存还是够用的,所以第一次GC时,byte[]仍旧在,还是可以通过cacheRef.get()获取到数组

2)在重新分配一个120M的byte[]时,此时200M的内存已经不够用了,所以,GC会主动触发一次,会将cacheRef这个弱引用所关联的byte[]内存清除掉,此时再通过cacheRef.get()就获取不到数组信息了

2.2.1 重设内存条件,再次弱引用测试

    刚才设置是200M的堆内存,现在我们设置成500M的堆内存,重新测试该代码,会发现结果有所不同

第一次GC前null
第一次GC前[B@6996db8
第一次GC后null
第一次GC后[B@6996db8
分配后null
分配后[B@6996db8

结果的不同之处在于:在分配120M的数组时,cacheRef所关联的100M的byte[]数据依旧没有被清除

总结:该结果直接认证了我们对弱引用的介绍:软引用所关联的对象,只有当内存不足时,才会考虑将这些被软引用所引用的对象回收掉。

2.3 软引用适用场景

    针对软引用这种内存充足时就不清除,只有当内存不足时才进行回收的对象引用方式,就特别适合做数据的缓存。

3.弱引用

    相对于软引用而言,它对对象的引用强度更弱。在只有弱引用来引用的对象,在GC时,无论内存是否够用,都会将该对象回收。

3.1 弱引用示例
//100M的缓存数据
byte[] cacheData = new byte[100 * 1024 * 1024];
//将缓存数据用弱引用持有
WeakReference cacheRef = new WeakReference(cacheData);

这里的100M的数组对象,存在两个引用,一个是cacheData的强引用,另一个是cacheRef的弱引用。

当然,我们也可以直接创建一个唯一的弱引用对象,如下所示:

WeakReference cacheRef = new WeakReference(new byte[100 * 1024 * 1024]);
3.2 弱引用GC示例
// -Xmx200m -Xms200m
@Test
public void test3() throws Exception {
    //100M的缓存数据
    byte[] cacheData = new byte[100 * 1024 * 1024];
    //将缓存数据用弱引用持有
    WeakReference cacheRef = new WeakReference(cacheData);
    System.out.println("第一次GC前" + cacheData);
    System.out.println("第一次GC前" + cacheRef.get());

    //将缓存数据的强引用去除
    cacheData = null;
    System.gc();
    //等待GC
    Thread.sleep(500);
    System.out.println("第一次GC后" + cacheData);
    System.out.println("第一次GC后" + cacheRef.get());
}

// res
第一次GC前[B@6996db8
第一次GC前[B@6996db8
第一次GC后null
第一次GC后null

结果很明确,无论内存够不够,只要发生了GC,弱引用所引用的对象都会被回收

3.3 弱引用适用场景

    弱引用这种有什么适用场景呢?回到我们在前言中的那个示例,我们来对其改造下

// -Xmx200m -Xms200m
@Test
public void test() {
    byte[] key = new byte[100 * 1024 * 1024];
    Object val = new Object();

    Map map = new HashMap();
    // 在这里改造下,使用WeakReference作为map的key
    WeakReference weakRef = new WeakReference(key);
    map.put(weakRef, val);
    System.gc();
    System.out.println("---------------");

    key = null;

    System.gc();

    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
// res
[GC (System.gc()) [PSYoungGen: 11382K->1704K(59904K)] 113782K->104112K(196608K), 0.0052081 secs] [Times: user=0.00 sys=0.03, real=0.02 secs] 
[Full GC (System.gc()) [PSYoungGen: 1704K->0K(59904K)] [ParOldGen: 102408K->103975K(136704K)] 104112K->103975K(196608K), [Metaspace: 5245K->5245K(1056768K)], 0.0113374 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
---------------
[GC (System.gc()) [PSYoungGen: 1034K->32K(59904K)] 105009K->104007K(196608K), 0.0016759 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [PSYoungGen: 32K->0K(59904K)] [ParOldGen: 103975K->1541K(136704K)] 104007K->1541K(196608K), [Metaspace: 5246K->5246K(1056768K)], 0.0079988 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 59904K, used 3102K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000)
  eden space 51712K, 6% used [0x00000000fbd80000,0x00000000fc087b28,0x00000000ff000000)
  from space 8192K, 0% used [0x00000000ff800000,0x00000000ff800000,0x0000000100000000)
  to   space 8192K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ff800000)
 ParOldGen       total 136704K, used 1541K [0x00000000f3800000, 0x00000000fbd80000, 0x00000000fbd80000)
  object space 136704K, 1% used [0x00000000f3800000,0x00000000f3981448,0x00000000fbd80000)
 Metaspace       used 5512K, capacity 5696K, committed 5888K, reserved 1056768K
  class space    used 632K, capacity 659K, committed 768K, reserved 1048576K

相对于前言中的测试结果,这里我们看到,ParOldGen只有1% used,那100M的byte[]被清除掉了。

总结:针对Map的这种对key的依赖,如果我们想在key的强依赖被删除时,就释放其所对应的内存时,那么我们就可以使用WeakReference来包装这个byte[],这样当发生GC时,这个byte[]就被内存回收掉。

实际这个也就是WeakHashMap做的事情,具体源码笔者不再分析。

除了这个还有哪些呢?可以参考笔者另一篇博客:关于ThreadLocal的。

4.虚引用

    虚引用是最后一种引用方式,也是最弱的一种引用方式。

    我们无法通过虚引用来获取其所引用的对象信息。如果一个对象仅有虚引用指向它,那么它就和没有任何引用一样,在任何时候它都有可能被GC回收掉。

4.1 虚引用作用

    既然这么弱,那么虚引用还有什么作用呢?

    它的主要作用就是:当GC准备回收一个对象时,如果发现它还有虚引用,就会在回收对象内存前,把这个虚引用加入到与之关联的引用队列中。程序通过判断引用队列是否已经加入了虚引用,来了解与之关联的对象是否要被垃圾回收。

4.2 虚引用使用示例
static class TestClass {
    private String name;

    public TestClass(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "TestClass - " + name;
    }
}

// -Xmx10m -Xms10m -XX:+PrintGCDetails
@Test
public void test() {

    final List TEST_DATA = new LinkedList();
    final ReferenceQueue QUEUE = new ReferenceQueue();

    TestClass obj = new TestClass("Test");
    final PhantomReference phantomReference = new PhantomReference(obj, QUEUE);

    // 该线程不断读取这个虚引用,并不断往列表里插入数据,以促使系统早点进行GC
    new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                TEST_DATA.add(new byte[1024 * 1024]);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
                System.out.println(phantomReference.get());
            }
        }
    }).start();

    // 这个线程不断读取引用队列,当弱引用指向的对象被回收时,该引用就会被加入到引用队列中
    new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                Reference            
关注
打赏
1655041699
查看更多评论
1.5438s