`

[转载]高性能JAVA开发之内存管理

阅读更多
这几天在找一个程序的bug,主要是java虚拟机内存溢出的问题,调研了一些java内存管理的资料,现整理如下:

一、JVM中的对象生命周期

对象的生命周期一般分为7个阶段:创建阶段,应用阶段,不可视阶段,不可到达阶段,可收集阶段,终结阶段,释放阶段。

创建阶段,首先大家看一下,如下两段代码:

test1:

for( int i=0; i<10000; i++)

Object obj=new Object();

test2:

Object obj=null;

for( int i=0; i<10000; i++)

obj=new Object();

这两段代码都是相同的功能,但是显然test2的性能要比test1性能要好,内存使用率要高,这是为什么呢?原因很简单,test1每次执行for循环都要创建一个Object的临时对象,但是这些临时对象由于JVM的GC不能马上销毁,所以他们还要存在很长时间,而test2则只是在内存中保存一份对象的引用,而不必创建大量新临时变量.从而降低了内存的使用.

另外不要对同一个对象初始化多次.例如:

public class A{

private Hashtable table = new Hashtable();

public A(){

table = new Hashtable(); // 这里应该去掉,因为table已经被初始化.

}

来源:(http://blog.sina.com.cn/s/blog_5cdbcf3e0100c72w.html) - 高性能JAVA开发之内存管理(一)_Kevin_新浪博客
}

这样就new了两个Hashtable,但是却只使用了一个.另外一个则没有被引用.而被忽略掉.浪费了内存.并且由于进行了两次new操作.也影响了代码的执行速度。

应用阶段:即该对象至少有一个引用在维护他。

不可视阶段:即超出该变量的作用域.这里有一个很好的做法,因为JVM在GC的时候并不是马上进行回收,而是要判断对象是否被其他引用在维护.所以,这个时候如果我们在使用完一个对象以后对其obj=null或者obj.doSomething()操作,将其标记为空,可以帮助JVM及时发现这个垃圾对象。

不可到达阶段:就是在JVM中找不到对该对象的直接或者间接的引用。

可收集阶段,终结阶段,释放阶段:此为回收器发现该对象不可到达,finalize方法已经被执行,或者对象空间已被重用的时候。

二、java内存管理特点

Java一个最大的优点就是取消了指针,由垃圾收集器来自动管理内存的回收。程序员不需要通过调用函数来释放内存。

(1)Java的内存管理就是对象的分配和释放问题。

在Java中,内存的分配是由程序完成的,而内存的释放是有GC完成的,这种收支两条线的方法简化了程序员的工作。但也加重了JVM的工作。这也是Java程序运行速度较慢的原因之一。

在Java中,程序员需要通过关键字new为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间,对象的释放是由GC决定和执行的。

GC释放空间方法:监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。当该对象不再被引用时,释放对象,但程序中对GC的操作并不一定能达到管理内存的效果,GC对于程序员来说基本是透明的,不可见的。我们只有几个函数可以访问GC,例如运行GC的函数System.gc()。但是根据Java语言规范定义,System.gc()函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低,而且强制内存回收对于系统自动的内存回收机制会产生负面影响,会加大系统自动回收的处理时间,所以应该尽量避免显式使用System.gc(),

JVM调用GC的策略有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。

(2)内存管理结构

Java使用有向图的方式进行内存管理,对于程序运行的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。

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

(3)使用有向图方式管理内存的优缺点

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

三、Java的内存泄露

Java虽然由GC来回收内存,但也是存在泄露问题的,只是比C++小一点。

(1)与C++的比较

C++所有对象的分配和回收都需要由用户来管理。即需要管理点,也需要管理边。若存在不可达的点,无法在回收分配给那个点的内存,导致内存泄露。存在无用的对象引用,自然也会导致内存泄露。

Java由GC来管理内存回收,GC将回收不可达的对象占用的内存空间。所以,Java需要考虑的内存泄露问题主要是那些被引用但无用的对象——即指要管理边就可以。被引用但无用的对象,程序引用了该对象,但后续不会再使用它。它占用的内存空间就浪费了,如果存在对象的引用,这个对象就被定义为“活动的”,同时不会被释放。

(2)Java内存泄露处理

处理Java的内存泄露问题:确认该对象不再会被使用,接着典型的做法——把对象数据成员设为null

注意,当局部变量不需要时,不需明显的设为null,因为一个方法执行完毕时,这些引用会自动被清理。

例子:

List myList=new ArrayList();

for (int i=1;i<100; i++)

{

Object o=new Object();

myList.add(o);

o=null;

}

此时,所有的Object对象都没有被释放,因为变量myList引用这些对象。当myList后来不再用到,将之设为null,释放所有它引用的对象。之后GC便会回收这些对象占用的内存。

(3)内存泄露检测

市场上已有几种专业检查Java内存泄漏的工具,它们的基本工作原理大同小异,都是通过监测Java程序运行时,所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员将根据这些信息判断程序是否有内存泄漏问题。这些工具包括Optimizeit Profiler,JProbe Profiler,JinSight, Rational公司的Purify等。

在运行过程中,我们可以随时观察内存的使用情况,通过这种方式,我们可以很快找到那些长期不被释放,并且不再使用的对象。我们通过检查这些对象的生存周期,确认其是否为内存泄露。

四、java程序设计中有关内存管理的经验

1. 最基本的建议是尽早释放无用对象的引用。如:...

A a = new A();

//应用a对象

a = null; //当使用对象a之后主动将其设置为空

….

注:如果a 是方法的返回值,不要做这样的处理,否则你从该方法中得到的返回值永远为空,而且这种错误不易被发现、排除

2. 尽量少用finalize函数。它会加大GC的工作量。

3. 注意集合数据类型,包括数组、树、图、链表等数据结构,这些数据结构对GC来说,回收更为复杂。

4. 尽量避免在类的默认构造器中创建、初始化大量的对象,防止在调用其自类的构造器时造成不必要的内

存资源浪费。由于对象的创建是递归式的,也就是先调用超级类的构造,然后依次向下递归调用构造函数,

来源:(http://blog.sina.com.cn/s/blog_5cdbcf3e0100c734.html) - 高性能JAVA开发之内存管理(二)_Kevin_新浪博客
所以应该避免在类的构造函数中初始化变量,这样可以避免不必要的创建对象造成不必要的内存消耗.当

然这里也就看出来接口的优势。

5. 尽量避免强制系统做垃圾内存的回收,增长系统做垃圾回收的最终时间

6. 尽量避免显式申请数组空间

7. 别用new Boolean()

在很多场景中Boolean类型是必须的,比如JDBC中boolean类型的set与get都是通过Boolean封装传递的,大部分ORM也是用Boolean来封装boolean类型的,比如:

ps.setBoolean("isClosed",new Boolean(true));

ps.setBoolean("isClosed",new Boolean(isClosed));

ps.setBoolean("isClosed",new Boolean(i==3));

通常这些系统中构造的Boolean实例的个数是相当多的,所以系统中充满了大量Boolean实例小对象,这是相当消耗内存的。Boolean类实际上只要两个实例就够了,一个true的实例,一个false的实例。

Boolean类提供两了个静态变量:

public static final Boolean TRUE = new Boolean(true);

public static final Boolean FALSE = new Boolean(false);

需要的时候只要取这两个变量就可以了,

比如:ps.setBoolean("isClosed",Boolean.TRUE);

那么象2、3句那样要根据一个boolean变量来创建一个Boolean怎么办呢?可以使用Boolean提供的静态方法:Boolean.valueOf()

比如:

ps.setBoolean("isClosed",Boolean.valueOf(isClosed));

ps.setBoolean("isClosed",Boolean.valueOf(i==3));

因为valueOf的内部实现是:return (b ? TRUE : FALSE);

所以可以节省大量内存。相信如果Java规范直接把Boolean的构造函数规定成private,就再也不会出现这种情况了。

8. 别用new Integer

和Boolean类似,java开发中使用Integer封装int的场合也非常多,并且通常用int表示的数值通常都非常小。SUN SDK中对Integer的实例化进行了优化,Integer类缓存了-128到127这256个状态的Integer,如果使用Integer.valueOf(int i),传入的int范围正好在此内,就返回静态实例。这样如果我们使用Integer.valueOf代替new Integer的话也将大大降低内存的占用。如果您的系统要在不同的SDK(比如IBM SDK)中使用的话,那么可以自己做了工具类封装一下,比如IntegerUtils.valueOf(),这样就可以在任何SDK中都可以使用这种特性。

9. 不要用StringBuffer代替字符串相加

10. 不要过滥使用哈希表

有一定开发经验的开发人员经常会使用hash表(hash表在JDK中的一个实现就是HashMap)来缓存一些数据,从而提高系统的运行速度。比如使用HashMap缓存一些物料信息、人员信息等基础资料,这在提高系统速度的同时也加大了系统的内存占用,特别是当缓存的资料比较多的时候。其实我们可以使用操作系统中的缓存的概念来解决这个问题,也就是给被缓存的分配一个一定大小的缓存容器,按照一定的算法淘汰不需要继续缓存的对象,这样一方面会因为进行了对象缓存而提高了系统的运行效率,同时由于缓存容器不是无限制扩大,从而也减少了系统的内存占用。现在有很多开源的缓存实现项目,比如ehcache、oscache等,这些项目都实现了FIFO、MRU等常见的缓存算法。

11. 避免过深的类层次结构和过深的方法调用。因为这两者都是非常占用内存的(特别是方法调用更是堆栈空间的消耗大户)。

12. 变量只有在用到它的时候才定义和实例化。

13. 共享静态存储空间

我们都知道静态变量在程序运行期间其内存是共享的,因此有时候为了节约内存工件,将一些变量声明为静态变量确实可以起到节约内存空间的作用。但是由于静态变量生命周期很长,不易被系统回收,所以使用静态变量要合理,不能盲目的使用.以免适得其反。

因此建议在下面情况下使用:变量所包含的对象体积较大,占用内存过多;变量所包含对象生命周期较长;变量所包含数据稳定;该类的对象实例有对该变量所包含的对象的共享需求.(也就是说是否需要作为全局变量)。
分享到:
评论

相关推荐

    Java 9 High Performance

    而Java 9作为Java语言的第九个主要版本,于2017年9月21日发布,它不仅包含了语言上的新特性,还包含了JDK(Java开发工具包)的新模块系统,从而大幅提升了应用程序的性能和可维护性。 书名《Java 9 High ...

    Java 最常见 200+ 面试题全解析:面试必备.pdf

    15. Kafka:作为高性能的分布式消息系统,Kafka用于构建实时的数据管道和流应用程序,了解其生产者和消费者模型。 16. Zookeeper:分布式协调服务,主要管理分布式环境中的数据,如配置信息、命名服务、分布式锁等...

    深入分析 Java I/O 的工作机制(转载)

    深入理解Java I/O的工作机制对于开发高效、可靠的系统至关重要。以下是对Java I/O机制的详细分析: 1. **I/O 流的概念** Java中的I/O操作基于流的概念,流是数据的有序传输通道。Java将所有的I/O操作抽象为流对象...

    palm650用的JAVA VM

    "J9"可能是指IBM的J9 Java虚拟机实现,这是一种开源、高性能的Java虚拟机。"Midp20"则表示这个版本的Java VM支持MIDP(Mobile Information Device Profile)2.0规范,这是Java ME(Micro Edition)的一个子集,专为...

    Java面试资料大集合

    Java是世界上最流行的编程语言之一,尤其在企业级应用开发领域占据主导地位。为了在激烈的求职竞争中脱颖而出,理解和掌握Java的面试知识点至关重要。...同时,持续学习和实践,将使您在Java开发领域更加游刃有余。

    JNA—JNI终结者(转载)

    然而,如果需要高性能或者对本地库有深度定制的需求,JNI仍然是必要的选择。 文章可能会比较JNA和JNI在实际项目中的应用案例,解释为何在某些情况下JNA能成为JNI的终结者。此外,可能还会涵盖JNA的局限性、性能比较...

    jdk常用命令

    在IT行业中,Java Development Kit(JDK)是Java编程的...通过阅读《JDK常用命令详解.doc》和《jdk-bin下的命令详解---转载,学习下.txt》,你可以更深入地学习每个命令的用法和具体示例,从而提升你的Java开发技能。

    [转载]hotspot源码(JDK7)

    Hotspot是Oracle JDK中的一个关键组件,它是一个高性能的Java虚拟机(JVM)。Hotspot的名字源于其核心理念:在运行时识别出程序的“热点”代码,然后对其进行优化,以提供最佳的执行效率。本文将深入探讨JDK7版本的...

    jdbc与hibernate的优缺点比较(转载的精髓).pdf

    综上所述,JDBC更适合于对性能要求高且数据库操作相对简单的场景,而Hibernate则适用于需要快速开发、对象关系映射和事务管理的复杂应用。在选择使用哪种技术时,需要根据项目需求、团队技能和性能要求来平衡。在...

    使用open source产品组装你的web应用架构(转载)

    1. Nginx:作为一款高性能的HTTP和反向代理服务器,Nginx以其轻量级、高并发的特点被广泛应用。它能有效地处理静态资源,同时通过反向代理和负载均衡功能分发请求到不同的后端服务。 2. Apache:另一个常见的Web...

    Tomcat 7-9转载资源

    《深入理解Tomcat 7-9:从基础到实践》 ...通过深入研究这些版本的Tomcat,开发者不仅可以掌握服务器的基本操作,还能深入了解Java Web应用的运行机制,为开发和维护高质量的Web应用打下坚实的基础。

    服务器系统平台技术规格书收集.pdf

    * 主机一和主机二为并行存取操作数据库方式,并结合硬件HA系统,实现并行计算,提高转载、查询、备份、维护的时间性能,突破主机内存和系统体系结构 * 主机二采用分区技术,划分一个分区作为逻辑主机三,提供应用...

    Python(面向对象编程语言) v3.4.0.zip

    它合理地结合了高性能与使得编写程序简单有趣的特色。 Python 3.4.0 发布,此版本包括一系列 3.x 系列的改进,数百个小的改进和 bug 修复。 新语法特性: Python 3.4 并未添加任何新的语法特性

    spark使用案例------

    Spark提供了多种API,包括Scala、Java、Python和R,使得开发人员可以方便地进行大规模数据处理。在这个“Spark使用案例”中,我们将探讨Spark在大数据分析中的应用,以及如何利用它进行高效的数据处理。 首先,...

    springmybatis

    mybatis实战教程mybatis in action之一开发环境搭建 mybatis实战教程mybatis in action之二以接口的方式编程 mybatis实战教程mybatis in action之三实现数据的增删改查 mybatis实战教程mybatis in action之四实现...

    二十三种设计模式【PDF版】

    提供 Java运行性能,降低小而大量重复的类的开销. C. 行为模式 设计模式之 Command(命令) 什么是将行为封装,Command 是最好的说明. 设计模式之 Observer(观察者) 介绍如何使用 Java API 提供的现成 Observer ...

Global site tag (gtag.js) - Google Analytics