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);
}
}
答案 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[]
可以更快,因为它涉及更少的位移,并且只有所使用的数组部分需要在内存中。
正如您所看到的,使用任一解决方案,您都可以获得更高效的结果,因为您可以使用更好的算法来完成所需的工作。