为了使垃圾回收(GC)回收程序不再使用的对象,对象的逻辑生存期(应用程序将使用该对象的时间)和对该对象的未完成引用的实际生存期必须相同。 大多数时候,良好的软件工程技术可确保这种情况自动发生,而无需我们在对象生存期问题上花费大量精力。 但是有时,我们会创建一个引用,该引用将对象保存在内存中的时间比我们预期的要长得多,这种情况称为意外对象保留 。
全局地图的内存泄漏
意外对象保留的最常见来源是使用Map
将元数据与临时对象相关联。 假设您有一个对象,其生存期是中间的-比分配它的方法调用的生存期长,但是比应用程序的生存期短-例如来自客户端的套接字连接。 您想将某些元数据与此套接字相关联,例如进行连接的用户的身份。 在创建Socket
时您不知道此信息,并且由于无法控制Socket
类或其实例化,因此无法向Socket
对象添加数据。 在这种情况下,典型的方法是将此类信息存储在全局Map
,如清单1的SocketManager
类所示:
清单1.使用全局Map将元数据与对象相关联
public class SocketManager {
private Map<Socket,User> m = new HashMap<Socket,User>();
public void setUser(Socket s, User u) {
m.put(s, u);
}
public User getUser(Socket s) {
return m.get(s);
}
public void removeUser(Socket s) {
m.remove(s);
}
}
SocketManager socketManager;
...
socketManager.setUser(socket, user);
这种方法的问题在于,元数据的生存期需要与套接字的生存期联系在一起,但是除非您确切地知道程序何时不再需要该套接字,并记得从Map
删除相应的映射,否则Socket
和User
对象将在请求得到服务且套接字已关闭后很长时间内永久保留在Map
。 即使应用程序不再使用它们中的任何一个,这也可以防止Socket
和User
对象被垃圾回收。 如果不进行检查,那么如果程序运行足够长时间,很容易导致程序内存不足。 在除最琐碎的情况以外的所有情况下,用于标识何时程序不再使用Socket
的技术类似于手动内存管理所需的烦人且易出错的技术。
识别内存泄漏
程序出现内存泄漏的第一个迹象通常是由于频繁的垃圾回收,它引发了OutOfMemoryError
或开始表现不佳。 幸运的是,垃圾收集器愿意共享许多可用于诊断内存泄漏的信息。 如果使用-verbose:gc
或-Xloggc
选项调用JVM,则每次GC运行时,诊断消息都会打印在控制台或日志文件中,包括花费了多长时间,当前堆使用情况以及使用了多少时间。内存已恢复。 记录GC的使用不是侵入性的,因此在您需要分析内存问题或调整垃圾收集器的情况下,默认情况下在生产中启用GC记录是合理的。
工具可以获取GC日志输出并以图形方式显示; 一个这样的工具是免费的JTune(参见相关主题 )。 通过查看GC之后的堆大小图,可以看到程序的内存使用趋势。 对于大多数程序,可以将内存使用情况分为两个部分: 基准使用情况和当前负载使用情况。 对于服务器应用程序,基准使用率是应用程序在不承受任何负载但准备接受请求时使用的使用率; 当前的负载用法是处理请求过程中使用的负载,但在请求处理完成时释放。 只要负载大致恒定,应用程序通常就会相当快地达到内存使用的稳定状态。 即使应用程序完成了初始化并且其负载没有增加,如果内存使用量继续呈上升趋势,则该程序可能会保留在处理先前请求的过程中生成的对象。
清单2显示了一个存在内存泄漏的程序。 MapLeaker
处理线程池中的任务,并在Map
记录每个任务的状态。 不幸的是,它永远不会在任务完成时删除该条目,因此状态条目和任务对象(以及它们的内部状态)将永远累积。
清单2.带有基于Map的内存泄漏的程序
public class MapLeaker {
public ExecutorService exec = Executors.newFixedThreadPool(5);
public Map<Task, TaskStatus> taskStatus
= Collections.synchronizedMap(new HashMap<Task, TaskStatus>());
private Random random = new Random();
private enum TaskStatus { NOT_STARTED, STARTED, FINISHED };
private class Task implements Runnable {
private int[] numbers = new int[random.nextInt(200)];
public void run() {
int[] temp = new int[random.nextInt(10000)];
taskStatus.put(this, TaskStatus.STARTED);
doSomeWork();
taskStatus.put(this, TaskStatus.FINISHED);
}
}
public Task newTask() {
Task t = new Task();
taskStatus.put(t, TaskStatus.NOT_STARTED);
exec.execute(t);
return t;
}
}
图1显示了MapLeaker
在GC MapLeaker
随着时间推移的应用程序堆大小的图表。 向上倾斜的趋势表明存在内存泄漏。 (在实际应用中,斜率永远不会如此剧烈,但是如果您收集足够长时间的GC数据,通常斜率就会变得明显。)
图1.持久的内存使用趋势
一旦确定存在内存泄漏,下一步就是找出导致问题的对象类型。 任何内存分析器都可以生成按对象类细分的堆快照。 有一些出色的商业堆分析工具,但您不必花费任何金钱来查找内存泄漏-内置的hprof
工具也可以解决问题。 要使用hprof
并指示它跟踪内存使用情况,请使用-Xrunhprof:heap=sites
选项调用JVM。
清单3显示了hprof
输出的相关部分,该部分细分了应用程序的内存使用情况。 ( hprof
工具会在应用程序退出后,或者在Windows上以kill -3
或通过按Ctrl + Break信号通知应用程序时,产生使用情况细分。)请注意, Map.Entry
, Task
和int[]
有了显着增长。两个快照之间的int[]
对象。
参见清单3 。
清单4显示了hprof
输出的另一部分,提供了Map.Entry
对象分配站点的调用堆栈信息。 此输出告诉我们哪些调用链正在生成Map.Entry
对象; 通过一些程序分析,通常很容易查明内存泄漏的根源。
清单4. HPROF输出显示Map.Entry对象的分配站点
TRACE 300446:
java.util.HashMap$Entry.<init>(<Unknown Source>:Unknown line)
java.util.HashMap.addEntry(<Unknown Source>:Unknown line)
java.util.HashMap.put(<Unknown Source>:Unknown line)
java.util.Collections$SynchronizedMap.put(<Unknown Source>:Unknown line)
com.quiotix.dummy.MapLeaker.newTask(MapLeaker.java:48)
com.quiotix.dummy.MapLeaker.main(MapLeaker.java:64)
对救援的参考不足
SocketManager
的问题在于Socket
- User
映射的生存期应与Socket
的生存期匹配,但是该语言没有提供任何简单的方法来强制执行此规则。 这迫使程序退回到类似于手动内存管理的技术上。 幸运的是,从JDK 1.2开始,垃圾收集器提供了一种声明此类对象生命周期依赖项的方法,以便垃圾收集器可以帮助我们防止此类内存泄漏(使用弱引用) 。
弱引用是用于参考的一个对象,称为指涉的支架。 使用弱引用,您可以维护对引用的引用,而不会阻止对其进行垃圾回收。 当垃圾回收器跟踪堆时,如果对对象的唯一未完成引用是弱引用,则该引用将成为GC的候选者,就好像没有未完成引用一样,并且会清除所有未完成的弱引用。 (仅由弱引用引用的对象称为弱可达 。)
WeakReference
的引用对象是在构造时设置的,如果尚未清除,则可以使用get()
检索它的值。 如果弱引用已被清除(因为已经对垃圾进行了垃圾回收,或者因为有人调用了WeakReference.clear()
),则get()
返回null
。 因此,您应该始终检查get()
在使用其结果之前是否返回非null值,因为预期该引用最终将被垃圾回收。
使用普通(强)引用复制对象引用时,将引用的生存期限制为至少与复制的引用的生存期一样长。 如果您不小心,则可能是程序的生命周期,例如将对象放置在全局集合中时。 另一方面,在创建对对象的弱引用时,根本不会延长引用对象的生存期。 您只需保持一种替代的方式即可在它还活着时到达它。
弱引用对于构建弱集合最有用,例如那些仅在应用程序的其余部分使用这些对象时才存储有关对象的元数据的弱集合-这正是SocketManager
类应该做的。 因为这是弱引用的常见用法,所以WeakHashMap
也将弱引用用于键(但不用于值),也已添加到JDK 1.2的类库中。 如果在普通的HashMap
使用对象作为键,在从Map
删除映射之前无法收集该对象; WeakHashMap
允许您将对象用作Map
键,而不会阻止对该对象进行垃圾回收。 清单5显示了WeakHashMap
get()
方法的可能实现,其中显示了弱引用的使用:
清单5. WeakReference.get()的可能实现
public class WeakHashMap<K,V> implements Map<K,V> {
private static class Entry<K,V> extends WeakReference<K>
implements Map.Entry<K,V> {
private V value;
private final int hash;
private Entry<K,V> next;
...
}
public V get(Object key) {
int hash = getHash(key);
Entry<K,V> e = getChain(hash);
while (e != null) {
K eKey= e.get();
if (e.hash == hash && (key == eKey || key.equals(eKey)))
return e.value;
e = e.next;
}
return null;
}
调用WeakReference.get()
,它会返回对引用对象的强引用(如果它仍处于活动状态),因此无需担心映射在while
循环主体中消失,因为强引用使它无法被垃圾收集。 WeakHashMap
的实现说明了一个带有弱引用的常见用法-一些内部对象扩展了WeakReference
。 在下一节讨论参考队列时,其原因将变得很清楚。
当您将映射添加到WeakHashMap
,请记住,该映射可能稍后会“掉出”,因为密钥是垃圾回收的。 在那种情况下, get()
返回null
,这比通常更重要的是测试get()
的返回值是否为null
。
使用WeakHashMap堵塞泄漏
在SocketManager
修复泄漏SocketManager
容易; 只需用WeakHashMap
替换HashMap
即可,如清单6所示。(如果SocketManager
需要是线程安全的,则可以用Collections.synchronizedMap()
包装WeakHashMap
)。 每当必须将映射的生存期与密钥的生存期联系在一起时,就可以使用此方法。 但是,您应注意不要过度使用此技术。 大多数情况下,普通的HashMap
是要使用的正确Map
实现。
清单6.使用WeakHashMap修复SocketManager
public class SocketManager {
private Map<Socket,User> m = new WeakHashMap<Socket,User>();
public void setUser(Socket s, User u) {
m.put(s, u);
}
public User getUser(Socket s) {
return m.get(s);
}
}
参考队列
WeakHashMap
使用弱引用来保存映射键,从而可以在应用程序不再使用键对象时对其进行垃圾回收,并且get()
实现可以通过WeakReference.get()
判断死对象的实时映射。返回null
。 但这只是在整个应用程序生存期内避免Map
的内存消耗增加的一半。 在收集了关键对象之后,还必须执行一些操作以从Map
删除无效条目。 否则, Map
将仅填充与死键对应的条目。 尽管这对于应用程序是不可见的,但仍可能导致应用程序用尽内存,因为即使键是键,也不会收集Map.Entry
和value对象。
可以通过定期扫描Map
,在每个弱引用上调用get()
并在get()返回null
删除映射来消除null
映射。 但是,如果Map
有许多实时条目,这将是低效的。 当弱引用的引用对象被垃圾收集时,如果有一种方法可以通知,那就很好了,这就是引用队列的目的。
参考队列是垃圾收集器将有关对象生命周期的信息反馈给应用程序的主要方法。 弱引用具有两个构造函数:一个仅将引用对象作为参数,而另一个也接受引用队列。 如果已经使用关联的参考队列创建了弱引用,并且该参考对象成为GC的候选对象,则清除引用后, 将把参考对象(不是参考对象) 排入参考队列中。 然后,该应用程序可以从引用队列中检索引用,并了解已收集了引用对象,因此它可以执行关联的清除活动,例如删除掉那些来自弱集合的对象的条目。 (参考队列提供与BlockingQueue
相同的出队模式-轮询,定时阻塞和非定时阻塞。)
WeakHashMap
有一个称为expungeStaleEntries()
的私有方法,在大多数Map
操作期间都会调用该方法,该方法会轮询参考队列中是否有过期的参考,并删除关联的映射。 清单7显示了expungeStaleEntries()
可能实现。用于存储键-值映射的Entry
类型扩展了WeakReference
,因此,当expungeStaleEntries()
请求下一个过期的弱引用时,它将返回Entry
。 使用引用队列来清理Map
而不是定期拖曳其内容会更有效,因为在清理过程中永远不会触及实时条目。 它只有在确实有排队的引用时才起作用。
清单7. WeakHashMap.expungeStaleEntries()的可能实现
private void expungeStaleEntries() {
Entry<K,V> e;
while ( (e = (Entry<K,V>) queue.poll()) != null) {
int hash = e.hash;
Entry<K,V> prev = getChain(hash);
Entry<K,V> cur = prev;
while (cur != null) {
Entry<K,V> next = cur.next;
if (cur == e) {
if (prev == e)
setChain(hash, next);
else
prev.next = next;
break;
}
prev = cur;
cur = next;
}
}
}
结论
弱引用和弱集合是用于堆管理的强大工具,允许应用程序使用更复杂的可及性概念,而不是普通(强)引用提供的“全有或全无”可及性。 下个月,我们将看看软引用 ,这是有关弱引用,我们将看看在弱和软引用的存在垃圾收集器的行为。
翻译自: https://www.ibm.com/developerworks/java/library/j-jtp11225/index.html