-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMemoryLeaks.java
More file actions
209 lines (186 loc) · 8.65 KB
/
Copy pathMemoryLeaks.java
File metadata and controls
209 lines (186 loc) · 8.65 KB
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package Phase6_RuntimeMemoryRegexReflection.MemoryAllocation;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Memory Leaks in Java
* --------------------
* Java is garbage-collected, but the GC cannot help you if your code is
* STILL holding a reference to data you no longer need. The garbage
* collector reclaims only UNREACHABLE objects - so any "live" path back to
* the heap (a static field, a long-lived collection, a registered
* listener, ...) is a leak waiting to happen.
* <p>
*
* The Five Most Common Java Leak Patterns
* ---------------------------------------
* <p>
*
* 1. STATIC COLLECTIONS that grow forever
* A `static Map<String, Customer> CACHE = new HashMap<>();` with no
* eviction policy will hold every customer it ever sees. Use a
* bounded cache (Guava `Cache`, `Caffeine`, or your own LRU).
* <p>
*
* 2. KEYS WITHOUT proper equals/hashCode
* Entries put into a HashMap can become "lost" - present in memory
* but unfindable - if you mutate the key after insertion.
* <p>
*
* 3. INNER CLASSES holding the enclosing instance
* A non-static inner class keeps an implicit reference to its outer
* instance. Same with lambdas that capture `this`. If the inner
* object outlives the outer (e.g. registered with a long-lived
* executor), the outer cannot be reclaimed.
* <p>
*
* 4. LISTENERS / CALLBACKS that you never unregister
* Every observer is a strong reference held by the subject. When you
* stop using the observer but forget to call `removeListener`, the
* subject keeps it alive forever.
* <p>
*
* 5. UNCLOSED RESOURCES (streams, JDBC connections, native handles)
* Even Java-managed wrappers hold on to OS-level resources until
* `close()` runs. Use try-with-resources for everything that
* implements AutoCloseable.
* <p>
*
* Detecting Leaks
* ---------------
* - Watch heap usage over time - a healthy app oscillates; a leaking one
* trends upward after every GC.
* - Take a heap dump (-XX:+HeapDumpOnOutOfMemoryError or `jmap -dump`)
* and inspect with Eclipse MAT, VisualVM, or JProfiler.
* - Use `jcmd <pid> GC.class_histogram` for a quick instance count.
* <p>
*
* This file demonstrates the FIRST three leak patterns and their fixes.
*/
public class MemoryLeaks {
// ============================================================
// LEAK 1 - A static cache that grows forever
// ============================================================
static final Map<String, byte[]> LEAKY_CACHE = new HashMap<>();
// The fixed version - same idea but capped to N entries (very basic LRU).
static final Map<String, byte[]> FIXED_CACHE = new java.util.LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {
return size() > 100;
}
};
// ============================================================
// LEAK 3 - A non-static inner class that holds the outer
// ============================================================
static class Outer {
private final byte[] heavyState = new byte[1024 * 1024]; // 1 MB
Runnable task() {
// Non-static inner class - captures `Outer.this` implicitly.
return new Runnable() {
@Override public void run() {
// touches outer's state, hence the implicit reference
System.out.println("inner saw " + heavyState.length + " bytes");
}
};
}
}
static class FixedOuter {
private final byte[] heavyState = new byte[1024 * 1024];
// Static nested class does NOT capture the enclosing instance.
static class Task implements Runnable {
@Override public void run() {
System.out.println("static nested task running (no outer ref)");
}
}
Runnable task() { return new Task(); }
}
public static void main(String[] args) throws Exception {
section("1) Static collection growing forever - bad");
for (int i = 0; i < 10_000; i++) {
LEAKY_CACHE.put("k-" + i, new byte[1024]); // 10 MB of junk
}
long mb = 1024 * 1024;
Runtime rt = Runtime.getRuntime();
System.out.printf("leaky size = %d entries, heap = %d MB%n",
LEAKY_CACHE.size(), (rt.totalMemory() - rt.freeMemory()) / mb);
section("2) Same scenario with a bounded LRU cache - fixed");
for (int i = 0; i < 10_000; i++) {
FIXED_CACHE.put("k-" + i, new byte[1024]);
}
System.out.printf("fixed size = %d entries, heap = %d MB%n",
FIXED_CACHE.size(), (rt.totalMemory() - rt.freeMemory()) / mb);
// FIXED_CACHE never grows past 100.
section("3) Mutating a HashMap key after insertion - lost entry");
Map<MutableKey, String> map = new HashMap<>();
MutableKey k = new MutableKey("alpha");
map.put(k, "the value");
System.out.println("contains 'alpha' BEFORE mutation: " + map.containsKey(k));
k.name = "beta"; // changes the hash code!
System.out.println("contains same AFTER mutation: " + map.containsKey(k));
// The entry is still in the map, but it lives in the BUCKET FOR "alpha"
// while the new hash points to the BUCKET FOR "beta" - a leak.
System.out.println("map.size() = " + map.size());
section("4) Non-static inner class keeps outer alive - illustration");
Outer outer = new Outer();
Runnable r = outer.task(); // `r` holds Outer.this -> heavyState
outer = null;
System.gc(); Thread.sleep(50);
r.run(); // still works - the outer is alive
FixedOuter fixed = new FixedOuter();
Runnable r2 = fixed.task();
fixed = null;
System.gc(); Thread.sleep(50);
r2.run(); // works without holding the outer
section("5) Listener you forget to unregister");
Subject s = new Subject();
Listener l = msg -> System.out.println("listener got: " + msg);
s.add(l);
s.publish("first message");
// We "forget" to call remove(l). Subject -> Listener -> any heavy state
// the listener captures is kept alive forever.
System.out.println("subject still holds " + s.size() + " listener(s)");
section("6) Unclosed resource - native handle leak");
// Bad - forgetting to close:
// FileInputStream in = new FileInputStream("..."); // close was never called
// Fix with try-with-resources:
//
// try (FileInputStream in = new FileInputStream("...")) { ... }
section("7) Long-lived thread holding an executor full of work");
// Threads themselves are GC ROOTS - any data they reference is alive.
// Always shut down executors.
ScheduledExecutorService es = Executors.newSingleThreadScheduledExecutor();
es.scheduleAtFixedRate(() -> {}, 0, 1, TimeUnit.HOURS);
// ... use it ...
es.shutdown(); // remember THIS line.
System.out.println("executor shutdown called");
// OUTPUT (numbers vary)
}
/** A small Subject/Listener pair to demo the listener-leak pattern. */
static class Subject {
private final List<Listener> listeners = new ArrayList<>();
public void add(Listener l) { listeners.add(l); }
public void remove(Listener l) { listeners.remove(l); }
public void publish(String msg){ for (Listener l : listeners) l.onMessage(msg); }
public int size() { return listeners.size(); }
}
interface Listener { void onMessage(String msg); }
/** A key whose hashCode changes when the field is mutated - a leak pattern. */
static class MutableKey {
String name;
MutableKey(String n) { this.name = n; }
@Override public int hashCode() { return name.hashCode(); }
@Override public boolean equals(Object o) {
return o instanceof MutableKey k && k.name.equals(this.name);
}
}
private static void section(String title) {
System.out.println("\n====== " + title + " ======");
}
// Keep the imports referenced so the static analyzer is quiet.
@SuppressWarnings("unused")
private static WeakHashMap<?, ?> wh;
@SuppressWarnings("unused")
private static ConcurrentHashMap<?, ?> ch;
}