GC优化——对象复用
Java虚拟机的自动内存管理让程序员从频繁出错的内存操作中解放了,不需要像C++一样,每次new之后必须显示的调用delete进行内存释放操作。虽然,我们不用再操心内存泄露这样的bug,因为垃圾收集器可以很好的把垃圾对象清理掉。但是出于性能的考虑,最好不要肆无忌惮的创建对象,在可以复用对象的情况下尽量复用,这样可以减少对象内存的分配,降低gc的频率,有效的优化gc。 在程序运行的
在程序运行的过程中,JVM堆上存放着活对象(Active Object)以及程序无法再次引用的垃圾对象(Garbage)。在堆空间的空闲部分低于某个比例或者不能满足为新对象分配空间的要求时,JVM都会触发一次垃圾收集(Garbage Collect, GC)。gc时,JVM会暂停所有用户线程(就是传说中的Stop the World),垃圾收集器通过某种方式(引用计数或根搜索法)识别垃圾对象,然后释放它们占用的空间,并且还需要整理碎片(采用复制算法或标记整理算法)。如果gc的频率过高或时间过长,都会影响JVM执行用户线程的时间,导致程序性能大大下降。
堆空间是有限的,所以堆始终会因为对象的创建而没有足够的空闲空间导致垃圾收集的发生。我们无法避免这种情况,但是可以通过一些方法减少垃圾收集的频率。其中最简单的方法就是,复用对象。比如用于传输数据的VO(Value Object),没必要每次new一个新对象,可以采用某种机制有效的利用之前创建的对象,这样不仅减少了堆占用的空间,而且还避免了因为给对象分配空间所花费的时间。下面是一个例子,创建n个employee,统计平均年龄和平均薪水,每个employee都通过new创建:
Employee.java:
class Employee{
private String name;
private int age;
private double salary;
public Employee(String name, int age, double salary){
this.name = name;
this.age = age;
this.salary = salary;
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
public double getSalary(){
return this.salary;
}
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public void setSalary(double salary){
this.salary = salary;
}
@Override
public String toString(){
return this.name+" "+this.age+" "+this.salary;
}
}
NewTest.java
import java.util.Random;
class NewTest{
private static Random rand = new Random();
// get a employee
public static Employee getEmployee(){
int age = rand.nextInt(100);
double salary = 1000.0 + rand.nextDouble() * 10000;
// generate name: 3-5 characters
int nameLen = 3 + rand.nextInt(3);
StringBuilder nameBuilder = new StringBuilder();
for(int i=0; i<nameLen; i++){
nameBuilder.append((char)('a' + rand.nextInt(26)));
}
return new Employee(nameBuilder.toString(), age, salary);
}
public static void main(String[] args){
long s = System.currentTimeMillis();
double avgSalary = 0.0;
double avgAge = 0.0;
int count = 200000000;
for(int i=0; i<count; i++){
Employee e = getEmployee();
System.out.println(e);
avgSalary += e.getSalary();
avgAge += e.getAge();
}
avgSalary /= count;
avgAge /= count;
System.out.println(System.currentTimeMillis() - s);
}
}
通过命令 java -Xms20m -Xmx20m NewTest运行程序,保证JVM的堆只有20m,观察gc情况:
jstat -gc -h10 7887 1000 1000
S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
64.0 64.0 0.0 0.0 6656.0 0.0 13696.0 240.1 21248.0 2990.6 5849 1.251 0 0.000 1.251
通过jmap -histo 7887查看堆上分配的对象,Employee有30248个实例,占用1209920字节。根据程序的逻辑,我们知道这些对象中只有一个是活对象,其他的都是垃圾对象。就是因为每次循环都创建一个Employee对象,导致了5849次young gc。通过计算1209920/30248=40bytes,可以知道每个Employee实例占40字节(64bit的情况下),而程序要创建200000000个实例,可知仅仅是这些Employee对象就需要占用200000000*40/1024 = 7812500KB = 7629.39MB = 7.45GB,而整个新生代就只有6656+64 = 6720KB,这得触发多少次young gc??而如果采用复用对象的方式就不会浪费这么多空间。下面的代码,只是用一个Employee对象,每次不是new,而是使用setter方法设置值:
NewTest.java:通过静态变量employee每次set对象的值模拟复用,而不是new新对象。实际应用中可以使用对象池,比如commans-pool。
class NewTest{
private static Random rand = new Random();
private static Employee employee = new Employee("", 0, 0);
// get a employee
public static Employee getEmployee(){
int age = rand.nextInt(100);
double salary = 1000.0 + rand.nextDouble() * 10000;
// generate name: 3-5 characters
int nameLen = 3 + rand.nextInt(3);
StringBuilder nameBuilder = new StringBuilder();
for(int i=0; i<nameLen; i++){
nameBuilder.append((char)('a' + rand.nextInt(26)));
}
employee.setAge(age);
employee.setName(nameBuilder.toString());
employee.setSalary(salary);
return employee;
}
}
用相同的命令运行程序,观察gc:
S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
128.0 128.0 64.0 0.0 6528.0 0.0 13696.0 232.1 21248.0 2991.4 4714 0.847 0 0.000 0.847
可以看到young gc的次数是4714,减少了1135次,整个的gc时间从1.251-0.847,减少了0.4s(优化的力度比较小啊,这是因为Hotspot JVM的新生代采用复制算法,每次只复制那些活对象。而这个例子中每次gc时,大部分对象都是垃圾对象,所以说每次gc的只复制很少的对象,复制成本很低。但是在实际的程序中,因为程序运行的状态更加复杂,不会存在这种情况)。再通过jmap观察对象的分布,看到堆上只有一个Employee对象。这里用到了随机函数,可能会对测算结果的有一些影响,但是young gc次数减少是必然的。在编写程序时,可以对那些传递数据的对象采用复用对象的方式优化gc,这里复用的方式可以采用对象池。在orm中,通过数据库查询返回POJO对象时就是一个很好的利用场景。还有通过JSON数据实例化对象的情况。
总之,JVM的自动内存管理很方便,使程序员有更多的精力关注于业务逻辑。但是,要更加高效的运行程序,编程时还是有很多细节需要处理的。其中,复用对象就是一个。
更多推荐
所有评论(0)