NetBeans Profiler 案例研究


1. 简介

NetBeans Profiler 基于 Sun 开发的名为 Jfluid 的性能分析技术。我很喜欢使用 Jfluid。它可以帮助我检测一些 Java 应用程序中的内存泄漏和性能瓶颈。Jfluid 技术将集成到 NetBeans 中。现在,我们可以尝试将其与 NetBeans 4.0 一起使用。在本文中,我使用的是 Profiler Milestone 3 内部版本。我想与您分享一下我的经验,以使您了解它是如何帮助开发者改进应用程序的。我只能提供一些粗浅的见解。您得自己亲自动手实践一下。

如果您不熟悉 NetBeans Profiler,请先访问以下 URL。其中提供了一些有关设置 Profiler 的文档。


2. 检测内存泄漏问题的方法

此处介绍了使用 Profiler 检测内存泄漏问题的基本原则。内存泄漏检测是通过以下步骤完成的。

  • 监视 JVM 堆大小及其趋势。
  • 如果使用的堆大小持续增加,请检查哪个对象占用了大部分 JVM 堆。
  • 查看该对象的分配和释放方式/位置。
  • 检查是否在内联代码中正确分配和释放该对象。

我如何使用 Profiler 完成上述步骤?我们可以通过查看几个 Profiler 图形来执行这些操作。

'Profiler' 窗口中的堆图

首先,让我们看一下位于 Netbeans 窗口底部的 'Profiler' 窗口。左图是堆大小和使用的堆图。然后,您可以通过执行应用程序来了解堆大小和使用的堆如何变化。如果使用的堆随时间的推移而不断增加,则可能出现了内存泄漏问题。

内存泄漏检测

上图简要说明了 JVM 如何使用内存。此处,左图中的使用的堆相对稳定,因为它不随时间的推移而持续增加。

活动对象内存图

接下来,让我们看一下内存图以了解哪个对象占用了大部分内存。在 "Live Bytes"(活动字节)字段中,该图显示每个对象使用的内存大小。"Live Objects"(活动对象)字段表示 JVM 堆中的对象数。根据对象数,我们可以了解有多少个对象占用了内存。另外,请查看 "Generations"(年代数)字段。 "Generations"(年代数)表示存活的年代数。如果随时间推移持续分配而不释放类对象,"Generations"(年代数)将持续增加。这是一种典型的内存泄漏情况,该字段将指示这种情况。

在上面的“活动对象内存图”示例中,HashMap$Entry 占用了很大部分的内存。不过,"Generations"(年代数)字段中的数字很低。很难断定是否出现了内存泄漏问题。我们需要将应用程序运行更长时间,并监视内存使用情况。

反向调用图

如果有任何可疑的对象,让我们看一下反向调用图。它可以帮助我们了解占用内存的对象的分配位置。它告诉我们应该在 Java 代码中检查哪些内容。

上图显示了 'char' 对象的反向调用图。该图指示 'char' 是在 StringBuffer.expandCapacity(int) 方法中分配的。通过使用反向调用图,我们可以确定有问题的对象的分配位置。然后,让我们看一下该位置的内联代码。


3. 案例研究:检测内存泄漏

接下来,让我介绍一下使用 Profiler 检测内存泄漏问题的案例研究。NetBeans Profiler 具有对象活动性性能分析功能(记录对象创建和垃圾收集)。它显示分配和解除分配对象的统计信息,以帮助您检测任何内存问题。

样例代码

为了说明如何使用 Profiler 检测内存泄漏,我使用了下面的样例代码。

[TestHash 样例代码]


/*
 * Main.java
 *
 * Created on 2005/01/06, 11:00
 */

package memoryleak;

import java.util.*;
import java.io.*;

/**
 *
 * @author root
 */
public class Main {
    Hashtable hashtable = new Hashtable();
    Square sq[] = new Square[10];
   
    /** Creates a new instance of Main */
    public Main() {        
        
    }
    
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        // TODO code application logic here
        Main testhash = new Main();   
        for(int i=0; i<100000; i++) {
            testhash.create(i);  
            testhash.use(i);    
            testhash.release();
        }
    }
    
    // Create Square objects.
    void create(int i) {
        for(int j=0; j<10; j++) {
            long index = j+i*10;
            sq[j] = new Square(index);
            hashtable.put(sq[j].num, sq[j]);
        }
    }
    
    // Use Square objects.
    void use(int i) {
        for(int j=0; j<10; j++) {
            System.out.print(((Square)(hashtable.get(sq[j].num))).square + " ");
        }
        System.out.println();
    }
    
    void release() {
        for(int j=0; j<10; j++) {
            sq[j] = null;
        }
    }
    
}

class Square {
    String num;
    String square;
    public Square(long num) {
        this.num = new Long(num).toString();
        this.square = new Long(num*num).toString();
    }
}

假设代码按以下方式工作,但您可能已注意到此样例代码中的问题。:)

  • 创建 Square 对象。
  • 在 Hashtable 中存储创建的 Square 对象。
  • 在使用后,释放 Square 对象。

