前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文读懂《Effective Java》第6条:消除GC触及不到的过期对象引用

一文读懂《Effective Java》第6条:消除GC触及不到的过期对象引用

作者头像
后台技术汇
发布2022-05-28 12:37:17
2920
发布2022-05-28 12:37:17
举报
文章被收录于专栏:后台技术汇

因为 JVM 提供了自动管理内存的能力,当我们用完了对象之后,它们会被自动回收,这也容易让我们产生“开发者不再需要考虑内存管理”的错觉了,其实不然。

并非万能的JVM内存管理

上面提到,即使JVM为我们提供了垃圾回收器,将没用的对象回收以节省内存使用。下面我们通过一个例子,意识到内存泄露的存在:

代码语言:javascript
复制
public class Stack {
  private Object[] elements;
  private int size = 0;
  private static final int DEFAULT_INITIAL_CAPACITY = 16;

  // 初始化数组长度为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];
  }

  /**
   * <p>扩容</p>
   */
  private void ensureCapacity() {
    if (elements.length == size){
      elements = Arrays.copyOf(elements, 2 * size + 1);
    }
  }
}

上面程序段隐藏着一个“内存泄露”的问题:随着垃圾回收器活动的增加,或者由于内存占用的不断增加,程序性能的降低会逐渐表现出来。

这个内存泄露的情况就是 pop() 方法,从栈弹出的对象将不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,他们也不会被回收。原因就是,我们声明的栈内部(对象数组)维护着这些对象的过期引用(obsolete reference)。

Arrays.copyOf(elements, 2 * size + 1):扩容方法,底层声明一个两倍的内存空间,然后将原有的数组引用拷贝到新的内存空间里。这样导致引用永远保持存活。而弹出栈顶也仅仅是返回指针指向的元素地址,并未删除对象引用。

过期引用:指的是永远不会再被解除的引用。

在极端情况下,这种内存泄露会导致磁盘交换(Disk Paging),甚至程序失败(OutOfMemoryError 错误),即使这种情况非常少。

对清理过期对象引用进行优化

Java 语言的内存泄露是非常隐蔽的(无意识的对象保持,unintentional object retention)。

一个对象被无意识的保留起来,可能会导致潜在的重大影响:

  1. 垃圾回收机制不再处理这个对象
  2. 垃圾回收机制不再处理这个对象所引用的所以其它对象

因此,对pop()方法的有了下面的优化:

代码语言:javascript
复制
/**
   * 弹出栈顶元素
   */
public Object pop(){
    if (size == 0){
        throw new EmptyStackException();
    }
     Object result = elements[--size];
     elements[size] = null; //释放对象引用
     return result;
  }

清空过期引用的好处之一是,可以尽快检测出程序中的错误,如果不清理导致往后继续被错误解除引用,程序会立即抛出 NullPointException异常。

常见内存泄漏三个场景

第一个内存泄露的常见原因是自行管理内存(例如,开头的Stack 类):

  • 自己管理内存(manage is own memory),存储池(storage pool)包含了 elements 数组(对象引用单元,而不是对象本身)的元素。
  • 数组活动区域是已分配的(allocated),其余部分则是自由的(free),但是 GC 并不知道这一点,所以需要程序员自行将这个情况告知 GC。

解决方法:警惕类内存管理的场景,手动清空这些数组元素。

第二个内存泄漏的常见原因是缓存:一旦将对象引用放到缓存中,它很容易被遗忘掉,从而使得它不再有用并长期停留在缓存。

解决方法:使用 WeakHashMap 代表缓存,当缓存过期后会被自动删除。参考《弱引用是什么》

在Java集合中有一种特殊的Map类型:WeakHashMap。WeakHashMap 继承于AbstractMap,实现了Map接口。 和HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。 不过WeakHashMap的键是“弱键”,里面存放了键对象的弱引用,当某个键不再正常使用时,会从WeakHashMap中被自动移除。当一个键对象被垃圾回收,那么相应的值对象的引用会从Map中删除。WeakHashMap能够节约存储空间,可用来缓存那些非必须存在的数据。

第三个内存泄漏的常见原因是监听器与回调:如果你实现了某个API,客户端在这个 API 中注册回调(例如,流程上需要调用其他服务接口),却没有显式取消注册,这样会导致这类回调请求会积聚。

解决方法:同样将它们的服务调用对象保存为弱引用(weak reference),例如 WeakHashMap 的键。

总结

上文总结了3种常见的Java 内存泄露场景和对应的解决办法。

我们虽然可以依赖于GC,让软件系统不会表现为明显的失败,但如果开发者不注意内存泄露,那么风险依旧长期存在。

而我们往往只有通过仔细检查代码,或者借助Heap剖析工具(Heap Profiler)才能定位发现内存泄露问题。

因此,如果在问题发生前,有意识的阻止发生便是最好不过了。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后台技术汇 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 并非万能的JVM内存管理
  • 对清理过期对象引用进行优化
  • 常见内存泄漏三个场景
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档