【JAVA】十 创建对象与销毁对象

喜欢ヅ旅行 2022-07-17 00:17 399阅读 0赞

一 静态工厂代替构造方法

static factory method 是一个返回类实例的静态方法

来看看jdk的boolean类的封装

  1. public static Boolean valueOf(boolean b) {
  2. return (b ? TRUE : FALSE);
  3. }

优势

  1. 有不同的名称
  2. 不必每次调用都创建一个新的对象
  3. 可以返回原对象的任何子集
  4. 参数对象实例时代码更加简洁

二 用私有构造器/枚举强化Singleton属性

Singleton 指仅仅实例化一次的类 , Singleton通常代表唯一的系统组件 , 比如窗口管理 / 文件系统 . 使类成为Singleton会使它的客户端测试十分困难, 因为无法给Singleton替换模拟实现, 除非他实现一个充当其类型的接口 .

在jdk1.5之前.singleton两中方法都要私有化构造器,并导出共有的静态成员 . 以便客户端能够访问该类的唯一实例 .

如:

  1. public class Elvis{
  2. public static final Evlis INSTANCE = new Elvis();
  3. private Elvis(){...}
  4. }

但是可以借助AccessibleObject.setAccessible方法, 通过反射机制调用私有构造器 .

如何抵御攻击 ?

修改构造器,让它在被要求建立第二个实例的时候抛出异常.

在实现singleton 第二方法中, 共有成员是个静态工厂方法 .

对于静态方法Elvis.getInstance的所有调用, 都会返回同一个对象的引用. 所以永远补会创建其他的Elvis实例 .

  1. public class Elvis {
  2. private static final Elvis INSTANCE = new Elvis();
  3. private Elvis(){
  4. ...}
  5. public static Elvis getInstance(){ return INSTANCE ; }
  6. ...
  7. }

公共域优势

组成类的成员的生命很清晰的表明了这个类是一个singleton ;

公有的静态域是final的 , 所以该域将总是包含相同的对象引用 .

公有域方法在性能上不在有任何优势.

工厂方法优势

灵活性 在不变api的前提下 , 我们可以改变该类是否该singleton的想法.比如改为每个调用该方法的线程返回一个唯一的实例 .

Singleton 序列化问题

为了使singleton可序列化,仅仅加上 implements Serizlizable 是不够的 , 为了维护并保证Singleton 必须生命所有实例域都是 瞬时 transient 的 ,并提供readResolve方法. 否则反序列化是都会建立一个新的实例 , 比如在例子中会导致”假冒Elvis” ,为防止这情况 , 要在Evlis类中加入下面readResolve方法 .

  1. private Object readResolve(){
  2. return INSTANCE ;
  3. }

JDK1.5 Singleton

jdk1.5开始加入了枚举类型 enum , 只需要编写一个包含单个元素的枚举类型 .

这种方法在功能上与公有域方法相近,但是他更加的简洁,无偿第提供了序列化机制 , 绝对防止多次序列化,即使是爱面对复杂的序列化/反序列化攻击都没有问题 .枚举类方法的singleton是jdk1.5以来最佳的实现方法.

关于详细介绍参考我的另一篇文章 .
http://blog.csdn.net/maguochao_mark/article/details/51423725

  1. public enum Elvis {
  2. INSTANCE ;
  3. }

三 避免建立不必要的对象

一般而言,最好能重用对象而不是在每次使用是建立新的对象. 重用的方式既快捷又方便,如对象是不可变的immutable 那么它始终可以被重用 .

  1. String s = new String("string");

这句就建立了两个String对象 “string”本身就是一个string对象了 , 然后作为参数又传入了String的构造器,又生成了一个新的string对象,如果这个方法在一个循环中 , 或在以个频繁调用的方法中,就会建立出很多不必要的对象.

  1. String s = "string" ;

建立一个实例, 而且它可以保证对于所有在同一个虚拟机中的代码, 只要他们包含相同的字符串的常量, 该对象就会被重用 .

静态工厂方法 / 构造器

对于同时提供了静态工厂方法 和 构造器的不可变类 . 通常可以使用静态工厂方法而不是构造器 , 以避免建立不必要的对象 . 如 Boolean.valueOf(String) 几乎中是优先与构造器Boolean(String) . 构造器在妹子被调用的时候都会建立一个新的对象 . 而静态工厂方法则从来不要求这样做 , 实际上也补会这样做 .

可变对象

除了重用不可变的对象之外, 也可以重用那些一直不会被修改的可变对象 .

反面例子

