JAVA中堆内存空间管理问题探讨

简介: 摘要:Java堆是一个运行时的数据区,对象从中分配空间,但是空间的容量是有限的。在堆内存空间中有“有用信息”,也有“无用信息”, “无用信息”占据着内存空间,降低了内存的使用效率。

摘要:Java堆是一个运行时的数据区,对象从中分配空间,但是空间的容量是有限的。在堆内存空间中有“有用信息”,也有“无用信息”, “无用信息”占据着内存空间,降低了内存的使用效率。因此,我们必须使用某种算法将无用的信息从内存中清除,并把有用的信息重组,放在内存的一端,那么另一端则变成连续的空闲区,可以用来存放有用的信息。内存空间回收的作用是重大的。Java中堆内存的管理便达到了以上的要求。充分理解Java堆内存的特点,可以更有效的让我们利用资源。

关键词:堆内存,垃圾收集,算法,特点

 

 

第一章            堆内存管理的作用

 

程序的指令和数据只有存放在处理机能直接访问的内存中,这部分程序才能被执行,而计算机的内存容量总是有限的。面对着规模越来越大的计算机软件,内存容量不足的矛盾越来越尖锐。增加内存容量,可以在一定程度上解决这个问题但要从根本上解决这个问题,我们不应当从硬件着手,硬件“只能治标而不能治本”。处理机在执行某段程序时,那么处理机以前执行的程序便是“无用信息”,但这些“无用信息”仍占据着内存,以至于这些内存成了垃圾,无法存放其他的“有用信息”,那么对内存管理的任务是重大的,将堆内存中“无用信息”丢弃,把它的内存回收,用来存放其他的有用的信息。于是堆内存中的垃圾收集器便发挥了重大的作用,也正是因为垃圾收集完成了对堆内存的管理。我们所说的堆内存对空间的管理,也就是说堆内存中垃圾收集这一机制对堆内存空间的管理,实现内存空间的动态平衡。所谓“垃圾收集”,即是收集那些被“无用信息”占据的内存,以此来提高内存的使用效率。

在Java中,当没有对象引用指向原先分配给某个对象的内存时,JVM的一个系统线程便会自动释放该内存块,内存回收它所占用的空间,以便给后来新的对象使用。这就意味着程序不再需要的对象是“无用信息”,就应当被丢弃。在C++中,对象所占的内存在程序结束运行之前一直被占用,不管是“有用信息”还是“无用信息”,在没有明确释放之前是不可以分配给其他对象的,这样就给内存增加了负担。关于C++对象释放和内存回收问题将会在后文介绍。

容易理解,对象释放后,内存回收并不是连续的,必然会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。垃圾收集机制则可以清除内存记录碎片,它将所占用的堆内存存移到堆的一端,于是在堆的另一端便是连续的内存空间,用来分配给新的对象。

内存空间的释放从根本上解决了内存不足的问题,这样就提高了内存的使用效率和编程效率,保护了程序的完整性。

每一个先进的机制都有自己的缺点。堆内存管理中垃圾收集机制的一个潜在的缺点是它的开销影响程序的性能。Java虚拟机必须追踪运行程序中的有用对象,释放无用对象,这些都要经过处理机的处理,都要花费处理机的时间,而且垃圾收集机制也不可能100%收集到所有的废弃内存,这也正是它的不完备性。但是随着现代技术的发展,垃圾收集算法的改进,这些问题都将会得到解决。

 

 

第二章            堆内存管理中垃圾收集算法的种类

 

   在介绍垃圾收集算法前,先介绍一个概念:根集,所谓根集就是正在执行的Java程序可以访问的引用变量的集合,这个集合中包含了局部变量、参数以及类变量等等。垃圾收集算法要确定从根开始哪些是可达的,哪些是不可达的。从根开始可达的(包括间接可达)是活动对象,它们拒绝被释放,不能作为垃圾被回收;从根开始通过任意路径皆不可达的对象将被释放,作为垃圾而被回收。下面介绍集中常见的算法。

2.1 引用计数法(Reference Counting Collector)

引用计数法是唯一一个没有使用根集的垃圾收集算法,它使用引用计数器的计数来区分存活对象和不再使用的对象。每一次创建一个对象并赋给一个变量时,引用计数器为1,当对象赋给任意变量时,引用计数器加1,当对象出了作用域,引用计数器减1,一旦为0,对象就要被释放,它所占的内存将要被回收。引用计数器在加1和减1时,增加了程序执行的开销。

2.2 tracing算法(Tracing Collector)

tracing算法是为了解决引用计数器法的问题而提出的,它使用了根集的概念。它从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式对可达的对象进行标记。例如对每一个可达的对象设置一个或多个位。在扫描识别的过程中,tracing算法的垃圾收集也称为标记和清除(mak-and-sweep)垃圾收集器。

2.3 compacting算法(Compacting Collector)

