疯狂java


您现在的位置: 疯狂软件 >> 新闻资讯 >> 正文

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) 执行构造方法体

  在创建对象时的几个关键应用规则:

  避免在循环体中创建对象,即使该对象占用内存空间不大

  尽量及时使对象符合垃圾回收标准

  不要采用过深的继承层次

  访问本地变量优于访问类中的变量

  关于“在循环体中创建对象”,如下方式会在内存中产生大量的对象引用,浪费大量的内存空间,增大了系统做垃圾回收的负荷:

  Java代码

  for (int i = 0; i < 1000; i++) {

  Object obj = new Object();

  ...

  }

  而如下方式,仅在内存中保存一份对该对象的引用:

  Java代码

  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。

  例:

  Java代码

  …

  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能快速回收该对象空间。

  例:

  Java代码

  …

  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) 不可视阶段

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

  例:

  Java代码

  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()中,因为这样更保险、安全。在类深度继承的情况下,这种方法就显得更有效了。

  例:

  Java代码

  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及时回收垃圾内存,维护系统的稳定性。

  例:

  Java代码

  char[] obj = new char[1000000];

  SoftReference ref = new SoftReference(obj);

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

  5. 共享静态变量存储空间

  静态变量生命周期较长,不易被系统回收。因此如果不合理使用静态变量,会造成大量的内存浪费。建议在具备下列全部条件的情况下,尽量使用静态变量:

  1) 变量所包含的对象体积较大,占用内存较多

  2) 变量所包含的对象生命周期较长

  3) 变量所包含的对象数据稳定

  4) 该类的对象实例有对该变量所包含的对象的共享需求

  6. 对象重用与GC

  通常我们把用来缓存对象的容器对象成为对象池(Object Pool)。对象池通过对其所保存对象的共享与重用,缩减了应用线程反复重建、装载对象的过程所需的时间,有效避免了频繁GC带来的巨大系统开销,能大大提高应用性能,减少内存需求。

  但如果长时间将对象保存在对象池中,即驻留在内存中,而这些对象又不被经常使用,无疑会造成内存资源浪费,又或者该对象在对象池中遭到破坏,若不对其及时清除会非常麻烦。因此,若决定使用对象池技术,需要采取相应的手段清除遭到破坏的对象,甚至在某些情况下需要清除对象池中所有对象。或者可为对象池中的每个对象分配一个时间戳,设定对象的过期时间,对象过期则及时将其从内存中清除。可以专门创建一个线程来清除此类对象。

  例:

  Java代码

  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) 在合适的场景下使用对象池技术以提高系统性能,缩减系统内存开销,但需注意对象池的尺寸不宜过大,及时清除无效对象释放内存资源