`

关注性能: 引用对象(对象引用是怎样严重影响垃圾收集器的)

阅读更多

如果您认为 Java 游戏开发人员是 Java 编程世界的一级方程式赛车手,那么您就会明白为什么他们会如此地重视程序的性能。 游戏开发人员几乎每天都要面对的性能问题,往往超过了一般程序员考虑问题的范围。哪里可以找到这些特殊的开发人员呢?Java 游戏社区就是一个好去处(参见 参考资料)。 虽然在这个站点可能没有很多关于服务器端的应用,但是我们依然可以从中受益,看看这些“惜比特如金”的游戏开发人员每天所面对的,我们往往能从中得到宝贵的经验。让我们开始游戏吧!

对象泄漏


游戏程序员跟其他程序员一样――他们也需要理解 Java 运行时环境的一些微妙之处,比如垃圾收集。垃圾收集可能是使您感到难于理解的较难的概念之一, 因为它并不能总是毫无遗漏地解决 Java 运行时环境中堆管理的问题。似乎有很多类似这样的讨论,它的开头或结尾写着:“我的问题是关于垃圾收集”。

假如您正面遭遇内存耗尽(out-of-memory)的错误。于是您使用检测工具想要找到问题所在,但这是徒劳的。您很容易想到另外一个比较可信的原因:这是 Java 虚拟机堆管理的问题,而不会认为这是您自己的程序的缘故。但是,正如 Java 游戏社区的资深专家不止一次地解释的,Java 虚拟机并不存在任何被证实的对象泄漏问题。实践证明,垃圾收集器一般能够精确地判断哪些对象可被收集,并且重新收回它们的内存空间给 Java 虚拟机。所以,如果您遇到了内存耗尽的错误,那么这完全可能是由您的程序造成的,也就是说您的程序中存在着“无意识的对象保留(unintentional object retention)”。

内存泄漏与无意识的对象保留


内存泄漏和无意识的对象保留的区别是什么呢?对于用 Java 语言编写的程序来说,确实没有区别。两者都是指在您的程序中存在一些对象引用,但实际上您并不需要引用这些对象。一个典型的例子是向一个集合中加入一些对象以便以后使用它们,但是您却忘了在使用完以后从集合中删除这些对象。因为集合可以无限制地扩大,并且从来不会变小,所以当您在集合中加入了太多的对象(或者是有很多的对象被集合中的元素所引用)时,您就会因为堆的空间被填满而导致内存耗尽的错误。垃圾收集器不能收集这些您认为已经用完的对象,因为对于垃圾收集器来说,应用程序仍然可以通过这个集合在任何时候访问这些对象,所以这些对象是不可能被当作垃圾的。

对于没有垃圾收集的语言来说,例如 C++ ,内存泄漏和无意识的对象保留是有区别的。C++ 程序跟 Java 程序一样,可能产生无意识的对象保留。但是 C++ 程序中存在真正的内存泄漏,即应用程序无法访问一些对象以至于被这些对象使用的内存无法释放且返还给系统。令人欣慰的是,在 Java 程序中,这种内存泄漏是不可能出现的。所以,我们更喜欢用“无意识的对象保留”来表示这个令 Java 程序员抓破头皮的内存问题。这样,我们就能区别于其他使用没有垃圾收集语言的程序员。

跟踪被保留的对象

那么当发现了无意识的对象保留该怎么办呢?首先,需要确定哪些对象是被无意保留的,并且需要找到究竟是哪些对象在引用它们。然后必须安排好 应该在哪里释放它们。最容易的方法是使用能够对堆产生快照的检测工具来标识这些对象,比较堆的快照中对象的数目,跟踪这些对象,找到引用这些对象的对象,然后强制进行垃圾收集。有了这样一个检测器,接下来的工作相对而言就比较简单了:

    * 等待直到系统达到一个稳定的状态,这个状态下大多数新产生的对象都是暂时的,符合被收集的条件;这种状态一般在程序所有的初始化工作都完成了之后。
    * 强制进行一次垃圾收集,并且对此时的堆做一份对象快照。
    * 进行任何可以产生无意地保留的对象的操作。
    * 再强制进行一次垃圾收集,然后对系统堆中的对象做第二次对象快照。
    * 比较两次快照,看看哪些对象的被引用数量比第一次快照时增加了。因为您在快照之前强制进行了垃圾收集,那么剩下的对象都应该是被应用程序所引用的对象,并且通过比较两次快照我们可以准确地找出那些被程序保留的、新产生的对象。
    * 根据您对应用程序本身的理解,并且根据对两次快照的比较,判断出哪些对象是被无意保留的。
    * 跟踪这些对象的引用链,找出究竟是哪些对象在引用这些无意地保留的对象,直到您找到了那个根对象,它就是产生问题的根源。





显式地赋空(nulling)变量

一谈到垃圾收集这个主题,总会涉及到这样一个吸引人的讨论,即显式地赋空变量是否有助于程序的性能。赋空变量是指简单地将 null 值显式地赋值给这个变量,相对于让该变量的引用失去其作用域。

清单 1. 局部作用域

public static String scopingExample(String string) {
  StringBuffer sb = new StringBuffer();
  sb.append("hello ").append(string);
  sb.append(", nice to see you!");
  return sb.toString();
}


当该方法执行时,运行时栈保留了一个对 StringBuffer 对象的引用,这个对象是在程序的第一行产生的。在这个方法的整个执行期间,栈保存的这个对象引用将会防止该对象被当作垃圾。当这个方法执行完毕,变量 sb 也就失去了它的作用域,相应地运行时栈就会删除对该 StringBuffer 对象的引用。于是不再有对该 StringBuffer 对象的引用,现在它就可以被当作垃圾收集了。栈删除引用的操作就等于在该方法结束时将 null 值赋给变量 sb。

错误的作用域

既然 Java 虚拟机可以执行等价于赋空的操作,那么显式地赋空变量还有什么用呢?对于在正确的作用域中的变量来说,显式地赋空变量的确没用。但是让我们来看看另外一个版本的 scopingExample 方法,这一次我们将把变量 sb 放在一个错误的作用域中。

清单 2. 静态作用域

static StringBuffer sb = new StringBuffer();
public static String scopingExample(String string) {
  sb = new StringBuffer();
  sb.append("hello ").append(string);
  sb.append(", nice to see you!");
  return sb.toString();
}


现在 sb 是一个静态变量,所以只要它所在的类还装载在 Java 虚拟机中,它也将一直存在。该方法执行一次,一个新的 StringBuffer 将被创建并且被 sb 变量引用。在这种情况下,sb 变量以前引用的 StringBuffer 对象将会死亡,成为垃圾收集的对象。也就是说,这个死亡的 StringBuffer 对象被程序保留的时间比它实际需要保留的时间长得多――如果再也没有对该 scopingExample 方法的调用,它将会永远保留下去。

一个有问题的例子

即使如此,显式地赋空变量能够提高性能吗?我们会发现我们很难相信一个对象会或多或少对程序的性能产生很大影响,直到我看到了一个在 Java Games 的 Sun 工程师给出的一个例子,这个例子包含了一个不幸的大型对象。

清单 3. 仍在静态作用域中的对象

private static Object bigObject;
public static void test(int size) {
  long startTime = System.currentTimeMillis();
  long numObjects = 0;
  while (true) {
    //bigObject = null; //explicit nulling
    //SizableObject could simply be a large array, e.g. byte[]
    //In the JavaGaming discussion it was a BufferedImage
    bigObject = new SizableObject(size);
    long endTime = System.currentTimeMillis();
    ++numObjects;
    // We print stats for every two seconds
    if (endTime - startTime >= 2000) {
      System.out.println("Objects created per 2 seconds = " + numObjects);
      startTime = endTime;
      numObjects = 0;
    }
  }
}


这个例子有个简单的循环,创建一个大型对象并且将它赋给同一个变量,每隔两秒钟报告一次所创建的对象个数。现在的 Java 虚拟机采用 generational 垃圾收集机制,新的对象创建之后放在一个内存空间(取名 Eden)内,然后将那些在第一次垃圾收集以后仍然保留的对象转移到另外一个内存空间。在 Eden,即创建新对象时所在的新一代空间中,收集对象要比在“老一代”空间中快得多。但是如果 Eden 空间已经满了,没有空间可供分配,那么就必须把 Eden 中的对象转移到老一代空间中,腾出空间来给新创建的对象。如果没有显式地赋空变量,而且所创建的对象足够大,那么 Eden 就会填满,并且垃圾收集器就不能收集当前所引用的这个大型对象。所产生的后果是,这个大型对象被转移到“老一代空间”,并且要花更多的时间来收集它。

通过显式地赋空变量,Eden 就能在新对象创建之前获得自由空间,这样垃圾收集就会更快。实际上,在显式赋空的情况下,该循环在两秒钟内创建的对象个数是没有显式赋空时的5倍――但是仅当您选择创建的对象要足够大而可以填满 Eden 时才是如此, 在 Windows 环境、Java虚拟机 1.4 的默认配置下大概需要 500KB。那就是一行赋空操作产生的 5 倍的性能差距。但是请注意这个性能差别产生的原因是变量的作用域不正确,这正是赋空操作发挥作用的地方,并且是因为所创建的对象非常大。更加深入的讨论请参见“赋空变量和垃圾收集”这篇文章。(参见 参考资料)。

最佳实践

这是一个有趣的例子,但是值得强调的是,最佳实践是正确地设置变量的作用域,而不要显式地赋空它们。虽然显式赋空变量一般应该没有影响,但总有一些反面的例子证明这样做会对性能产生巨大的负面影响。例如,迭代地或者递归地赋空集合内的元素使得这些集合中的对象能够满足垃圾收集的条件,实际上是增加了系统的开销而不是帮助垃圾收集。请记住这是个有意弄错作用域的例子,其实质是一个无意识的对象保留的例子。

 

 

 

 

 

转自http://www.ibm.com/developerworks/cn/java/j-perf08273/

分享到:
评论

相关推荐

    java垃圾回收(gc)机制详解.pdf

    垃圾收集器工作后,无论当前内存是否足够,都会回收只被弱引用关联的对象。 4. 虚引用 虚引用是特殊类型的引用,主要用于跟踪对象被垃圾回收的状态。被虚引用关联的对象,在被回收时会收到一个系统通知,但这并不...

    JVM垃圾回收,参数,强软弱虚,常见错误OOM,与微服务结合.docx

    垃圾收集器是垃圾回收算法的落地实现,包括 Serial 串行垃圾收集器、Parallel 并行垃圾收集器、CMS 并发垃圾收集器、G1 垃圾收集器等。不同的垃圾收集器适用于不同的应用场景。 在 Java 中,可以通过命令行参数来...

    内存泄漏引用关系图

    通过这个图形化界面,我们可以发现哪些对象被长时间保持,无法被垃圾收集器回收,从而找出潜在的内存泄漏源。 Heap_walker_References.html分析: Heap_walker_References.html通常是一个内存分析工具的输出结果,...

    垃圾回收相关算法.pdf

    引用计数算法是最直观的垃圾回收策略,它为每个对象分配一个引用计数器,每当有对象引用它时,计数器加1;当引用失效时,计数器减1。当计数器为0时,表明对象不再被使用,可以进行回收。虽然简单易懂,但这种方法...

    [计算机]第5章-3异常与垃圾收集.ppt

    Java的垃圾收集器(Garbage Collector, GC)负责监控和清理不再有引用指向的对象,防止内存泄漏。 1. 对象生命周期:当一个对象被创建后,它会被分配到堆内存中。随着程序的执行,如果对象不再被任何引用指向,那么...

    个人总结之—JVM性能调优实战

    - G1收集器:基于Region的垃圾收集器,适合大堆内存的应用场景。 ##### 3. 常见性能问题及解决方案 - **内存泄漏**:分析内存泄漏的原因,常见的包括对象引用泄露、静态变量滥用等问题,并给出相应的解决措施。 -...

    C++垃圾回收器linux版本

    不正确的内存管理可能导致内存泄漏或悬挂指针,对程序的稳定性和性能造成严重影响。 2. **垃圾回收原理**:垃圾回收器通过跟踪和分析程序中的对象引用关系,找出不再被引用的对象,然后释放它们占用的内存。常见的...

    内存泄露的例子

    内存泄露是程序运行过程中的一个严重问题,尤其是在Java这样的高级编程语言中,它可能导致系统资源耗尽,影响程序性能甚至导致整个系统的崩溃。本文将深入探讨内存泄露的概念、原因、如何在Java中产生以及如何避免。...

    转一篇有关Java的内存泄露的文章

    当内存空间碎片化严重时,为了优化内存使用,垃圾收集器会执行压缩操作,将存活的对象移动到一起,释放连续的内存空间。 `HeapOfFishStrings.java`可能是一个示例,用来演示字符串对象如何影响内存,因为Java中的...

    JavaEE_performance_problem.doc

    当一个不再使用的对象仍然被其他对象引用时,垃圾收集器无法回收其占用的内存。在Web请求处理中,这种类型的问题尤为突出,少量的内存泄漏可能不会立即导致服务器崩溃,但随着大量类似泄漏的积累,服务器性能将受到...

    超级有影响力霸气的Java面试题大全文档

    finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。 16、sleep() 和 wait() 有什么区别? sleep是线程类(Thread)...

    1-JVM.docx

    其中,CMS和G1是并发标记的垃圾收集器,旨在减少垃圾回收对应用程序的影响。 - **CMS收集器**:特点是并发处理大部分垃圾回收阶段,只在初始标记和最终整理阶段暂停应用。优点是低停顿时间,缺点是内存碎片严重,且...

    java虚拟机相关

    Java虚拟机(JVM)是Java程序运行的基础,它的核心特性之一就是自动内存管理,即垃圾回收机制。...通过合理配置JVM参数、选择合适的垃圾收集器以及持续监控和分析应用程序的内存行为,可以有效地提升Java应用的性能。

    java内存分析-内存泄露问题.rar

    在Java虚拟机(JVM)中,垃圾收集器(Garbage Collector, GC)负责自动管理内存,回收不再使用的对象所占用的空间。 内存泄露通常发生在对象不再被程序引用,但仍然占据着内存空间,导致GC无法回收的情况。这可能是...

    Android_memory-leak-debugging.pdf.zip_Android memory le_android_

    - 垃圾收集(GC):Android通过垃圾收集器自动回收不再使用的对象,但这并不意味着开发者可以忽略内存管理。 2. **内存泄漏类型** - 静态变量:静态变量生命周期与应用相同,如果引用的对象不再使用,但静态变量...

    Java内存泄漏问题相关总结

    在Java编程中,内存泄漏是一个严重的问题,它指的是程序中已分配的内存没有被正确地释放,导致这部分内存无法被垃圾收集器(GC)回收,长时间积累会消耗大量系统资源,影响应用性能。本文将通过实例分析常见的内存...

    内存泄漏:Python中的隐蔽陷阱与应对策略

    1. **使用`gc`模块**:Python内置了一个名为`gc`(垃圾收集器)的模块,可以帮助开发者识别并处理循环引用。 2. **谨慎使用全局变量**:避免使用不必要的全局变量,并确保当不再需要它们时,能够被垃圾回收器回收。 ...

    内存泄露解决方法

    4. **启用并行垃圾收集**:通过设置`-XX:+UseParallelGC`或`-XX:+UseParallelOldGC`来启用并行垃圾收集器,提高GC效率。 综上所述,解决内存泄露问题需要从多个角度入手,包括但不限于代码层面的优化、工具辅助以及...

    Android应用源码之防止内存溢出浅析.zip

    8. **WeakReference与SoftReference**:使用WeakReference或SoftReference可以弱化对象引用,让垃圾收集器更容易回收。 9. **内存分析工具**:Android Studio提供了内存分析工具,通过实时监控内存分配和使用,找出...

    Node.js-LeakCanaryAndroid内存泄漏检测工具

    2. **检测泄漏**:在特定条件下(如Activity被销毁但仍然存活),LeakCanary会触发一次全局的垃圾收集,并对存活的对象进行分析。 3. **分析引用链**:LeakCanary通过分析存活对象的引用链,寻找可能的泄漏路径。...

Global site tag (gtag.js) - Google Analytics