其中涉及可变的deta对象, 他们的值一旦计算出来之后就补会变化 . 这个类建立了一个模型, 其中有一个人, 并由一个 isBabyBoomer 方法, 用来检验这个人是否为一个baby boomer ,就是检验这个人是否出生于1947 - 1964年之间.

  1. import java.util.Calendar;
  2. import java.util.Date;
  3. import java.util.TimeZone;
  4. public class Person {
  5. private final Date birthDate;
  6. public Person(Date birthDate) {
  7. this.birthDate = birthDate;
  8. }
  9. public boolean isBabyBoomer() {
  10. Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
  11. gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
  12. Date boomStart = gmtCal.getTime();
  13. gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
  14. Date boomEnd = gmtCal.getTime();
  15. return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
  16. }
  17. }

isBabyBoomer每次被调用的时候都会建立一个 Calendar , 一个TimeZone 和两个Date 实例 . 这是不必要的 . 下面的版本用一个静态的初始化initialiser 避免了这种效率低下的情况 .

正面例子

  1. public class Person {
  2. private final Date birthDate;
  3. private static final Date BOOM_START;
  4. private static final Date BOOM_END;
  5. public Person(Date birthDate) {
  6. this.birthDate = birthDate;
  7. }
  8. static {
  9. Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
  10. gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
  11. BOOM_START = gmtCal.getTime();
  12. gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
  13. BOOM_END = gmtCal.getTime();
  14. }
  15. public boolean isBabyBoomer() {
  16. return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
  17. }
  18. }

改进后的Person类只在初始化的时候建立Calendar , TimeZone , Date 实例一次 , 而不是在每次调用isBabyBoomer 的时候都建立这些实例. 如果isBabyBoomer 方法被频繁的调用 , 这种方法将会显著地调高性能 . 除了提高性能 ,代码的含义也更加的清晰. 但是 这种优化带来的效果并不总是那么明显, 因为Calendar 实例的建立代价特别的昂贵 .

四 自动 封/拆 箱

在jdk1.5之后的发行版本中, 有一种建立多余对象的新方法 . 自动 封/拆 箱 autoboxing . 它允许程序员将基本类型和装箱基本类类型 boxed primitive type 混用 , 按需要自动装箱和拆箱. 自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来. 但是并没有完全消除. 它们在语义上还有着微妙的差别. 在性能上也有着比较明显的差别 . 考虑下面的程序. 它计算所有int正值的和, 程序必须使用long来计算. 因为int不够大 .

  1. public static void main(String[] args) {
  2. long start = System.currentTimeMillis();
  3. Long sum = 0L;
  4. for (long i = 0; i < Integer.MAX_VALUE; i++)
  5. sum += i;
  6. System.out.println(sum);
  7. System.out.println(System.currentTimeMillis() - start);
  8. }

耗时

  1. 2305843005992468481
  2. 8332

变量sum被声声明成Long 而不是long , 意味这程序构建了大约

231

多余的Long 实例 , 将sum的声明 由Long 改为long long sum = 0L; 性能提升是很显著的 . 要优先使用基本数据类型 而不是装箱基本数据类型 , 要当心无意识的自动封/拆箱 .

五 对象池

真正正确使用对象池的典型对象事例就是数据库连接池 . 建立数据库连接池的代价是非常昂贵的 , 因此重用这些对象非常的有意义. 数据库的许可可能限制你只能使用一定数量的连接 . 但是 , 一般而言 , 维护自己的对象池必定会把代码弄的很乱. 同时增加内存占用 footpring , 并且还会损害ntne性能. 现代jvm实现具有调试优化胡垃圾回收器 , 性能很容易就会超过轻量级别的对象池的性能.

六 清除过期对象的引用

当你从手工管理内存的语言 c , c++ 转换到具有垃圾回收功能的语言的时候, 程序员的工作会变得更加的容易 , 因为当你用完了对象之后, 它们会被自动回收, 当你第一次经历对像回收功能的时候 , 会觉得这简直有点不可思议. 这很容易 给你留下这样的印象 , 认为自己不再要考虑内容管理的事情了 , 其实不然 .

考虑下面简单栈实现的例子

  1. public class Stack{
  2. private Object[] elemets;
  3. private int size = 0 ;
  4. private static final int DEFAULT_INITIAL_CAPACITY = 16 ;
  5. public Statc(){
  6. elements = new Object[DEFAULT_INITIAL_CAPACITY];
  7. }
  8. public void push(Object e){
  9. ensureCapacity();
  10. elements[size++] = e;
  11. }
  12. public Object pop(){
  13. if(size == 0)
  14. throw new EmptyStackException():
  15. return elements[--size];
  16. }
  17. private void ensureCapacity(){
  18. if(elements.length == size)
  19. elements = Arrays.copyOf(elements , 2 * size + 1);
  20. }
  21. }

