小心Java中的内存泄漏

简介:

    当你从手工管理内存的语言(比如C或C++)转换到具有垃圾回收功能的语言的时候,你的工作会变得更加容易,因为当你用完了这些对象以后,它们会被自动回收。当你第一次经历对象回收功能的时候,会觉得这简直不可思议。这很容易给你留下这样的印象:认为自己不需要考虑内存管理的事情了。但事实上并非如此。

    看下面这个简单的栈实现的例子:

?
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
public  class  Stack {
     private  Object elms[];
     private  int  size =  0 ;
     private  static  final  int  DEFAULT_CAPACITY =  16 ;
     public  Stack(){
         elms =  new  Object[DEFAULT_CAPACITY];
     }
     
     //模拟进栈
     public  void  push(Object o) {
         checkCapacity();
         elms[size++] = o;
     }
     
     //模拟出栈
     public  Object pop() {
         if  (size ==  0 ) {
             throw  new  EmptyStackException();
         }
         return  elms[--size]; //取最后一个元素
     }
     
     //检查数组长度,确保够用
     private  void  checkCapacity(){
         if  (elms.length == size) {
             elms = Arrays.copyOf(elms,  2  * size +  1 );
         }
     }
}

    这段程序中并没有很明显的错误,但是这个程序中隐藏着一个问题。随着栈中元素的增加,内存占用将不断增加,程序性能的降低会主键表现出来。在极端的情况下,这种内存泄漏会导致磁盘交换,甚至引发OutOfMemoryError错误。但这种情况相对比较少见。

    那么,程序中哪里发生了内存泄漏呢?想象一下,如果一个栈线增长,后收缩,那么从栈中弹出的对象将不会被当作垃圾回收,及时使用栈的程序不再引用这些对象,它们也不会被回收。这是因为,栈内部维护着对这些对象的过期引用。所谓过期引用是指永远也不会再被解除的引用。在本例中,凡是在elms数组“活动部分”之外的任何引用都是过期的。活动部份是指elms中,下标小于size的那些元素。

    在支持垃圾回收的语言中,内存泄漏是很隐蔽的(称这类内存泄漏为“无意识的对象保持”更为恰当)。如果一个对象引用被无意识的保留起来了,那么垃圾回收机制不仅不会处理这个对象,还不会处理被这个对象引用的所有其他对象。即使只有少量的几个对象引用被无意识的保留了下来,也会有许许多多的对象被排除在垃圾回收机制以外,从而对性能和安全造成潜在的影响。

    这类问题的修复办法很简单:一旦发现引用已经过期,清空他们就行了。对于本例而言,只要一个对象被弹出(这里其实仅仅是返回了一个引用,这个对象其实不在数组中),那么它指向的这个对象就应该为空,而不仅仅是简单的return。本例中的pop方法修复如下:

?
1
2
3
4
5
6
7
8
public  Object pop() {
     if  (size ==  0 ) {
         throw  new  EmptyStackException();
     }
     Object result = elms[--size];
     elms[size] =  null ; //取消对过期对象的引用
     return  result;
}

    清空过期引用的另一个好处是,如果他们以后又被错误的解除引用,程序会立即抛出NullPointerException异常,而不是悄悄错误的运行下去。尽快检测出程序中的错误是有益的。

    当程序员第一次被这样的问题困扰的时候,他们往往会过分小心:对于每一个对象引用,一旦程序不再用到,就会手动将其清空。其实这样做没有必要,也不是我们所期望的,因为这样做会把程序弄得很乱。清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用的做好办法是让包含该引用的变量结束其生命周期。

    那么何时应该清空引用呢?Stack类的哪些方面的特性使它容易遭受内存泄漏的影响呢?问题在于:Stack类自己管理内存。存储池包含了elms数组的元素。数组的活动区(前文有定义)中的元素是已分配的,而数组其余部分的元素则是自由的。但是垃圾回收器并不知道这一点。对于垃圾回收器而言,emls数组中的所有对象引用都是同等有效的,只有当程序员知道数组的非活动区是不重要的。程序员可以把这个情况告知垃圾回收器,做法很简单:一旦数组元素变成了非活动区的一部分,程序员就手工清空这些数组元素。

    一般而言,只要类是自己管理内存,程序员就应该警惕内存泄漏的问题。一旦元素被释放掉,则该元素中包含的任何引用对象都应该被清空。

    内存泄漏的另一个常见来源是缓存。一旦你把对象的引用防盗缓存中,它就很容易被遗忘掉,从而使得它不再有用。之后很长一段时间内仍然留在缓存中。对于这个问题,有几种可能的解决方案。如果你正好要实现这样的缓存:只要缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap(java.util下面的类)代表缓存;当缓存中的项过期之后,他们就会被自动删除(这是WeakHashMap自带的功能)。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。

    更为常见的情形是:“缓存项的生命周期是否有意义”并不是很容易确定,随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该时不时的清除掉没用的项。这项清除工作可以由一个后台线程(可能是Timer或是ScheduledThreadPoolExecutor)来完成,或者也可以在缓存添加新条目的时候顺便进行清理工作。LinkedHashMap类利用它的removeEldestEntry方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用java.lang.ref。

    内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显式的取消注册,那么除非你采取某些动作,否则它们就会积累。确保回调立即被当作垃圾回收的最佳方法是只保存他们的弱引用。例如:只将它们保存成WeakHashMap中的键。

    由于内存泄漏通常不会表现成明显的失败,所以它们可以在一个系统中存在多年。往往只有通过仔细检查代码,或者借助于Heap剖析工具,才能发现内存泄漏的问题。因此,如果能够在使用内存泄漏发生之前就知道如何预测此类问题,并阻止它们发生,那是最好不过的了。

目录
相关文章
|
1月前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
38 6
|
22天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
24 0
|
1月前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
1月前
|
存储 算法 Java
Java内存管理深度剖析与优化策略####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,重点分析了堆内存的分配策略、垃圾回收算法以及如何通过调优提升应用性能。通过案例驱动的方式,揭示了常见内存泄漏的根源与解决策略,旨在为开发者提供实用的内存管理技巧,确保应用程序既高效又稳定地运行。 ####
|
24天前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
37 8
|
22天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
26天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
53 5
|
24天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
24天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
29天前
|
算法 Java 开发者
Java内存管理与垃圾回收机制深度剖析####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,特别是其垃圾回收机制的工作原理、算法及实践优化策略。不同于传统的摘要概述,本文将以一个虚拟的“城市环卫系统”为比喻,生动形象地揭示Java内存管理的奥秘,旨在帮助开发者更好地理解并调优Java应用的性能。 ####