当前位置:编程学习 > JAVA >>

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:

[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
[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情况:

[plain]
jstat -gc -h10 7887 1000 1000 
[plain]
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。

[java]
class NewTest{ 
        private static Random rand = new Random(); 
 
        private static Employee employee = new Employee("", 0, 0); 
 
        // get a employee 
        public static Employee get

补充:软件开发 , Java ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,