《Effective Java》第6条:消除过期的对象引用

Posted by 代码无止境 on 2019-05-16

说到Java,大概很多人都知道GC。Java有自动的垃圾回收机制,当然了,在这篇文章里面就不去深究GC的具体实现了。那么以后了垃圾自动回收,我们是不是就在也不用担心内存泄露的问题了呢。这种问题的答案一般来讲都是否定的。那么这篇文章我们就一起来跟着《Effective Java》来了解一下这个问题。

过期引用

书中首先提到的就是由于过期引用而导致的内存泄露,举了一个Stack的例子:

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
public class Stack {

private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}

public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}

public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}

private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}

根据书中的描述,我们在这个例子中我们是自己管理内存,垃圾回收器并不清楚element中不活跃的部分可以被回收,解决这个问题的方式就是我们通过手动清空这些元素来告诉垃圾回收期它们可以被回收了。

1
2
3
4
5
6
7
8
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object obj = elements[--size];
elements[size] = null;
return obj;
}

我尝试了使用如下的代码,测试了上面有问题的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {

public static void main(String[] args) throws InterruptedException {
Stack stack = new Stack();
for (long i=0; i<Long.MAX_VALUE; i++) {
doSomething(stack);
}
}
private static void doSomething(Stack stack) {
for (int i=0; i<5; i++) {
stack.push(new User(1, "itweknow.cn"));
}
for (int i=0; i<5; i++) {
stack.pop();
}
}
}

可惜并没有测出来内存溢出的错误,在这里要感谢https://bbs.csdn.net/topics/391991081这个帖子给了我答案,有兴趣的同学可以去看一下。再仔细看了一下书中的描述是随着内存占用的不断增加,程序的性能的降低会逐渐表现出来。
在极端的情况下可能会导致内存溢出。
这么看来,一般情况下Stack并不会导致内存溢出,但是由于失效的元素并不能及时的被GC回收,所以在内存接近零界值的时候会极大的影响程序的性能,所以应该尽量避免自己管理内存。

内存的另一个泄露来源-缓存

事实上,如果我们将引用对象放在缓存中,很容易被遗忘,随着缓存的积累很大概率会出现溢出的可能。比方来讲下面这段代码运行一下就会出现异常。不过现在的电脑普遍的配置都比较高,所以在运行的时候我设置了一下JVM参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Question {
/**
* 假如我们现在有一个需求,需要在java内存中缓存发送给用户的短信验证码
* 这里使用HashMap来存储,测试当缓存达到1000000的时候是否会发生内存泄露。
* 启动时设置最大堆内存为10M
* @param args
*/
public static void main(String[] args) throws InterruptedException {
HashMap<Integer, String> codeCache = new HashMap<Integer, String>();
// 方便起见,使用固定验证码。
String code = "000000";
for (int i=0; i<1000000; i++) {
codeCache.put(i, code);
Thread.sleep(1);
}
}
}

监听器和其他回调

在看这条之前我对监听器和回调接触的并不是很多,所以这次决定研究一拨监听器倒是是啥。
其实监听器和回调可以算是一种异步调用的方式。假设我们现在有一个提问者有一个问题1+1=?需要问回答者,然后可能回答者计算这个提问需要一段时间。
为了在单位时间内提问者能够干更多的事儿,那么我们会在提问者类里实现一个回调接口,并且回答者在问题计算完成的时候调用这个回调方法来告知提问者答案。那么监听其实也类似,在我们实际工作中,特别是前端接触的可能比较多,比如说按钮的点击事件的监听,给按钮设置一个监听器,然后当有点击事件产生时,
监听器会调用相应的回调方法进而做相应的处理。下面是我们这个假设的代码实现:

  • 回调接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author ganchaoyang
* @date 2018/11/15 17:45
* @description 回调接口
*/
public interface CallBack {

/**
* 知道答案的回调接口
* @param result
*/
public void solve(String result);

}
  • 提问者
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
public class Questioner implements CallBack{

/**
* 提问者的姓名
*/
private String name;

/**
* 持有一个问题回答者,因为需要知道向谁提问
*/
private Author author;

public Questioner(String name) {
this.name = name;
}

/**
* 设置提问者,相当于设置监听器
* @return
*/
public Questioner setAuthor(Author author) {
this.author = author;
return this;
}

public void doRequest() {
new Thread(new Runnable() {
@Override
public void run() {
author.execRequest(Questioner.this, "question");
}
}).start();
// 问题问了之后,去做其他事情。
doOtherThings();
}

private void doOtherThings() {
System.out.println("do other things...");
}

/**
* 回答者知道答案后会通过这个方法来告知提问者答案
* @param result
*/
@Override
public void solve(String result) {
System.out.println(author.getName() + "告诉" + getName()
+ "答案是:" + result);
}

public String getName() {
return name;
}

public Questioner setName(String name) {
this.name = name;
return this;
}
}
  • 回答者
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
public class Author {

private String name;

public Author(String name) {
this.name = name;
}

public String getName() {
return name;
}

public Author setName(String name) {
this.name = name;
return this;
}

/**
* 处理问题
* @param callBack
* @param qeustion
*/
public void execRequest(CallBack callBack, String qeustion) {
// 有一个等待,模拟处理过程。
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 得到答案,调用提问者的回调方法。
callBack.solve("solved");
}
}
  • 调用方式
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {

public static void main(String[] args) throws InterruptedException {
// 假设现在有10000个提问者,10000个回答者,提问者分别向回答者提问。
for (int i=0; i< 10000 ; i++) {
Questioner questioner = new Questioner("Q" + i);
Author author = new Author("A" + i);
questioner.setAuthor(author);
questioner.doRequest();
Thread.sleep(100);
}
}
}

我们给被监听者设置了一个监听器,如果我们在被监听者销毁的时候没有去注销监听,那么监听器就会一直持有被监听者的引用,这个时候GC就不会去回收被监听者,久而久之也就有可能会发生内存泄漏。

ps:“学习不止,码不停蹄”,如果你喜欢我的文章,就关注我吧。

扫码关注“代码无止境”