• 浅谈Java中的软引用


    个人博客

    http://www.milovetingting.cn

    浅谈Java中的软引用

    前言

    Java中有四种引用类型:强引用、软引用、弱引用、虚引用。四种引用类型分别有不同的应用场景,本文主要演示软引用的简单使用、可能遇到的问题以及对应的解决方法。

    软引用的简单使用

    软引用的特点是:如果一个对象只存在软引用,那么当内存不足时,GC就会回收这个对象。

    设置JVM的最大内存

    为了模拟内存不足,这里通过-Xmx来设置JVM的最大可分配内存。

    -Xmx100m
    

    这里是使用IntelliJ IDEA来创建项目的。在Run-Edit Configurations中打开

    config_memory.png

    先输出JVM当前的内存信息

    private static void showInitialMemoryInfo() {
            MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();
            System.out.println("最大可用内存:" + toMB(mbean.getHeapMemoryUsage().getMax()));
            for (MemoryPoolMXBean mxBean : ManagementFactory.getMemoryPoolMXBeans()) {
                System.out.println("Name:" + mxBean.getName()
                        + ",Type:" + mxBean.getType()
                        + ",Size:" + toMB(mxBean.getUsage().getMax()));
            }
        }
    

    如果是使用CMD的方式,可以通过javac -encoding utf-8 Main.java先编译,再通过java -Xmx100m Main来执行

    运行结果:

    最大可用内存:96.00 M
    Name:Code Cache,Type:Non-heap memory,Size:240.00 M
    Name:Metaspace,Type:Non-heap memory,Size:-0.00 M
    Name:Compressed Class Space,Type:Non-heap memory,Size:1024.00 M
    Name:PS Eden Space,Type:Heap memory,Size:25.00 M
    Name:PS Survivor Space,Type:Heap memory,Size:4.00 M
    Name:PS Old Gen,Type:Heap memory,Size:67.00 M
    

    可以看到,虽然我们指定了100M的内存,但是实际上可分配的内存只有96M。这是因为,内存区域按照新生代:老年代=1:2划分,老年代的大小为67M,新生代又分为Eden区+Survivor From+Survivor To,比例约为8:1:1,由于Survivor From区域和Survivor To区域的大小是相同的,而且这两个区域同时只会使用一个,因此相当于有一个Survivor的空间是无法使用到的,因此最终可使用的大小为100-4=96M

    软引用的使用

    为了演示软引用,首先创建一个类

    static class SoftObject {
            byte[] data = new byte[50 * 1024 * 1024];
        }
    

    这个类占用内存大小为50M

    private static void softReference() {
            printSplitLine();
    
            //创建软引用对象
            SoftReference<SoftObject> reference = new SoftReference<>(new SoftObject());
            System.out.println(getCurrentMemoryInfo() + ",当前软引用对象:" + reference.get());
    
            printSplitLine();
    
            //创建强引用对象
            SoftObject object = new SoftObject();
            printSplitLine();
            System.out.println(getCurrentMemoryInfo() + ",当前软引用对象:" + reference.get());
    
            //创建强引用对象
            SoftObject object2 = new SoftObject();
            printSplitLine();
            System.out.println(getCurrentMemoryInfo() + ",当前软引用对象:" + reference.get());
        }
    

    在这个方法中,先创建了一个只有软引用的对象,然后打印了该对象的地址。然后又创建了一个强引用对象,由于此时内存不足以创建新的对象,因此会回收之前创建的只有软引用的对象。然后再次创建强引用对象,由于此时已经无法分配足够的空间来创建对象,因此会抛出OOM的异常。

    为了方便看到GC的信息,这里加上了-XX:PrintGC

    输出结果

    ========================================================================
    当前最大可用内存:96.00 M,当前空闲内存:42.48 M,当前软引用对象:Main$SoftObject@5cad8086
    ========================================================================
    [GC (Allocation Failure)  54806K->52176K(98304K), 0.0015117 secs]
    [GC (Allocation Failure)  52176K->52160K(98304K), 0.0015483 secs]
    [Full GC (Allocation Failure)  52160K->52073K(98304K), 0.0096901 secs]
    [GC (Allocation Failure)  52073K->52073K(98304K), 0.0009906 secs]
    [Full GC (Allocation Failure)  52073K->836K(77824K), 0.0088653 secs]
    ========================================================================
    当前最大可用内存:96.00 M,当前空闲内存:44.18 M,当前软引用对象:null
    [GC (Allocation Failure)  53060K->52076K(98304K), 0.0007633 secs]
    [GC (Allocation Failure)  52076K->52140K(94720K), 0.0013708 secs]
    [Full GC (Allocation Failure)  52140K->52039K(94720K), 0.0218573 secs]
    [GC (Allocation Failure)  52039K->52039K(97280K), 0.0006524 secs]
    [Full GC (Allocation Failure)  52039K->52031K(97280K), 0.0042017 secs]
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    	at Main$SoftObject.<init>(Main.java:113)
    	at Main.softReference(Main.java:34)
    	at Main.main(Main.java:13)
    

    使用软引用可能存在的问题

    目前来看,一切都是正常的。看下以下的代码

    
    static class SmallSoftObject {
            byte[] data = new byte[1024];
        }
    
    private static void softReferenceOverHeadLimit() {
            int capacity = 1024 * 1024;
            HashSet<SoftReference<SmallSoftObject>> set = new HashSet<>(capacity);
            for (int i = 0; i < capacity; i++) {
                set.add(new SoftReference<>(new SmallSoftObject()));
            }
            System.out.println("End");
        }
    

    为了演示这种异常,先通过-Xmx40m,将JVM的最大内存改为40M,然后创建的对象占用内存也改为1kb。通过一个HashSet来引用SoftReference对象。运行后,输出结果如下:

    //省略部分输出
    [Full GC (Ergonomics)  32742K->32697K(36864K), 0.1614924 secs]
    [Full GC (Ergonomics)  32742K->32700K(36864K), 0.1674457 secs]
    [Full GC (Ergonomics)  32743K->32703K(36864K), 0.1609792 secs]
    [Full GC (Ergonomics)  32742K->32705K(36864K), 0.1612034 secs]
    [Full GC (Ergonomics)  32742K->32708K(36864K), 0.1645191 secs]
    [Full GC (Ergonomics)  32742K->32710K(36864K), 0.1633229 secs]
    [Full GC (Ergonomics)  32743K->32712K(36864K), 0.1632551 secs]
    Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    	at Main$SmallSoftObject.<init>(Main.java:117)
    	at Main.softReferenceOverHeadLimit(Main.java:46)
    	at Main.main(Main.java:13)
    

    可以看到,不同于上面的异常,这里是抛出的overhead limit的OOM异常。抛出这个异常是因为,默认情况下,如果GC花费的时间超过98%,并且GC回收的内存少于2%,JVM就会抛出这个异常。

    解决方法

    通过引用队列,在触发GC时,手动移除HashSet中的SoftReference对象。

    private static void softReferenceOverHeadLimitResolve() {
            int capacity = 1024 * 1024;
            HashSet<SoftReference<SmallSoftObject>> set = new HashSet<>(capacity);
            ReferenceQueue<SmallSoftObject> referenceQueue = new ReferenceQueue<>();
            for (int i = 0; i < capacity; i++) {
                set.add(new SoftReference<>(new SmallSoftObject(), referenceQueue));
                removeObject(set, referenceQueue);
            }
            System.out.println("End");
        }
    
    private static void removeObject(HashSet<SoftReference<SmallSoftObject>> set, ReferenceQueue<SmallSoftObject> referenceQueue) {
            Reference<? extends SmallSoftObject> poll = referenceQueue.poll();
            while (poll != null) {
                set.remove(poll);
                poll = referenceQueue.poll();
            }
        }
    

    执行结果

    //省略部分输出
    [Full GC (Ergonomics)  32698K->32698K(36864K), 0.0309117 secs]
    [Full GC (Ergonomics)  32716K->32716K(36864K), 0.0309336 secs]
    [Full GC (Ergonomics)  32734K->32734K(36864K), 0.0323875 secs]
    [Full GC (Ergonomics)  32751K->32751K(36864K), 0.0311011 secs]
    [Full GC (Ergonomics)  32767K->32767K(36864K), 0.0309406 secs]
    [Full GC (Allocation Failure)  32767K->6943K(35840K), 0.0409029 secs]
    [GC (Allocation Failure)  12066K->12042K(35840K), 0.0036551 secs]
    [GC (Allocation Failure)  17162K->17298K(35840K), 0.0038763 secs]
    End
    

    完整代码

    import java.lang.management.ManagementFactory;
    import java.lang.management.MemoryMXBean;
    import java.lang.management.MemoryPoolMXBean;
    import java.lang.ref.Reference;
    import java.lang.ref.ReferenceQueue;
    import java.lang.ref.SoftReference;
    import java.util.HashSet;
    
    public class Main {
    
        public static void main(String[] args) {
            showInitialMemoryInfo();
            softReferenceOverHeadLimitResolve();
        }
    
        /**
         * 演示软引用
         */
        private static void softReference() {
            printSplitLine();
    
            //创建软引用对象
            SoftReference<SoftObject> reference = new SoftReference<>(new SoftObject());
            System.out.println(getCurrentMemoryInfo() + ",当前软引用对象:" + reference.get());
    
            printSplitLine();
    
            //创建强引用对象
            SoftObject object = new SoftObject();
            printSplitLine();
            System.out.println(getCurrentMemoryInfo() + ",当前软引用对象:" + reference.get());
    
            //创建强引用对象
            SoftObject object2 = new SoftObject();
            printSplitLine();
            System.out.println(getCurrentMemoryInfo() + ",当前软引用对象:" + reference.get());
        }
    
        /**
         * 演示软引用溢出
         */
        private static void softReferenceOverHeadLimit() {
            int capacity = 1024 * 1024;
            HashSet<SoftReference<SmallSoftObject>> set = new HashSet<>(capacity);
            for (int i = 0; i < capacity; i++) {
                set.add(new SoftReference<>(new SmallSoftObject()));
            }
            System.out.println("End");
        }
    
        /**
         * 演示软引用溢出解决方案
         */
        private static void softReferenceOverHeadLimitResolve() {
            int capacity = 1024 * 1024;
            HashSet<SoftReference<SmallSoftObject>> set = new HashSet<>(capacity);
            ReferenceQueue<SmallSoftObject> referenceQueue = new ReferenceQueue<>();
            for (int i = 0; i < capacity; i++) {
                set.add(new SoftReference<>(new SmallSoftObject(), referenceQueue));
                removeObject(set, referenceQueue);
            }
            System.out.println("End");
        }
    
        private static void removeObject(HashSet<SoftReference<SmallSoftObject>> set, ReferenceQueue<SmallSoftObject> referenceQueue) {
            Reference<? extends SmallSoftObject> poll = referenceQueue.poll();
            while (poll != null) {
                set.remove(poll);
                poll = referenceQueue.poll();
            }
        }
    
        /**
         * 显示初始的内存信息
         */
        private static void showInitialMemoryInfo() {
            MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();
            System.out.println("最大可用内存:" + toMB(mbean.getHeapMemoryUsage().getMax()));
            for (MemoryPoolMXBean mxBean : ManagementFactory.getMemoryPoolMXBeans()) {
                System.out.println("Name:" + mxBean.getName()
                        + ",Type:" + mxBean.getType()
                        + ",Size:" + toMB(mxBean.getUsage().getMax()));
            }
        }
    
        /**
         * 获取当前内存信息
         *
         * @return
         */
        private static String getCurrentMemoryInfo() {
            return "当前最大可用内存:" + toMB(Runtime.getRuntime().maxMemory()) + ",当前空闲内存:" + toMB(Runtime.getRuntime().freeMemory());
        }
    
        /**
         * 将字节转换成MB的格式
         *
         * @param b
         * @return
         */
        private static String toMB(Long b) {
            return String.format("%.2f M", (double) b / (1024 * 1024));
        }
    
        /**
         * 打印分隔行
         */
        private static void printSplitLine() {
            System.out.println("========================================================================");
        }
    
        static class SoftObject {
            byte[] data = new byte[50 * 1024 * 1024];
        }
    
        static class SmallSoftObject {
            byte[] data = new byte[1024];
        }
    }
    
    

    结束语

    软引用在开发中可能会常用到,了解它的基本用法及可能出现的异常都是有必要的,因此特记录于此。如有表述有误的地方,欢迎各位大佬留言指正,感谢!

    参考

    Android工程师进阶34讲-第02讲:GC回收机制与分代回收策略

  • 相关阅读:
    新增数据盘
    FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。
    将tomcat的protocol改为APR模式,以提高性能
    POJ 2195 Going Home (最小费用最大流)
    HDU 2732 Leapin' Lizards (最大流)
    POJ 2516 Minimum Cost (最小费用最大流)
    POJ 1087 A Plug for UNIX (最大流)
    POJ 3281 Dining (最大流)
    HDU 3605 Escape (最大流)
    POJ 1149 PIGS (最大流)
  • 原文地址:https://www.cnblogs.com/milovetingting/p/13796001.html
Copyright © 2020-2023  润新知