良好的Java编码习惯
Posted 水獭同学
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了良好的Java编码习惯相关的知识,希望对你有一定的参考价值。
Item5:消除过期对象的引用
JVM为我们实现了GC(垃圾回收)的功能,让我们从手工管理内存中解放了出来,这固然很好,但并不意味着我们就再也不需要去考虑内存管理的事情了;我们用简单的栈实现的例子来解释:
public class Stack { private Object[] elements; private in size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack(){ elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e){ ensureCapacity(); elements[size++] = e; } public Object pop(){ if(size == 0){ return new EmptyStackException(); } return element[--size]; } //保证每当栈满就会自动扩容为原来的两倍 private void ensureCapacity(){ if(elements.length == size){ elements = Arrays.copyOf(elements, 2*size+1); } } }
这段程序没有什么明显的错误,无论怎么测试,结果似乎都是正确的,但不严格的讲,这段程序存在"内存泄漏"的风险,极端情况下,会导致磁盘交换(Disk Paging),甚至是程序失败(OutOfMemoryError);
为什么是内存泄漏?书中给出的解释是:从栈中pop出来的对象的引用还被栈内部维护着,这些引用被称作"过期引用"(指下标小于size的那些元素的引用),所以,GC不会去处理该对象以及该对象所引用的其他对象,内存就被这样渐渐"填满"导致溢出.
修复方法,改写pop()
public Object pop(){ if(size == 0){ throw new EmptyStackException(); } Object result = element[--size]; elements[size] = null; //清空出栈对象的引用,告知GC可以释放其资源
return result; }
手动将过期引用告知垃圾回收器还有的好处就是,如果之后又用了此过期引用,程序就会报空指针异常,而不是一直"错误下去"..
内存泄漏的另一常见来源是缓存,对象引用在缓存中就很容易被忘记,如果不用它会长时间驻留缓存.对应的几种解决方法:
1.使用WeakHashMap代表缓存,该类拥有expungeStaleEntries方法,用于清除那些外部没有引用并且缓存内存在的键,来段代码说明:
public class Test { public static void main(String[] args) throws Exception { String a = new String("a"); String b = new String("b"); Map weakmap = new WeakHashMap(); Map map = new HashMap(); map.put(a, "aaa"); map.put(b, "bbb"); weakmap.put(a, "aaa"); weakmap.put(b, "bbb"); map.remove(a); a=null; b=null; System.gc(); } }
由于HashMap移除了a的引用,且清除了a的外部引用,此时a的引用只有WeakHashMap来维护了,此时WeakHashMap会自动舍弃掉a,但HashMap还保存着b的引用,所以b不会被WeakHashMap丢弃掉。在工程当中WeakHashMap是很实用的,我们使用短时间内就过期的缓存时最好使用weakHashMap。
2.可以将清除过期引用的任务交给“Time”或“ScheduledThreadPoolExecutor”来完成
即定时清除过期引用。
3.使用LinkedHashMap当做缓存,调用removeEldesttEntry方法清除过期引用
4.直接使用java.lang.ref
说道ref包,就绕不开Java的四类引用:强引用,软引用,弱引用,虚引用。这里大致概括一下,具体引用就不过多赘述,细节可以参照深入探讨java.lang.ref包
强引用:只要引用存在,垃圾回收器永远不会回收,Object obj = new Object()的形式
软引用:非必须引用,内存溢出之前进行回收,可以通过以下代码实现,SoftReference<Object> sf = new SoftReference<Object>(obj)的形式,通过sf.get()获取对象,软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
弱引用:第二次垃圾回收时回收,WeakReference<Object> wf = new WeakReference<Object>(obj)的形式,弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。
虚引用:垃圾回收时回收,无法通过引用取到对象值,PhantomReference<Object> pf = new PhantomReference<Object>(obj)的形式,pf.isEnQueued()主要用于检测对象是否已经从内存中删除。
关于内存泄漏的检测,往往不会表现的很明显,但它能在系统中存在很多年,只有通过检查代码或借助于Heap剖析工具(Heap Profiler)发现问题,关于Heap Profiler我有机会在单独介绍。
Item6:避免使用终结方法(finalizer)
下面由一段代码引入:
/** * @author YHW * @ClassName: Test1 * @Description: * @date 2019/1/4 20:55 */ public class Test1 { public static void main(String[] args){ Bob bob = new Bob(); System.out.println(bob.getState()); Thread thread = new Thread(){ @Override public void run() { super.run(); try{ sleep(2000); }catch(Exception e){ e.printStackTrace(); }finally{ bob.setClosed(true); } } }; thread.start(); System.out.println(bob.getState()); } }
运行结果截图:
在main方法中开了一个子线程,我故意睡了2秒,故根本走不到终结方法那里,主程序就”关闭“了,这时候子线程还没执行完,这有可能因此而产生终结对象的速度达不到对象入队的速度,出现内存泄漏而死掉,所以我们不应该以来终结方法来更新重要的持久状态,而且并没有很轻便的方法能够避免这样的问题;
由此引出了终结方法所具备的特性:
1.finalizer方法的线程优先级比当前程序的其他线程优先级要低,且JAVA语言规范不保证任何线程中finalizer方法的执行;
2.及时执行终结方法是JVM的一大功能,但在不同的JVM实现都大相径庭,有时候终结方法的线程优先级会非常低,造成JVM“没时间”释放不用的资源,引起OutOfMemoryError;
3.唯一声称保证终结方法执行的System.runFinalizersOnExit和Runtime.runFinalizersOnExit都存在致命缺陷,已经废弃了;
4.finalizer会有非常严重的性能损失;
以上是关于良好的Java编码习惯的主要内容,如果未能解决你的问题,请参考以下文章