个人博客
http://www.milovetingting.cn
浅谈Java中的软引用 前言 Java中有四种引用类型:强引用、软引用、弱引用、虚引用。四种引用类型分别有不同的应用场景,本文主要演示软引用的简单使用、可能遇到的问题以及对应的解决方法。
软引用的简单使用 软引用的特点是:如果一个对象只存在软引用,那么当内存不足时,GC就会回收这个对象。
设置JVM的最大内存 为了模拟内存不足,这里通过-Xmx来设置JVM的最大可分配内存。
这里是使用IntelliJ IDEA来创建项目的。在Run-Edit Configurations中打开
先输出JVM当前的内存信息
1 2 3 4 5 6 7 8 9 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来执行
运行结果:
1 2 3 4 5 6 7 最大可用内存: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
软引用的使用 为了演示软引用,首先创建一个类
1 2 3 static class SoftObject { byte [] data = new byte [50 * 1024 * 1024 ]; }
这个类占用内存大小为50M
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
输出结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ======================================================================== 当前最大可用内存: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)
使用软引用可能存在的问题 目前来看,一切都是正常的。看下以下的代码
1 2 3 4 5 6 7 8 9 10 11 12 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对象。运行后,输出结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 //省略部分输出 [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对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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(); } }
执行结果
1 2 3 4 5 6 7 8 9 10 //省略部分输出 [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
完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 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())); } } private static String getCurrentMemoryInfo () { return "当前最大可用内存:" + toMB(Runtime.getRuntime().maxMemory()) + ",当前空闲内存:" + toMB(Runtime.getRuntime().freeMemory()); } 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回收机制与分代回收策略