最后,它释放 Square 对象,以使其似乎没有内存泄漏问题。

问题:OutOfMemoryError

现在,让我们运行一下样例代码。如果没有问题,它应该显示数字 1 至 999999 的平方。以下是执行结果。

[执行结果]


0 1 4 9 16 25 36 49 64 81 
	:
	snip
	:
179411544900 179412392041 179413239184 179414086329 179414933476 
179415780625 179416627776 179417474929 179418322084 179419169241 
Exception in thread "main" java.lang.OutOfMemoryError

遗憾的是,应用程序由于 OutOfMemoryError 异常而终止。

使用 Profiler

为什么样例应用程序由于 OutOfMemoryError 而终止?我们可以猜出来,原因是应用程序用完了 JVM 堆,因为它出现的是 OutOfMemoryError 错误。不过,是哪些对象导致此错误,又是如何导致的呢?

现在该是使用 NetBeans Profiler 的时候了。它分析新对象的分配和解除分配。要运行 Profiler,请在资源管理器窗口中选择应用程序的主类,然后选择 "Profile"(分析)-> "Profile File..."(分析文件...)。

接下来,选择 "Analyze Memory Usage"(分析内存使用情况)。选择 "Record both objects and garbage collection."(同时记录对象创建和垃圾回收)。

'Profiler' 窗口中的堆图

使用 Profiler 运行应用程序。要查看 Profiler 统计信息,请选择 "Profile"(分析)-> "Get Current"(获取当前结果)。作为第一步,先查看整个窗口底部的 'Profiler' 窗口。在 'Profiler' 窗口的堆图中,我们可以看到使用的堆大小(紫色)继续增加。因此,可能出现了内存泄漏问题。

[当前结果图像]

整个 Netbeans 窗口

活动对象内存图

接下来,让我们看一下上图中的活动对象内存图。有四个对象占用了大部分内存。它们是 char[]、String、Hashtable$Entry 和 Square。还要看一下上图中的 "Generations"(年代数)字段。该字段实质上表明是否出现了内存泄漏问题。如果该数字稳定增加,则可能表明出现了内存泄漏问题。这四个对象在 "Generations"(年代数)字段中具有较大的数字。它们是 51。与此相反,其他对象为 1。由于数字较大,我怀疑未正确释放这些对象而导致内存泄漏。

反向调用图

现在,让我们看一下列表顶部的 'char' 对象的反向调用图。您可以双击 'char' 对象项目以执行此操作。

[char 的反向调用图]

整个 NetBeans 窗口

根据反向调用图,似乎在 String.<init> 中创建了一些 'char' 对象,但从未释放这些对象,因为在 Square.<init> 中创建的 'char' 在 "Live Objects"(活动对象)和 "Allocated Objects"(分配的对象)字段中具有相同的数值。现在,让我们看一下 'String' 对象的反向调用图。

[String 的反向调用图]

整个 NetBeans 窗口

情况与 'char' 类似。在 Long.toString() 中创建了一些 'String' 对象,但未释放这些对象。在 Long.toString() 中创建的 'String' 具有相同的 "Live Objects"(活动对象)和 "Allocated Objects"(分配的对象)数值。为什么从未释放这些对象?线索就在活动对象列表中。在该列表中,'Hashtable.$Entry' 对象也具有较高的 "Generations"(年代数)字段值。在调用 Hashtable.remove() 之前,'Hashtable.$Entry' 一直处于活动状态。而 'Hashtable.$Entry' 包含存储在 Hashtable 中的对象。因此,我们可以猜出来,Hashtable 中存储的对象未正确释放。

查看内联代码。

然后,让我们返回到样例程序的源代码。

[源代码摘要]


    void release() {
        for(int j=0; j<10; j++) {
            sq[j] = null;
        }
    }

很好!现在,我们就快找到问题的根本原因了。'Square' 对象正确设置为 'null'。不过,我们似乎忘记释放 Hashtable 中的 'Square' 对象。Hashtable 仍然包含这些对象。我们可以看到 'Square' 对象具有 'String' 对象。'Square' 对象中的 'String' 对象似乎保留在堆中。为了解决该问题,我按下列方式修改了源代码中的 'release' 方法。

[修改后的源代码]


    void release() {
        for(int j=0; j<10; j++) {
            hashtable.remove(sq[j].num);  /* added */
            sq[j] = null;
        }
    }

让我们运行更正后的样例程序。

[结果]


0 1 4 9 16 25 36 49 64 81 
 :
snip
 :
999980000100 999982000081 999984000064 999986000049 999988000036 
999990000025 999992000016 999994000009 999996000004 999998000001 

哇噻!我们看到样例程序正常完成,没有出现任何错误。通过使用 NetBeans Profiler 的内存使用情况监视功能,我们找到了出现内存泄漏问题的原因。


4. 小结

我简要说明了使用 NetBeans Profiler 检测内存泄漏问题的方法和案例研究。请尝试使用最新的 NetBeans Profiler。这可帮助您分析 JVM 内存问题。祝您使用 NetBeans Profiler 一切顺利。


By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2013, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo