Java 理论与实践:用弱引用堵住内存泄漏-1

上一篇 / 下一篇  2012-07-27 10:00:24 / 个人分类:Java

Z8Wub\0  简介:虽然用 Java™ 语言编写的程序在理论上是不会出现“内存泄漏”的,但是有时对象在不再作为程序的逻辑状态的一部分之后仍然不被垃圾收集。本月,负责保障应用程序健康的工 程师 Brian Goetz 探讨了无意识的对象保留的常见原因,并展示了如何用弱引用堵住泄漏。51Testing软件测试网F/en Zg

cYS#w)\C0  要让垃圾收集(GC)回收程序不再 使用的对象,对象的逻辑 生命周期(应用程序使用它的时间)和对该对象拥有的引用的实际 生命周期必须是相同的。在大多数时候,好的软件工程技术保证这是自动实现的,不用我们对对象生命周期问题花费过多心思。但是偶尔我们会创建一个引用,它在 内存中包含对象的时间比我们预期的要长得多,这种情况称为无意识的对象保留(unintentional object retention)。51Testing软件测试网v9m;dPVA.|"T

q J Ws9v-U7N Ne0  全局 Map 造成的内存泄漏51Testing软件测试网:x{-b*c E0T%QT

51Testing软件测试网O:z Cg nN?

   无意识对象保留最常见的原因是使用 Map 将元数据与临时对象(transient object)相关联。假定一个对象具有中等生命周期,比分配它的那个方法调用的生命周期长,但是比应用程序的生命周期短,如客户机的套接字连接。需要将 一些元数据与这个套接字关联,如生成连接的用户的标识。在创建 Socket 时是不知道这些信息的,并且不能将数据添加到 Socket 对象上,因为不能控制 Socket 类或者它的子类。这时,典型的方法就是在一个全局 Map 中存储这些信息,如清单 1 中的 SocketManager 类所示:51Testing软件测试网o _.LX!RDx

D iE ^ B%\|0  清单 1. 使用一个全局 Map 将元数据关联到一个对象51Testing软件测试网9x.[;YFbAUa^(Q

public class SocketManager {51Testing软件测试网M2GK!@&{
    private Map<Socket,User> m = new HashMap<Socket,User>();51Testing软件测试网 b|[E'EX4T.l
   51Testing软件测试网+U/j `V ?^
    public void setUser(Socket s, User u) {
:A4@ H&`.G9x k7i/eO y0        m.put(s, u);51Testing软件测试网,Y?"e1Yq ax,V3_g
    }51Testing软件测试网N e:M.O5O/M3o
    public User getUser(Socket s) {
hb9oe)@`{0        return m.get(s);51Testing软件测试网9_;A8H%b#]O5w
    }51Testing软件测试网6}op`:d5d[3@%~
    public void removeUser(Socket s) {51Testing软件测试网~1_(]7NkN
        m.remove(s);
!pw*cmo4w0    }51Testing软件测试网X'f PVz
}
^:[A.ey'zI0SocketManager socketManager;51Testing软件测试网;z.pn$U|Q W6J
...
;h&@;G5TAv(WI S7r0socketManager.setUser(socket, user);

@O G}RLs0   这种方法的问题是元数据的生命周期需要与套接字的生命周期挂钩,但是除非准确地知道什么时候程序不再需要这个套接字,并记住从 Map 中删除相应的映射,否则,Socket 和 User 对象将会永远留在 Map 中,远远超过响应了请求和关闭套接字的时间。这会阻止 Socket 和 User 对象被垃圾收集,即使应用程序不会再使用它们。这些对象留下来不受控制,很容易造成程序在长时间运行后内存爆满。除了最简单的情况,在几乎所有情况下找出 什么时候 Socket 不再被程序使用是一件很烦人和容易出错的任务,需要人工对内存进行管理。

M5Kr H^9RIS0

9U%Pl%X4_ }:d4h0  找出内存泄漏

$^{5Df9a7z:I)h A0

:{,zNaZ-G$P,t-b'N6u0   程序有内存泄漏的第一个迹象通常是它抛出一个 OutOfMemoryError,或者因为频繁的垃圾收集而表现出糟糕的性能。幸运的是,垃圾收集可以提供能够用来诊断内存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 选项调用 JVM,那么每次 GC 运行时在控制台上或者日志文件中会打印出一个诊断信息,包括它所花费的时间、当前堆使用情况以及恢复了多少内存。记录 GC 使用情况并不具有干扰性,因此如果需要分析内存问题或者调优垃圾收集器,在生产环境中默认启用 GC 日志是值得的。

'C{;M n z:}A z%p051Testing软件测试网a4~| n.sg~ M

   有工具可以利用 GC 日志输出并以图形方式将它显示出来,JTune 就是这样的一种工具。观察 GC 之后堆大小的图,可以看到程序内存使用的趋势。对于大多数程序来说,可以将内存使用分为两部分:baseline 使用和 current load 使用。对于服务器应用程序,baseline 使用就是应用程序在没有任何负荷、但是已经准备好接受请求时的内存使用,current load 使用是在处理请求过程中使用的、但是在请求处理完成后会释放的内存。只要负荷大体上是恒定的,应用程序通常会很快达到一个稳定的内存使用水平。如果在应用 程序已经完成了其初始化并且负荷没有增加的情况下,内存使用持续增加,那么程序就可能在处理前面的请求时保留了生成的对象。51Testing软件测试网)s'J7i lPCx1w

#S:AO&M*ZX%Q_(V0  清单 2 展示了一个有内存泄漏的程序。MapLeaker 在线程池中处理任务,并在一个 Map 中记录每一项任务的状态。不幸的是,在任务完成后它不会删除那一项,因此状态项和任务对象(以及它们的内部状态)会不断地积累。

,_ LPXuL+Vo}P0

tB(H%L5] [4{}j7i _ r0  清单 2. 具有基于 Map 的内存泄漏的程序

y SE0mqmP0public class MapLeaker {
q.s1k T^9@0    public ExecutorService exec = Executors.newFixedThreadPool(5);51Testing软件测试网}7{ Wt'x"v.i Em[.u
    public Map<Task, TaskStatus> taskStatus51Testing软件测试网i E;VS7b:m)l+z
        = Collections.synchronizedMap(new HashMap<Task, TaskStatus>());51Testing软件测试网3CRjR#yX)k
    private Random random = new Random();
9SW4t&Z9}+l7C0    private enum TaskStatus { NOT_STARTED, STARTED, FINISHED };51Testing软件测试网&u7u*Z)b4H7X
    private class Task implements Runnable {
)SK&X ^;_^0        private int[] numbers = new int[random.nextInt(200)];51Testing软件测试网3B7r5a!Dk
        public void run() {51Testing软件测试网7I$e)S?9}'h(S"{N3p4e
            int[] temp = new int[random.nextInt(10000)];
| C~G.t ]/` V0            taskStatus.put(this, TaskStatus.STARTED);51Testing软件测试网#v,HeurY
            doSomeWork();51Testing软件测试网 Z)t3F.BR(A*I
            taskStatus.put(this, TaskStatus.FINISHED);51Testing软件测试网CX7GZ%V8m
        }51Testing软件测试网I1{Gtdn
    }
#P'p6W&| }O.G/eFB0    public Task newTask() {
D(O fRy0        Task t = new Task();
4hbD%r/|^1|+G0        taskStatus.put(t, TaskStatus.NOT_STARTED);
^:`,t4kD)p}r!^!vg0[0        exec.execute(t);
3E"Y\6B,Q;w"D`0        return t;
yqp*w.IDJ0    }
2pUAjJN/b"n A!Zj0}51Testing软件测试网/c.V-v/OZ
51Testing软件测试网 D6G0CT$YOI!~8C;@

  图 1 显示 MapLeaker GC 之后应用程序堆大小随着时间的变化图。上升趋势是存在内存泄漏的警示信号。(在真实的应用程序中,坡度不会这么大,但是在收集了足够长时间的 GC 数据后,上升趋势通常会表现得很明显。)51Testing软件测试网tZ#ttP;`u%[

51Testing软件测试网EV*o(\Z7^R)p

  图 1. 持续上升的内存使用趋势

e@k7n!Z`Jj A)y0

51Testing软件测试网MrE&z |

  确信有了内存泄漏后,下一步就是找出哪种对象造成了这个问题。所有内存分析器都可以生成按照对象类进行分解的堆快 照。有一些很好的商业堆分析工具,但是找出内存泄漏不一定要花钱买这些工具 —— 内置的 hprof 工具也可完成这项工作。要使用 hprof 并让它跟踪内存使用,需要以 -Xrunhprof:heap=sites 选项调用 JVM。

O3L:h1Q3Ca^G0

  清单 3 显示分解了应用程序内存使用的 hprof 输出的相关部分。(hprof 工具在应用程序退出时,或者用 kill -3 或在 Windows 中按 Ctrl+Break 时生成使用分解。)注意两次快照相比,Map.Entry、Task 和 int[] 对象有了显著增加。

;Me:L'i\%{x#e _0

  清单 3. HPROF 输出,显示 Map.Entry 和 Task 对象的增加

;f%| uY&_!\~y0

51Testing软件测试网;J8}.a G7rG*zs

SITES BEGIN (ordered by live bytes) Fri Oct 28 16:30:48 2005
Cp|gK6P,Scl2J0          percent          live          alloc'ed  stack class51Testing软件测试网E(eS c"u)nSK+D
 rank   self  accum     bytes objs     bytes  objs trace name
,[Lh:Q%~%Cb/R0    1 70.13% 70.13%   5694888 13909   5694888 13909 300305 int[]51Testing软件测试网 _An(F6| f|#Pu4V
    2 18.27% 88.40%   1483976   68 278273632 13908 300321 int[]
1I#X^4V$VE0    3  4.11% 92.51%    333816 13909    333816 13909 300310 java.util.HashMap$Entry51Testing软件测试网8uZ ZL.xQ OI {#\
    4  2.74% 95.25%    222544 13909    222544 13909 300304 com.quiotix.dummy.MapLeaker$Task
Sahphi&S$@0    5  2.42% 97.67%    196640    2    262192    11 300325 java.util.HashMap$Entry[]51Testing软件测试网6f0e!H_JS0Z
    6  0.66% 98.33%     53680 3355    222464 13904 300324 java.util.concurrent.LinkedBlockingQueue$Node51Testing软件测试网i5rh9oN.E^w
SITES END
i4P/A'Q4aH0SITES BEGIN (ordered by live bytes) Fri Oct 28 16:31:32 2005
|;_7X+^1id0S la$C0          percent          live          alloc'ed  stack class
q-s7Y$} A }j0 rank   self  accum     bytes objs     bytes  objs trace name
:y$pg+Lo._0    1 77.07% 77.07%  41176024 100020  41176024 100020 301069 int[]51Testing软件测试网_*Bt(s9m4|
    2 12.98% 90.05%   6933768  359 2001885688 100020 301093 int[]
+m-m b$c7[A0    3  4.49% 94.55%   2400480 100020   2400480 100020 301082 java.util.HashMap$Entry
)l D'JC8teC+r0    4  3.00% 97.54%   1600320 100020   1600320 100020 301068 com.quiotix.dummy.MapLeaker$Task51Testing软件测试网P'dj.dX2P
    5  1.96% 99.50%   1048592    1   2097248    14 301104 java.util.HashMap$Entry[]51Testing软件测试网#wZ6|!YF0hl
    6  0.05% 99.55%     25936 1621   1600240 100015 301101 java.util.concurrent.LinkedBlockingQueue$Node
qIf'd!i7y.r0SITES END

  清单 4 展示了 hprof 输出的另一部分,给出了 Map.Entry 对象的分配点的调用堆栈信息。这个输出告诉我们哪些调用链生成了 Map.Entry 对象,并带有一些程序分析,找出内存泄漏来源一般来说是相当容易的。51Testing软件测试网W OD(HK x?];U6rt3e

  清单 4. HPROF 输出,显示 Map.Entry 对象的分配点

+pVty8Q2Ldzx0

2AhM ?(g#b)TS0TRACE 300446:51Testing软件测试网+zu3nE,f-Z"r3S
 java.util.HashMap$Entry.<init>(<Unknown Source>:Unknown line)51Testing软件测试网.y1[0l+s.~v:XF"T&~9XA
 java.util.HashMap.addEntry(<Unknown Source>:Unknown line)51Testing软件测试网2@2giE;K
 java.util.HashMap.put(<Unknown Source>:Unknown line)
FR0pO+k-z6s\zy0 java.util.Collections$SynchronizedMap.put(<Unknown Source>:Unknown line)
(B}0X~3` k5Oc0 com.quiotix.dummy.MapLeaker.newTask(MapLeaker.java:48)51Testing软件测试网kcoT"k3Y({*G
 com.quiotix.dummy.MapLeaker.main(MapLeaker.java:64)51Testing软件测试网FW b a-g whU'`"|

TAG:

 

评分:0

我来说两句

Open Toolbar