`

Java 内存管理原理、内存泄漏实例及解决方案

阅读更多

 在项目的最后阶段,就是要防止系统的内存泄漏了,顺便找了些资料,看了些java内存泄漏的实例及解决,总结一下:
Java是如何管理内存


为了判断Java中是否有内存泄露,我们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。在Java中,程序员需要通过关键字new为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在Java中,内存的分配是由程序完成的,而内存的释放是有GC完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是Java程序运行速度较慢的原因之一。因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。

监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

为了更好理解GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。

以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示意图。


 
Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。

什么是Java中的内存泄露


下面,我们就可以描述什么是内存泄漏。在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。

通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java提高了编程的效率。


 


因此,通过以上分析,我们知道在Java中也有内存泄漏,但范围比C++要小一些。因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。

对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义, 该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。

下面给出了一个简单的内存泄露的例子。在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个Vector中,如果我们仅仅释放引用本身,那么Vector仍然引用该对象,所以这个对象对GC来说是不可回收的。因此,如果对象加入到Vector后,还必须从Vector中删除,最简单的方法就是将Vector对象设置为null。

 

Vector v=new Vector(10);
for (int i=1;i<100; i++)
{
 Object o=new Object();
 v.add(o);
 o=null; 
}

//此时,所有的Object对象都没有被释放,因为变量v引用这些对象。


Java内存泄漏的类型、实例及解决

1.对象游离

  一种形式的内存泄漏有时候叫做对象游离(object loitering),是通过清单 1 中的 LeakyChecksum 类来说明的,清单 1 中有一个 getFileChecksum() 方法用于计算文件内容的校验和。getFileChecksum() 方法将文件内容读取到缓冲区中以计算校验和。一种更加直观的实现简单地将缓冲区作为 getFileChecksum() 中的本地变量分配,但是该版本比那样的版本更加 “聪明”,不是将缓冲区缓存在实例字段中以减少内存 churn。该 “优化”通常不带来预期的好处;对象分配比很多人期望的更便宜。(还要注意,将缓冲区从本地变量提升到实例变量,使得类若不带有附加的同步,就不再是线程安全的了。直观的实现不需要将 getFileChecksum() 声明为 synchronized,并且会在同时调用时提供更好的可伸缩性。)

  清单 1. 展示 “对象游离” 的类

// BAD CODE - DO NOT EMULATE
public class LeakyChecksum {
 private byte[] byteArray;

 public synchronized int getFileChecksum(String fileName) {
  int len = getFileSize(fileName);
  if (byteArray == null || byteArray.length < len)
   byteArray = new byte[len];
  readFileContents(fileName, byteArray);
  // calculate checksum and return it
 }
}
  这个类存在很多的问题,但是我们着重来看内存泄漏。缓存缓冲区的决定很可能是根据这样的假设得出的,即该类将在一个程序中被调用许多次,因此它应该更加有效,以重用缓冲区而不是重新分配它。但是结果是,缓冲区永远不会被释放,因为它对程序来说总是可及的(除非 LeakyChecksum 对象被垃圾收集了)。更坏的是,它可以增长,却不可以缩小,所以 LeakyChecksum 将永久保持一个与所处理的最大文件一样大小的缓冲区。退一万步说,这也会给垃圾收集器带来压力,并且要求更频繁的收集;为计算未来的校验和而保持一个大型缓冲区并不是可用内存的最有效利用。

  LeakyChecksum 中问题的原因是,缓冲区对于 getFileChecksum() 操作来说逻辑上是本地的,但是它的生命周期已经被人为延长了,因为将它提升到了实例字段。因此,该类必须自己管理缓冲区的生命周期,而不是让 JVM 来管理。

  软引用

  弱引用如何可以给应用程序提供当对象被程序使用时另一种到达该对象的方法,但是不会延长对象的生命周期。Reference 的另一个子类 —— 软引用 —— 可满足一个不同却相关的目的。其中弱引用允许应用程序创建不妨碍垃圾收集的引用,软引用允许应用程序通过将一些对象指定为 “expendable” 而利用垃圾收集器的帮助。尽管垃圾收集器在找出哪些内存在由应用程序使用哪些没在使用方面做得很好,但是确定可用内存的最适当使用还是取决于应用程序。如果应用程序做出了不好的决定,使得对象被保持,那么性能会受到影响,因为垃圾收集器必须更加辛勤地工作,以防止应用程序消耗掉所有内存。

  高速缓存是一种常见的性能优化,允许应用程序重用以前的计算结果,而不是重新进行计算。高速缓存是 CPU 利用和内存使用之间的一种折衷,这种折衷理想的平衡状态取决于有多少内存可用。若高速缓存太少,则所要求的性能优势无法达到;若太多,则性能会受到影响,因为太多的内存被用于高速缓存上,导致其他用途没有足够的可用内存。因为垃圾收集器比应用程序更适合决定内存需求,所以应该利用垃圾收集器在做这些决定方面的帮助,这就是件引用所要做的。

  如果一个对象惟一剩下的引用是弱引用或软引用,那么该对象是软可及的(softly reachable)。垃圾收集器并不像其收集弱可及的对象一样尽量地收集软可及的对象,相反,它只在真正 “需要” 内存时才收集软可及的对象。软引用对于垃圾收集器来说是这样一种方式,即 “只要内存不太紧张,我就会保留该对象。但是如果内存变得真正紧张了,我就会去收集并处理这个对象。” 垃圾收集器在可以抛出 OutOfMemoryError 之前需要清除所有的软引用。

  通过使用一个软引用来管理高速缓存的缓冲区,可以解决 LeakyChecksum 中的问题,如清单 2 所示。现在,只要不是特别需要内存,缓冲区就会被保留,但是在需要时,也可被垃圾收集器回收:

清单 2. 用软引用修复 LeakyChecksum

public class CachingChecksum {
 private SoftReference bufferRef;

 public synchronized int getFileChecksum(String fileName) {
  int len = getFileSize(fileName);
  byte[] byteArray = bufferRef.get();
  if (byteArray == null || byteArray.length < len) {
   byteArray = new byte[len];
   bufferRef.set(byteArray);//把缓冲区设为软引用
  }
  readFileContents(fileName, byteArray);
  // calculate checksum and return it
 }
}

2、基于数组的集合

  当数组用于实现诸如堆栈或环形缓冲区之类的数据结构时,会出现另一种形式的对象游离。清单 3 中的 LeakyStack 类展示了用数组实现的堆栈的实现。在 pop() 方法中,在顶部指针递减之后,elements 仍然会保留对将弹出堆栈的对象的引用。这意味着,该对象的引用对程序来说仍然可及(即使程序实际上不会再使用该引用),这会阻止该对象被垃圾收集,直到该位置被未来的 push() 重用。

  清单 3. 基于数组的集合中的对象游离
public class LeakyStack {
 private Object[] elements = new Object[MAX_ELEMENTS];
 private int size = 0;

 public void push(Object o) { elements[size++] = o; }
 
 public Object pop() {
  if (size == 0)
   throw new EmptyStackException();
  else {
   Object result = elements[--size];
   // elements[size+1] = null;
  return result;
 }
}
}

修复这种情况下的对象游离的方法是,当对象从堆栈弹出之后,就消除它的引用,如清单  // elements[size+1] = null;
所示。但是这种情况 —— 由类管理其自己的内存 —— 是一种非常少见的情况,即显式地消除不再需要的对象是一个好主意大部分时候,认为不应该使用的强行消除引用根本不会带来性能或内存使用方面的收益,通常是导致更差的性能或者 NullPointerException。该算法的一个链接实现不会存在这个问题。在链接实现中,链接节点(以及所存储的对象的引用)的生命期将被自动与对象存储在集合中的期间绑定在一起。弱引用可用于解决这个问题 —— 维护弱引用而不是强引用的一个数组 —— 但是在实际中,LeakyStack 管理它自己的内存,因此负责确保对不再需要的对象的引用被清除。使用数组来实现堆栈或缓冲区是一种优化,可以减少分配,但是会给实现者带来更大的负担,需要仔细地管理存储在数组中的引用的生命期。

 

分享到:
评论

相关推荐

    如何解决Java内存泄漏

    因此,了解Java内存泄漏的成因、检测方法以及解决方案对于保证应用的高效稳定运行至关重要。 #### 2. Java内存回收机制 Java的内存管理主要集中在堆(Heap)区域,其中对象的创建通常是通过`new`关键字或反射方式...

    java内存溢出解决方案

    本文将深入探讨Java内存溢出的不同类型及其解决方案。 1. **内存溢出类型** - **1.1. java.lang.OutOfMemoryError: PermGen space** PermGen空间是JVM用于存储类元数据和常量池的部分,不会被垃圾收集器自动...

    关于java内存泄露问题解决

    本文旨在深入探讨Java中的内存泄露问题及其解决方案。 #### Java的内存管理:GARBAGE COLLECTION详解 垃圾收集器(GC)在Java中扮演着至关重要的角色,它负责自动回收不再使用的对象所占用的内存。在Java中,所有...

    Java内存溢出解决办法

    解决方案是优化代码,减少递归或调整栈的大小(通过-Xss设置)。 3. **方法区/永久代溢出**: 方法区用于存储类信息、常量、静态变量等。JDK8之前,这部分内存被称为永久代,容易因类信息过多而溢出。JDK9之后,这...

    java内存泄露、溢出检查方法和工具

    本文将基于一个真实的案例,深入探讨Java内存溢出的原因分析、解决方案以及常用的检查方法和工具。 #### 二、Java内存管理基础知识 在理解内存泄露和溢出之前,我们需要先了解Java虚拟机(JVM)的基本内存结构及其...

    Java内存机制学习和使用

    - **解决方案**:针对不同的问题,可以采取相应的解决措施,如增加堆外内存大小、减少线程数量等。 #### 五、硬件限制与操作系统角色 在深入理解Java内存机制之前,我们需要了解底层硬件和操作系统的限制。 **...

    Java系统中内存泄漏测试方法的研究

    总结,Java系统中的内存泄漏测试和分析涉及多个方面,包括理解内存管理机制、识别泄漏原因、使用专业工具以及制定有效的解决方案。开发者应当养成良好的编程习惯,注重代码质量,以减少内存泄漏的发生。同时,利用...

    java 内存泄露

    Java内存泄露:诊断与解决方案详解 一、Java内存泄露概览 Java内存泄露是指在Java应用程序运行过程中,已经不再使用的对象或数据结构由于某种原因未能被垃圾回收器(Garbage Collector,GC)回收,导致系统内存...

    Java内存溢出问题

    解决方案可以是:限制应用的类加载数量,优化类的设计,避免大量动态生成的类;对于JDK8及以后版本,方法区已改为元空间(Metaspace),可以通过-XX:MaxMetaspaceSize来调整大小。 4. 本地方法栈溢出: 本地方法栈...

    JVM内存泄露解决之道

    本文将针对一个具体案例——浪潮烟草技术人员处理的广东烟草12月10日内存溢出事件,深入探讨Java虚拟机(JVM)中的Class类加载器内存泄露问题,并提出相应的解决方案。 #### 问题描述 在该事件中,技术人员发现了一...

    以线上实例来看,内存泄漏的图文解决方案.docx

    ### 内存泄漏图文解决方案详解 #### 一、引言 在软件开发过程中,内存泄漏是一种常见的性能问题,尤其在高并发环境下更为突出。本文将以一个具体的线上实例为背景,介绍如何逐步排查并解决内存泄漏问题。通过...

    Quest JProbe教程:Java内存分析示例(节选)

    - **内存分析**:帮助开发者发现和解决Java内存泄漏及对象循环问题,从而确保应用程序具有最佳的效率和稳定性。 - **性能分析**:帮助识别和解决Java应用程序中的性能瓶颈、死锁等问题,确保程序具有优秀的性能和可...

    推动现网局点的Java内存的修改

    在IT行业中,优化Java应用程序的性能是至关重要的,尤其是在运行大型系统时,内存管理成为了一个核心议题。"推动现网局点的Java内存的修改"这个主题,涉及到的是如何调整Java虚拟机(JVM)的内存配置以适应现网环境...

    GMSSL的java调用(JNI库和调用实例).zip

    总之,这个压缩包提供了一种在Java环境中利用GMSSL国密算法的解决方案,通过JNI库和Java调用示例,可以帮助开发者轻松地在自己的Java应用中集成SM2、SM3和SM4算法,满足我国对信息安全的特殊需求。

Global site tag (gtag.js) - Google Analytics