处理大型数据集时减少内存流失

时间:2011-12-31 04:11:55

标签: java performance memory

Java倾向于创建大量对象,在处理大型数据集时需要对其进行垃圾回收。当从数据库传输大量数据,创建报告等时,这种情况经常发生。是否有减少内存流失的策略。

在此示例中,基于对象的版本花费大量时间(2秒以上)生成对象并执行垃圾收集,而布尔数组版本在一小部分内完成,没有任何垃圾收集。

在处理大型数据集时,如何减少内存流失(需要大量垃圾收集)?

java -verbose:gc -Xmx500M UniqChars
...
----------------
[GC 495441K->444241K(505600K), 0.0019288 secs] x 45 times
70000007
================
70000007



import java.util.HashSet;
import java.util.Set;
public class UniqChars {
    static String a=null;
    public static void main(String [] args) {
            //Generate data set
            StringBuffer sb=new StringBuffer("sfdisdf");
            for (int i =0; i< 10000000; i++) {
                    sb.append("sfdisdf");
            }
            a=sb.toString();
            sb=null;  //free sb
            System.out.println("----------------");
            compareAsSet();
            System.out.println("================");
            compareAsAry();
    }

    public static void compareAsSet() {
            Set<String> uniqSet = new HashSet<String>();
            int n=0;
            for(int i=0; i<a.length(); i++) {
                    String chr = a.substring(i,i);
                    uniqSet.add(chr);
                    n++;
            }
            System.out.println(n);
    }

    public static void compareAsAry() {
            boolean uniqSet[] = new boolean[65536];
            int n=0;
            for(int i=0; i<a.length(); i++) {
                    int chr = (int) a.charAt(i);
                    uniqSet[chr]=true;
                    n++;
            }
            System.out.println(n);
    }
}

3 个答案:

答案 0 :(得分:4)

在你的例子中,你的两种方法做了很多不同的事情。

compareAsSet()中,您生成相同的4个字符串(“s”,“d”,“f”和“i”)并调用String.hashCode()和String.equals(String)(HashSet当你尝试添加它们时)70000007次。你最终得到的是一个大小为4的HashSet。当你这样做时,每次String.substring(int,int)返回时都会分配String对象,这会在每次“new”生成的垃圾收集器时强制进行次要集合得到了充实。

compareAsAry()中,你已经分配了一个单独的数组,65536个元素的宽度改变了一些值,然后当方法返回时它超出了范围。这是在compareAsSet中完成的单堆内存操作与70000007相比。你有一个局部int变量被更改70000007次,但这发生在堆栈内存而不是堆内存中。与其他方法(基本上只是数组)相比,这种方法在堆中并没有真正产生那么多垃圾。

关于流失,您的选项是回收对象或调整垃圾收集器。

一般来说,使用Strings实际上不可能进行回收,因为它们是不可变的,尽管VM可以执行实际操作,这只会减少总内存占用而不是垃圾流失。针对上述场景的解决方案可以生成回收,但实施将是脆弱且不灵活的。

调整垃圾收集器以便“新”生成更大可以减少在方法调用期间必须执行的集合总数,从而增加调用的吞吐量,您也可以增加堆大小一般会做同样的事情。

为了进一步阅读Java 6中的垃圾收集器调优,我推荐下面链接的Oracle白皮书。

http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html

答案 1 :(得分:4)

正如其中一条评论所指出的那样,这是你的代码,而不是因故障而导致内存流失的Java。所以让我们看看你编写了这个从StringBuffer构建一个疯狂大字符串的代码。在它上面调用toString()。然后在循环中创建新的a.length()字符串的那个疯狂的大字符串上调用substring()。然后在一个阵列上做一些垃圾,因为没有对象创建,所以真的会非常快速地执行,但最终会在一个巨大的数组中写入相同的5-6个位置。浪费多少?那么你认为会发生什么? Ditch StringBuffer并使用StringBuilder,因为它没有完全同步,这会更快一些。

好的,这就是你的算法可能花费时间的地方。请参阅StringBuffer分配内部字符数组以在每次调用append()时存储内容。当该字符数组完全填满时,它必须分配一个更大的字符数组,将刚写入的所有垃圾复制到新数组中,然后附加你最初调用它的内容。因此,您的代码正在分配填充,分配更大的块,将该垃圾复制到新阵列,然后重复该过程,直到它完成1000000次。您可以通过为StringBuffer预分配字符数组来加快速度。大约是10000000 *“sfdisdf”。length()。这将使Java不会创建大量内存,而只是一遍又一遍地转储。

接下来是compareAsSet()混乱。你的行String chr = a.substring(i,i);正在创建新的字符串a.length()次。好吧,因为你正在做a.substring(我,我)只是一个你可以简单的角色(i)然后没有分配发生。还有一个CharSequence选项,它不会创建一个带有它自己的字符数组的新String,而只是指向具有偏移量和长度的原始底层char []。 String.subSequence()

你用任何其他语言插入相同的代码,它也会在那里吮吸。事实上,我说的要差得多。试试这是C ++,如果你分配和释放这么多,那就看它比Java要糟糕得多。请参阅Java内存分配比C ++快得多,因为Java中的所有内容都是从内存池中分配的,因此创建对象的速度要快得多。但是,有限制。此外,如果Java变得过于分散,Java会压缩它的内存,而C ++则不会。因此,当你以相同的方式分配内存并转储它时,你可能会冒着在C ++中分割内存的风险。这可能意味着你的StringBuffer可能会耗尽大到足以完成并崩溃的能力。

事实上,这也可能解释了GC的一些性能问题,因为在取出大量垃圾之后,它必须让房间更加连续。所以Java不仅要清理内存,还必须压缩内存地址空间,这样它就可以为StringBuffer提供足够大的块。

无论如何,我确定你只是测试轮胎,但是用这样的代码进行测试并不是很聪明,因为它永远不会表现良好,因为这是不切实际的内存分配。你知道旧格言Garbage In Garbage Out。那就是你得到的垃圾。

答案 2 :(得分:1)

为了比较,如果你写这个,它会做同样的事情。

public static void compareLength() {
    // All the loop does is count the length in a complex way.
    System.out.println(a.length());
}

// I assume you intended to write this.
public static void compareAsBitSet() {
    BitSet uniqSet = new BitSet();
    for(int i=0; i<a.length(); i++)
        uniqSet.set(a.charAt(i));
    System.out.println(uniqSet.size());
}

注意:BitSet每个元素使用1位,而不是每个元素1个字节。它也会根据需要扩展,所以说你有ASCII文本,BitSet可能使用128位或16字节(加上32字节开销)boolean []使用64 KB,这要高得多。具有讽刺意味的是,使用boolean[]可以更快,因为它涉及更少的位移,并且只有所使用的数组部分需要在内存中。

正如您所看到的,使用任一解决方案,您都可以获得更高效的结果,因为您可以使用更好的算法来完成所需的工作。