内存回收的过程中必然会出现碎片,compacting算法正是为了解决这一问题而提出的。它在清除的过程中,将所有的对象移到堆的一端,堆的另一端就变成了连续的空闲区,收集器会对它移动的所有对象的所有引用进行刷新,使得这些引用在新的位置上能识别原来的对象,保证原有程序的完整性,为了compacting算法的收集器的实现,一般增加句柄和句柄表,这也增加了程序的开销。

2.4 coping算法(Coping Collector)

为了解决compacting算法中增加句柄和句柄表给程序带来更大的开销问题,于是提出了coping算法,不仅如此coping算法还解决了堆碎片的垃圾回收问题。它把堆分成一个对象面和多个空闲面,为对象分配空间。当对象满了,coping算法的垃圾收集从根集中扫描可达对象,并将每个可达对象复制到空闲面,这样使得对象所占内存之间没有空闲洞。空闲面也就变成了对象面,原来的对象面也就变成了空闲面,程序会在新的对象面中分配内存。

Stop-and-copy算法是一种典型的coping算法,它将堆分成对象面和空闲区域,在对象面与空闲区域面的切换过程中,程序会暂停执行。

2.5 generation算法(Generational Collector)

Stop-and-copy垃圾收集器必须复制所有的可达对象,这也无形的增加了程序的等待时间,coping算法的低效性也正是如此。程序设计中有这样的规律:多数对象存在的时间比较短,少数的对象存在时间比较长。Generation算法也正是根据这一规律,它将堆分成两个或多个,每个子堆作为对象的一代。由于多数对象存在的时间比较短,垃圾收集器将从做年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行没有被释放的对象移到下一最高代的子堆中,由于老一代的子堆不会经常被回收,因而节省了时间。

2.6 adaptive算法(Adaptive Collector)

特定的垃圾收集算法在特定的环境中会发挥更好的效果。Adaptive算法的垃圾收集器就是监视当前堆的使用情况,并选择合适的垃圾收集器。

由以上的种种算法可知,任何一种垃圾收集算法并没有被定性为解决某种内存回收问题,但是它们都是在解决这样的事情;

(1)发现无用的信息对象;

(2)回收被无用对象占用的内存空间,使该空间可以被其它程序再次使用。

所有的垃圾收集算法都是为了让不可达的对象被释放,让它们堆内存变得“干净”而更有效。

 

第三章            透视Java垃圾收集的运行

 

3.1命令行参数透视垃圾收集器的运行

命令行参数-verbosegc 可以查看Java的堆内存使用情况,堆内存也因此在用户面前透明化了,它的格式如下

java -verbosegc classfile

垃圾收集的算法很多,但使用System.gc()可以不管JVM使用的某种算法,将Java的无用对象释放,垃圾回收。通过下面的例子说明这一点:

class TestGC { public static void main(String[] args) { new TestGC(); System.gc(); System.runFinalization(); } }

可以看出,此例的目的是建立一个新的对象,它的classfile为TestGC,并且回收垃圾。程序编译后执行java -verbosegc TestGC ,查看堆内存使用情况。假设结果为:[Full GC 168K->97K(1984K), 0.0253873 secs]

则说明堆内存容量为1984K,168K和97K分别表示垃圾收集GC前后所有存活对象使用的内存容量,说明有168K—97K=71K的对象的内存被回收了,收集所需要的时间为0.0253873秒。

3.2 finalize方法透视垃圾收集器的运行

JVM在提供很多垃圾收集算法的同时还提供了缺省机制来释放资源,这个方法就是finalize(),原型为:

protected void finalize() throws Throwable

在finalize()方法返回之后,对象被释放,垃圾收集开始执行。原型中的throws Throwable表示可以抛开任何异常类型,强制的回收。

finalize()是从Java里调用非Java方法的一种方式,并且通过它分配内存做一些具有C风格的事情,这里主要通过“固有方法”来进行。目前C和C++是唯一获得固有方法支持的语言,它们能够调用通过其他编程语言编写的子程序,故finalize()可以有效的调用任何东西。但是其他语言编写的程序所占的空间不能自动被释放,比如说调用C中的malloc()函数分配存储空间,在程序运行完毕后,就必须用free()函数释放该空间,从而造成内存“漏洞”的出现,所以说我们不能过多的使用finalize()。

一般地清除过程中,要清除一个对象,必须在清除地点调用一个清除方法,这与C++中的“破坏器”相对应,但是在C++中所有的对象都回被破坏。若在C++中创建一个本地对象,比如在堆栈中创建(在Java中是不可能的),那么清除或破坏工作就会在“结束花括号”所代表的创建这个对象的作用域的末尾进行;若对象是用new创建的(类似于Java),那么程序员就必须通过调用C++中的delete命令(Java中没有此命令)进行清除,同时也就调用了相应的破坏器。如果程序员没有调用delete命令,那么永远不会调用破坏器,对象的其他部分将不可能被清除,而且还会出现内存“漏洞”。

Java中所有的对象必须用new进行创建,删除对象则不必使用delete命令,因为垃圾收集机制会自动完成这项工作。将Java与C++在删除对象时作简单的比较,我们可以看出Java中的垃圾收集机制代替了C++中的破坏器,但是随着学习的深入,垃圾收集机制并不能完全的消除对破坏器(或以破坏器为代表的那种机制)的需要。

 

第四章            垃圾收集的特点和注意事项

 

4.1 垃圾收集的特点

通过以上分析,我们可以知道垃圾收集对堆内存的管理也正是堆内存对空间的管理,它们是同一的概念。垃圾收集有以下几个特点:

(1)垃圾收集的不可预知性:JVM中有很多垃圾收集算法,提供了不同的垃圾收集机制。它们有可能定时发生,有可能在CPU空闲时发生,也有可能在内存出现极限时发生,这些都与垃圾收集器的选择和设置有关。

(2)垃圾收集的精确性:垃圾收集器可以精确的标记出可达的对象,这是完全回收所有废弃对象的前提,这样很容易造成内存泄露;不仅如此,垃圾收集器也能够精确的定位对象之间的引用关系,这就为归并和复制算法提供了前提条件,这样所有的不可达对象都能够可靠的回收,所有可达对象被复制,重新分配,放在连续的内存空间,这样就能有效德尔防止内存地址的不连续。

(3) 垃圾收集器的种类各异,他们的算法也不一样,有只允许垃圾收集器运行的,也有允许垃圾收集器和应用程序同时运行的,还有通一时间垃圾收集多线程运行的。

(4)不同的JVM可能采用不同的垃圾收集,因此垃圾收集与具体的JVM以及JVM的内存模型有非常密切的关系。

(5)随着技术的发展,现代垃圾收集技术提供了许多可选的垃圾收集器,而且在配置每种收集器的时候又可以设置不同的参数,这就使得根据不同的应用环境获得最优的应用性能成为可能。

4.2 垃圾收集的注意事项

根据以上特点,我们在使用垃圾收集时应注意以下事项:

(1)不可以假定垃圾收集发生的时间,因为垃圾收集随时可能发生,是一个未知数。

(2)Java垃圾收集的方法也是一个不确定的数,因为所有的垃圾收集方法只不过是向JVM发出这样一个申请,会不会去执行,我们并不知道,包括强行执行的垃圾收集方法——调用system.gc()。

(3)挑选适合自己的垃圾收集器,一般说来,如果系统没有特殊和苛刻的性能要求,可以采用JVM缺省选项。

(4)要有良好的编程习惯和严谨的编程态度,不要在编程时发生错误而导致内存泄露。

(5)尽早释放无用对象的引用,以免占据内存,提高内存的使用效率。

总之,不管使用这样的垃圾收集算法和怎样的垃圾收集机制,我们的目的都是唯一的,让有用的信息进入内存,让无用的信息离开内存,让内存保持在动态中,而且让内存有一个连续的空闲区,这样就提高了内存的使用效率。JVM中堆内存的分配和垃圾的处理,可以让我们更有效的利用资源,我们有必要理解Java的这一特性。

 

目录
相关文章
|
26天前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
2月前
|
缓存 easyexcel Java
Java EasyExcel 导出报内存溢出如何解决
大家好,我是V哥。使用EasyExcel进行大数据量导出时容易导致内存溢出,特别是在导出百万级别的数据时。以下是V哥整理的解决该问题的一些常见方法,包括分批写入、设置合适的JVM内存、减少数据对象的复杂性、关闭自动列宽设置、使用Stream导出以及选择合适的数据导出工具。此外,还介绍了使用Apache POI的SXSSFWorkbook实现百万级别数据量的导出案例,帮助大家更好地应对大数据导出的挑战。欢迎一起讨论!
174 1
|
2月前
|
存储 监控 算法
Java中的内存管理:理解Garbage Collection机制
本文将深入探讨Java编程语言中的内存管理,着重介绍垃圾回收(Garbage Collection, GC)机制。通过阐述GC的工作原理、常见算法及其在Java中的应用,帮助读者提高程序的性能和稳定性。我们将从基本原理出发,逐步深入到调优实践,为开发者提供一套系统的理解和优化Java应用中内存管理的方法。
|
1月前
|
安全 Java 编译器
Java对象一定分配在堆上吗?
本文探讨了Java对象的内存分配问题,重点介绍了JVM的逃逸分析技术及其优化策略。逃逸分析能判断对象是否会在作用域外被访问,从而决定对象是否需要分配到堆上。文章详细讲解了栈上分配、标量替换和同步消除三种优化策略,并通过示例代码说明了这些技术的应用场景。
Java对象一定分配在堆上吗?
|
7天前
|
Java
java内存区域
1)栈内存:保存所有的对象名称 2)堆内存:保存每个对象的具体属性 3)全局数据区:保存static类型的属性 4)全局代码区:保存所有的方法定义
17 1
|
21天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
40 6
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
72 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
25天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
37 2
|
26天前
|
存储 安全 Java
什么是 Java 的内存模型?
Java内存模型(Java Memory Model, JMM)是Java虚拟机(JVM)规范的一部分,它定义了一套规则,用于指导Java程序中变量的访问和内存交互方式。
61 1
|
1月前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
32 1