这段程序中并没有很明显的错误, 无论如何测试 , 它都会成功地通过第一项测试, 但是这个程序 中隐藏着一个问题, 不严格地讲 , 这段程序有一个内存泄漏, 随着垃圾回收器活动的增加,或者由于内存占用的不断增加, 程序性能的降低会逐渐表现出来. 在极端情况下,这种内存泄漏会导致磁盘交换 Disk Paging 甚至导致程序失败 OutOfMemoryError 错误 , 但是这种失败情况比较少见 .

程序中那里发生了内存泄漏呢?如果一个栈是先增长,然后再收缩 , 那么从栈中弹出来的对象将不会被当做垃圾回收, 即使使用栈的程序不在引用这些对象 ,他们也不会被回收. 这是因为 , 栈内部维护这对这些对象的过期引用 obsolete reference . 所谓的过期引用,是指永远也不会在被解除的引用. 在本例中 , 凡事在elements数组的 活动部分 active portion之外的任何引用都是过期引用. 活动部分是指elements中下标小于size的那些元素 .

在支持垃圾回收的豫园中,内存泄漏是很隐蔽 . 如果一个对象引用被无意识地保留起来, 那么垃圾回收机制不仅不会处理这个对象 , 而且也不会处理被这个对象引用的所有其他对象 . 即使只有少量的几个对象引用被无意思的保留下来, 也会有许许多多的对象被排除在垃圾回收机制之外, 从而对性能造成潜在的问题 .

这类问题的修复方法很简单,一旦对象过期,只需要清空这些引用即可.

  1. public Object pop(){
  2. if(size == 0)
  3. throw new EmptyStackException();
  4. Object result = elements[--size];
  5. elements[size] = null;
  6. return result ;
  7. }

清空过期引用的另一个好处是,如果他们以后被错误的解除引用,程序将会立即抛出NullPointerException 异常.二部是悄悄的错误的运行下去.尽快的检测出程序中的错误总是有益的 .

清空对象引用是一种例外,而不是一种规范

那么何时应该清空对象引用呢? Statck类的哪方面特性使它易于受到内存泄漏的影响呢?

问题在于,stack类自己管理内存 manage its own memory . 存储池storage pool包含了elements数组, 对象引用但愿,而不是对象本身的元素. 数组活动区域中的元素是已分配的, 而数组其余部分的元素则是自由的free . 但是垃圾回收器并不知道这一点. 对于垃圾回收器而言, elements数组中的所有对象引用多同等的有效. 只有程序员指定数组的非活动部分是不重要的. 程序员可把这个情况告诉垃圾回收器. 方法很简单, 一旦数组元素变成了非活动的一部分, 程序员就手动清空这些数组元素 .

一般而言,只要类是自己管理内存,程序员将应该警惕内存泄漏问题. 一旦元素被释放掉,则改元素包含的任何对象引用都应该被清空 .

内存泄漏的另一个常见的来源是缓存

一旦你把对象引用放到缓存中, 它就很容易被遗忘掉, 从而使得它不在有用之后很长一段时间内仍然保留在缓存中, 对于这问题, 有几种可能解决方案. 如果你正好要实现这样的缓存,值要在缓存之外在对某个项的key的引用, 该项就有意义, 那么将可以用WeakHashMap代表缓存, 当缓存中的项过期之后, 他们呢就会自动被删除. 记住值有当所要的缓存项的生命周期是由该key的外部引用而不是由value决定时, WeakHashMap 才有用处 .

更为常见的情况是, 缓存项的生命周期是否有意义 并不是很容易决定 ,随着时间的推移 , 其中项会变得越来越没有价值. 在这种情况下 , 缓存应该时不时的清除掉没用的项. 这项的清除工作可以由一个后台线程 / Timer / ScheduledThreadPoolExecutor 来完成, 或者也可以在给缓存添加新的条目时顺便清理. LinnkedHashMap类利用他的removeEldestEntry 方法可以很永日地实现后一种方案 . 对于更加复杂的缓存, 必须直接使用java.lang.ref .

内存泄漏的第三个问题常见来源监听器与其他回调

如果你实现一个API ,客户端在这个API中注册回调,却没有现实的取消注册,那么除非你采用某些动作,否则他们将会积累. 确保回掉立即被当作垃圾回收的最佳方法是只保存他们呢的弱引用weak reference , 例如将他们保存成WeakHashMap中的Key .

由于内存泄漏通常不会表现成明显的失败, 所以他们可以在一个系统中存在很多年. 往往只有通过代码检查, 或借助Heap工具 heap profiler 才能发现内存泄漏问题. 因此如果能在内存泄漏发生前将知道如何预测此类问题,并阻止它的发生,那么最好不过.

七 避免使用finalizer

finaliser 通常是不可以预测, 也是很危险的, 一般请款下是不必要 . 是用finalizer方法会导致行为不稳定, 降低性能, 以及可移植性问题.C++的程序员被告知 不要把终结方法当作是C++中的解析器 destructors 的对应方法 , 在C++中析构方法是回收一个对象所占用支援的常规方法,是构造器所必须的对象方法. 在java中迪昂一个对象变得不可到达的时候,垃圾回收器与改对象相关的存储空间,并不需要程序员做专门的工作.C++的析构方法也可以被用来回收其他的非内存资源 . 而在java中,一般用try finally 来完成类似的工作.

终结方法的缺点是不能保证被解释的执行, 从一个对象变得不可到达开始,高它的终结方法被执行,所话费的这段时间是任意长的. 这意味这,注重时间 time-critical的人物不应该由终结方法来完成.例如终结方法来关闭已经打开的文件,这是严重错误,意味打开文件的描述符是一种很有限的资源.由JVM会延迟执行终结方法,所以大量的文件会保留在打开的状态,迪昂一个程序不在能打开文件的时候,它可能运行失败 .

不应该依赖终结方法来更新重要的持久状态.

不要被System.gc / System.runFinalization 这两个方法所诱惑, 他们呢确实增加了终结方法被执行的接回,但是他们并不保证终极方法一定会被执行.保证终结方法被执行的是System.runFinalizaerOnExit / Runtime.runFinalizersOnExit , 这两个方法都有致命的缺点.

本地对等体 native peer 本地对等体是一个本地对象 native object ,普通对象通过本地方法 native method 委托给一个本地对象,因为本地对象对等体不是一个普通的java object , 所以垃圾回收器不会知道也不会回收它, 当它的java对象对等体被回收的时候,它不会被回收. 在本地对等体并不拥有关键资源的前提下, 总结方法正式执行这项任务的最合适的工具,如果本地对等体拥有必要被及时终止的资源,那么该类就应该具有一个显示的终止方法. 终止方法应该完成所有必要的工作以便 释放关键的资源. 终止方法可以是本地方法, 或者它也可以调用本地方法.

终结方法链 finalizer chaining并不会被自动执行. 如果类有终结方法, 并且子类覆盖了终结方法,子类的终结方法就鼻血手动调用超类的终结方法.你应该在一个try块中终结子类, 并在finally中调用超类的终结方法. 这样做保证即使子类终结过程抛出异常,超类的终结方法也会执行.

  1. @Override
  2. protected void finalize() throws Throwable {
  3. try{
  4. //sub class finalize
  5. }finally{
  6. super.finalize();
  7. }
  8. }

如果子类实现或者覆盖超类的终结方法,但是忘记手工调用超类终结方法,那么超类终结方法将永远不会被调用. 要防范这样的事情发生,代价就是为每个被终结的对象建立一个附加的对象. 不是班终结方法放在要求终结处理类中,而是把终结方法放在一个匿名类中. 该匿名类的唯一用途就是终结它的外围实例 enclosing instance .

匿名类的当个实例被称为终结方法守护者 finalizer guardian 外围类的每个实例都会创建这样一个守卫者. 外围实例在它的私有实例域中保留着一个对其终结方法守卫者的唯一引用, 因此终结方法守卫者与外围实例可以同时启动终结过程.当守卫者被终结的时候, 它执行外围实例所期望的终结行为,就好像它的终结方法是外围对象上的一个方法一样.

  1. //Finalzer guardian idiom
  2. public class Foo{
  3. //sole purpose of this object is to finalize outer foo objcet
  4. private final Object finalizerGuardian = new Object(){
  5. @Override
  6. protected void finalize() throws Throwable{
  7. ...// finalize outer foo object
  8. }
  9. };
  10. ...//remainder omitted
  11. }

注意共有Foo并没有终结方法(由Object中继承了一个无关紧要的之外),所以子类的终结方法是否调用super.finalize并不重要. 对于每一个带有终结方法的非final共有类, 都应该考虑使用这个方法 .

发表评论

表情:
评论列表 (有 0 条评论,399人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java对象销毁

    对象使用完之后需要对其进行清除。对象的清除是指释放对象占用的内存。在创建对象时,用户必须使用 new 操作符为对象分配内存。不过,在清除对象时,由系统自动进行内存回收,不需要用