`
EalayKing
  • 浏览: 8546 次
最近访客 更多访客>>
社区版块
存档分类
最新评论

Java优化编程笔记——内存管理

阅读更多
1. 垃圾回收
JVM运行环境中垃圾对象的定义:
        一个对象创建后被放置在JVM的堆内存(heap)中,当永远不再引用这个对象时,它将被JVM在堆内存(heap)中回收。被创建的对象不能再生,同时也没法通过程序语句释放它们。

        不可到达的对象被JVM视为垃圾对象,JVM将给这些对象打上标记,然后清扫回收它们,并将散碎的内存单元收集整合。

JVM管理的两种类型的内存:
        堆内存(heap),主要存储程序在运行时创建或实例化的对象与变量。
        栈内存(stack),主要存储程序代码中声明为静态(static)(或非静态)的方法。

        堆内存(heap)通常情况下被分为两个区域:新对象(new object)区域与老对象(old object)区域。

新对象区域:
        又可细分为Eden区域、From区域与To区域。
        Eden区域保存新创建的对象。当该区域中的对象满了后,JVM系统将做可达性测试,主要任务是检测有哪些对象由根集合出发是不可到达的,这些对象就可被JVM回收,且将所有的活动对象从Eden区域拷到To区域,此时有一些对象将发生状态交换,有的对象就从To区域被转移到From区域,此时From区域就有了对象。
        该过程执行期间,JVM的性能非常低下,会严重影响到正在运行的应用的性能。

老对象区域:
        在老对象区域中的对象仍有一个较长的生命周期。经过一段时间后,被转入老对象区域的对象就变成了垃圾对象,此时它们被打上相应的标记,JVM将自动回收它们。
        建议不要频繁强制系统做垃圾回收,因为JVM会利用有限的系统资源,优先完成垃圾回收工作,致使应用无法快速响应来自用户端的请求,这样会影响系统的整体性能。

2. JVM中对象的生命周期
        对象的整个生命周期大致分为7个阶段:创建(creation)、应用(using)、不可视(invisible)、不可到达(unreachable)、可收集(collected)、终结(finalized)、释放(free)。

1) 创建阶段
        系统通过下面步骤,完成对象的创建:
        a) 为对象分配存储空间
        b) 开始构造对象
        c) 递归调用其超类的构造方法
        d) 进行对象实例初始化与变量初始化
        e) 执行构造方法体

        在创建对象时的几个关键应用规则:
        • 避免在循环体中创建对象,即使该对象占用内存空间不大
        • 尽量及时使对象符合垃圾回收标准
        • 不要采用过深的继承层次
        • 访问本地变量优于访问类中的变量

        关于“在循环体中创建对象”,如下方式会在内存中产生大量的对象引用,浪费大量的内存空间,增大了系统做垃圾回收的负荷:
    for (int i = 0; i < 1000; i++) {
        Object obj = new Object();
        ...
    }

        而如下方式,仅在内存中保存一份对该对象的引用:
    Object obj = null;
    for (int i = 0; i < 1000; i++) {
        obj = new Object();
        ...
    }

        另外,不要对一个对象进行多次初始化,这同样会带来较大的内存开销,降低系统性能。

2) 应用阶段
        该阶段是对象得以表现自身能力的阶段,即是对象整个生命周期中证明自身“存在价值”的时期。此时对象具备下列特征:
        a) 系统至少维护着对象的一个强引用(Strong Reference)
        b) 所有对该对象的引用全是强引用(除非我们显示使用了软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))

软引用
        主要特点是具有较强的引用功能。只有当内存不够时,才回收这类内存。另外,这些引用对象还能保证在Java抛出OutOfMemory异常前,被置为null。它可用于实现一些常用资源的缓存,保证最大限度的使用内存而不引起OutOfMemory。

例:
    …
    import java.lang.ref.SoftReference;
    …
    A a = new A();
    …// 使用a
    SoftReference sr = new SoftReference(a);// 使用完了a,将它设置为软引用类型
    a = null;// 释放强引用
    …
    // 下次使用时
    if (sr != null) {
        a = sr.get();
    } else {
        // GC由于内存资源不足,系统已回收了a的软引用,故需重新装载
        a = new A();
        sr = new SoftReference(a);
    }
    …

        软引用技术使Java应用能更好地管理内存,稳定系统,防止系统内存溢出,避免系统崩溃。在处理一些占用内存较大,且声明周期较长,但使用并不频繁的对象时,应尽量应用该技术。

        但某些时候对软引用的使用会降低应用的运行效率与性能,如:应用软引用的对象的初始化过程较耗时,或对象的状态在运行过程中发生了变化,都会给重新创建对象与初始化对象带来不同程度的麻烦。

弱引用
        与软引用对象的最大不同在于:GC在进行回收时,需通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。故弱引用对象拥有更短的生命周期,更易更快被GC回收。

        虽然GC在运行时一定回收弱引用对象,但是负责关系的弱对象群常常需要好几次GC的运行才能完成。弱引用对象常用于Map数据结构中,引用占用内存空间较大的对象,一旦该对象的强引用为null,GC能快速回收该对象空间。

例:
    …
    import java.lang.ref.WeakReference;
    …
    A a = new A();
    …// 使用a
    WeakReference wr = new WeakReference(a);// 使用完了a,将它设置为弱引用类型
    a = null;// 释放强引用
    …
    // 下次使用时
    if (wr != null) {
        a = wr.get();
    } else {
    a = new A();
    wr = new WeakReference(a);
    }
    …

虚引用
        用途较少,主要用于辅助finalize方法。虚对象指一些执行完了finalize方法,且为不可达对象,但还未被GC回收的对象。这种对象可辅助finalize进行一些后期的回收工作,通过覆盖Reference的clear方法增强资源回收机制的灵活性。

注意:实际程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,因为软引用能加上JVM对垃圾内存的回收速度,能维护系统的运行安全,防止内存溢出等。

3) 不可视阶段
        对象经历应用阶段后,便处于不可视阶段,说明我们在其他区域的代码中已不可再引用它,其强引用已消失。如:本地变量超出其可视范围。

例:
    public void process() {
        try {
            Object obj = new Object();
            obj.doSomething();
        } catch (Exception e) {
            e.printStackTrace();
        }
        while (isLoop) {// … loops forever
            // 该区域对应obj对象来说已是不可视的了
            // obj. doSomething(); 在编译时会引发错误
        }
    }

        若一个对象已使用完,且在其可视区域不再使用,应主动将其设置为null。针对上面的例子,可主动添加obj = null;这样一行代码,强制将obj置为null。这样的意义是,帮助JVM及时发现这个垃圾对象,且可以及时回收该对象所占用的系统资源。

4) 不可到达阶段
        处于不可到达阶段的对象,都是要被GC回收的预备对象,但此时该对象并不能被GC直接回收。其实所有垃圾回收算法所面临的问题是相同的——找出由分配器分配的,但用户程序不可到达的内存块。

5) 可收集阶段、终结阶段与释放阶段
        此时JVM就可以直接回收对象了,而对象可能处于下面三种情况:
        a) GC发现该对象已不可到达
        b) finalize方法已被执行
        c) 对象空间已被重用

3.  Java中的析构方法finalize
        Java中的finalize()与C++中的析构函数的职能极为类似。
        虽然我们可以在一个Java类调用其父类的finalize方法,但由于该方法未自动实现递归调用,我们必须手动实现,因此该方法的最后一个语句通常是super.finalize()语句。通过这种方式,我们能实现从下到上finalize的迭代调用,即先释放用户类自身的资源,再释放父类资源。通过可在finalize()中释放一些不易控制,且非常重要的资源,如:一些I/O操作,数据的连接。
        finalize()最终由JVM中的垃圾回收器调用,其调用时间是不确定或不及时的,调用时机对我们来说是不可控的。因此,有时我们需要通过其他手段释放系统资源,如声明一个destroy(),在该方法中添加释放系统资源的处理代码。虽然我们可以通过调用自定义的destroy()释放系统资源,但最好将对destroy()的调用放入当前类的finalize()中,因为这样更保险、安全。在类深度继承的情况下,这种方法就显得更有效了。
例:
    public class A {
        Object a = null;

        public A() {
            a = new Object();
            System.out.println("create a");
        }

        protected void destroy() {
            a = null;
            System.out.println("release a");
        }

        protected void finalize() throws Throwable {
            destroy();
            super.finalize();// 递归调用父类的finalize()
        }
    }

    public class B extends A {
        Object b = null;

        public B() {
            b = new Object();
            System.out.println("create b");
        }

        protected void destroy() {
            b = null;
            System.out.println("release b");
            super.destroy();
        }

        protected void finalize() throws Throwable {
            destroy();
            super.finalize();// 递归调用父类的finalize()
        }
    }

    public class MyTest {
        B obj = null;
    
        public MyTest() {
            obj = new B();
        }
    
        protected void destroy() {
            if (obj != null) {
                obj.destroy();
            } else {
                System.out.println("obj already released");
            }
        }
    
        public static void main(String[] args) {
            MyTest mt = new MyTest();
            mt.destroy();
        }   
    }

        MyTest的运行结果:
                create a
                create b
                release b
                release a
        初始化B对象时,其构造器产生了递归调用,由父类开始依次调用,而在调用B对象的destroy()时,系统产生了与初始化调用相反的递归调用,释放资源是由子类开始的。由此可见,在设计类时,应尽可能避免在类的默认构造器中创建、初始化大量对象,或执行某种复杂、耗时的运算逻辑。

4. 数组的创建
        如果数组中所保存的元素占用内存空间较大,或数组本身长度较长,我们可采用软引用的技术来引用数组,以“提醒”JVM及时回收垃圾内存,维护系统的稳定性。
例:
    char[] obj = new char[1000000];
    SoftReference ref = new SoftReference(obj);

        JVM根据运行时的内存资源的使用情况,来把握是否回收该对象,释放内存。虽然这样会对应用程序产生一些影响(如当需要使用该数组对象时,该对象被回收了),但却能保证应用整体的稳健性,达到合理使用系统内存的目的。

5. 共享静态变量存储空间
        静态变量生命周期较长,不易被系统回收。因此如果不合理使用静态变量,会造成大量的内存浪费。建议在具备下列全部条件的情况下,尽量使用静态变量:
        1) 变量所包含的对象体积较大,占用内存较多
        2) 变量所包含的对象生命周期较长
        3) 变量所包含的对象数据稳定
        4) 该类的对象实例有对该变量所包含的对象的共享需求

6. 对象重用与GC
        通常我们把用来缓存对象的容器对象成为对象池(Object Pool)。对象池通过对其所保存对象的共享与重用,缩减了应用线程反复重建、装载对象的过程所需的时间,有效避免了频繁GC带来的巨大系统开销,能大大提高应用性能,减少内存需求。
但如果长时间将对象保存在对象池中,即驻留在内存中,而这些对象又不被经常使用,无疑会造成内存资源浪费,又或者该对象在对象池中遭到破坏,若不对其及时清除会非常麻烦。因此,若决定使用对象池技术,需要采取相应的手段清除遭到破坏的对象,甚至在某些情况下需要清除对象池中所有对象。或者可为对象池中的每个对象分配一个时间戳,设定对象的过期时间,对象过期则及时将其从内存中清除。可以专门创建一个线程来清除此类对象。
例:
    class CleanUpThread extends Thread {
        private ObjectPool pool;
        private long sleepTime;
        
        CleanUpThread(ObjectPool pool, long sleepTime) {
            this.pool = pool;
            this.sleepTime = sleepTime;
        }
    
        public void run() {
            while (true) {
                try {
                    sleep(sleepTime);
                } catch (InterruptedException e) {
                    …// 相应处理
                }
                pool.cleanUp();
            }
        }
    }


7. transient变量
        在做远程方法调用(RMI)类的应用开发时,应该会遇到transient变量与Serializable接口,之所以要对象实现Serializable接口,是因为这样就可以从远程环境以对象流的方式,将对象传递到相应的调用环境中,但有时这些被传递的对象的一些属性并不需要被传递,因为这些数据成员对于应用需求而言是无关紧要的,那么这些属性变量就可被声明为transient。被声明为transient的变量是不会被传递的,这样能节约调用方的内存资源,减少网络开销,提高系统性能。这个transient变量所携带的数据量越大(如数据量较大的数组),其效用越大。

8. JVM内存参数调优
-XX:NewSize,设置新对象成产堆内存(set new generation heap size)
        通常该选项数值为1024的整数倍且大于1MB。取值规则为,取最大堆内存(maximum heap size)的1/4。

-XX:MaxNewSize,设置最大新对象生产堆内存(set maximum new generation heap size)
        其功用同-XX: NewSize。

-Xms,设置堆内存池的最小值(set minimum heap size)
        要求系统为堆内存池分配内存空间的最小值。通常该选项数值为1024的整数倍且大于1MB。取值规则为,取与最大堆内存(-Xmx)相同的值,以降低GC的频度。

-Xmx,设置堆内存池的最小值(set maximum heap size)
        要求系统为堆内存池分配内存空间的最大值。通常该选项数值为1024的整数倍且大于1MB。取值规则为,取与最大堆内存(-Xms)相同的值,以降低GC的频度。

例:
        java –XX:NewSize=128m –XX:MaxNewSize=128m –Xms512m –Xmx512m MyApp

9. Java程序设计中有关内存管理的其他经验
1) 尽早释放无用对象的引用,加速GC的工作
2) 尽量少用finalize()。finalize()是Java给程序员提供一个释放对象或资源的机会,但会加大GC的工作量
3) 对常用到的图片,可采用SoftReference
4) 注意集合数据类型,包括数组、树、图、链表等数据结构,它们对GC来说,回收更复杂。另外,注意一些全局变量、静态变量,它们易引起悬挂对象,造成内存浪费
5) 尽量避免在类的默认构造器中创建、初始化大量的对象,防止在调用其子类的构造器时造成不必要的内存资源浪费
6) 尽量避免强制系统做GC(通过显式调用System.gc()),增长系统GC的最终时间,降低系统性能
7) 尽量避免显式申请数组空间,当不得不显式申请时,尽量准确估计出其合理值
8) 在做远程方法调用(RMI)类应用开发时,尽量使用transient变量
9) 在合适的场景下使用对象池技术以提高系统性能,缩减系统内存开销,但需注意对象池的尺寸不宜过大,及时清除无效对象释放内存资源
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics