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 SoftReferencebufferRef;
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;
}
}
}
修复这种情况下的对象游离的方法是,当对象从堆栈弹出之后,就消除它的引用,如清单 3 中注释掉的行所示。但是这种情况 —— 由类管理其自己的内存 —— 是一种非常少见的情况,即显式地消除不再需要的对象是一个好主意。大部分时候,认为不应该使用的强行消除引用根本不会带来性能或内存使用方面的收益,通常是导致更差的性能或者 NullPointerException。该算法的一个链接实现不会存在这个问题。在链接实现中,链接节点(以及所存储的对象的引用)的生命期将被自动与对象存储在集合中的期间绑定在一起。弱引用可用于解决这个问题 —— 维护弱引用而不是强引用的一个数组 —— 但是在实际中,LeakyStack 管理它自己的内存,因此负责确保对不再需要的对象的引用被清除。使用数组来实现堆栈或缓冲区是一种优化,可以减少分配,但是会给实现者带来更大的负担,需要仔细地管理存储在数组中的引用的生命期。
相关推荐
"java管理windows系统内存_java释放内存缓存_java获得CPU使用率_系统内存_硬盘_进程源代码" 在Windows操作系统中,内存管理是一个非常重要的方面。Windows实现按需调页的虚拟内存机制,使得应用程序可以使用超过...
- 堆内存是所有线程共享的,由Java虚拟机自动管理。 - 堆内存的大小可以通过JVM参数来设置,如`-Xms`和`-Xmx`来指定初始大小和最大限制。 3. **数据段(Data Segment)**: - 包括静态域和常量池两部分。 - **...
Java内存管理是Java编程中的核心概念,它涉及到程序运行时数据的存储、分配以及回收。在Java中,内存主要分为堆内存(Heap)和栈内存(Stack),还有方法区(Method Area)、程序计数器(PC Register)以及本地方法...
在C++中,程序员需要手动管理内存,包括分配和释放,而在Java中,这一过程则由Java虚拟机(JVM)自动进行,通过垃圾收集(Garbage Collection, GC)来确保内存的有效使用。然而,这并不意味着Java程序员可以完全忽略...
Java内存管理是Java核心技术的重要组成部分,对于每个开发者来说,理解其工作原理都是十分必要的。这一主题既实用又有趣。以下是对Java内存管理的精彩概述,主要基于Sun Hotspot JVM,但请注意,不同JVM可能有不同的...
资源名称:Java内存管理机制相关资料汇总资源目录:【】java内存回收机制及预防【】java内存管理机制【】java内存管理白皮书【】Java虚拟机内存管理_对象和引用_空指针【】深入理解java虚拟机jvm高级行与最佳实践...
使用java来实现模拟的内存管理 有分配内存 回收内存等功能 有可视化效果 可以很清楚的看到内存的分配情况
Java虚拟机内存管理总结 Java虚拟机(JVM)中的内存管理是指Java语言中对象的分配和释放问题。Java中的内存管理可以分为两部分:对象的分配和释放。 对象的分配是由程序完成的,程序员需要通过关键字new为每个对象...
Java本身具有垃圾回收机制,能够自动管理内存,但在调用DLL时,由于这部分代码是运行在C/C++环境中,不遵循Java的内存管理规则。如果DLL中分配了内存但未正确释放,或者Java和DLL之间对对象的引用处理不当,都可能...
总的来说,这个项目为初学者提供了一个了解Java虚拟机内存管理的实践平台,通过研究源代码,可以深入理解JVM如何在JDK 6.2版本中管理内存,以及如何使用和配置这些内存区域,这对于提升Java开发技能非常有帮助。...
Java内存机制是Java虚拟机(JVM)的关键组成部分,它管理着程序运行时的数据存储。在Java中,内存主要分为以下几个区域: 1. **Heap(堆)**:这是Java中最主要的内存区域,用于存储所有的类实例和数组。当堆空间...
堆内存是Java中的全局共享区域,用于存储所有的对象实例和数组。当通过`new`关键字创建一个新的对象时,其实际的数据结构和属性值都在堆中分配。由于堆内存是所有线程共享的,因此对象实例的创建和访问需要考虑线程...
在Java 2平台的强大支持下,J2SE能够自动管理内存,这大大减轻了开发者的负担,让他们不必再进行复杂的显式内存管理工作。本文档旨在对Sun J2SE 5.0版本中的Java HotSpot虚拟机(JVM)内存管理机制进行全面概述,包括...
Java内存管理是Java编程中至关重要的一环,它涉及到程序的性能和稳定性。本文将深入探讨Java内存管理机制,包括垃圾回收、内存分配与释放、内存泄漏及其预防措施,以及四种引用类型的特点和应用场景。 首先,Java...
Java内存管理是开发Java应用程序时的关键环节,内存泄露和溢出问题可能导致系统性能下降,甚至导致服务崩溃。本文将深入探讨如何检测和分析Java内存泄露与溢出,并介绍一种常用的工具——Memory Analyzer(MAT)。 ...
本笔记主要涵盖了四个核心知识点:Java反射机制、流(I/O流)、内存管理和Java学习的基础。以下是对这些主题的详细阐述: 一、Java反射机制 Java反射机制是Java语言的一个强大特性,允许程序在运行时检查类、接口、...
堆内存是Java虚拟机(JVM)用于存储所有对象实例的地方。当一个对象被创建时,其数据将被分配到堆内存中。堆内存的特点是动态分配,即对象的大小在创建时不确定,且对象的生命周期也较难预测。由于这些特性,堆内存的...