编程错误实例的剖析[1]内存不足与GC的错误用法
这类文章没有一个主题范围,主要是将现实中遇到的比较有代表性、值得一说的错误(烂大街的错误就不说了~)进行详细的讨论。遇到了就写,也算是个人总结了。
今天这个错误是我拜访一个朋友时,去他那里偶然遇到的,由于时间关系,当下没有跟他讨论。后来想了想,这个问题很有意思,也有点让人哭笑不得。
这个朋友当时正在开发安卓应用的一个手机拍照模块,这片代码据他所说是网上抄的,为了解决内存不足的问题,这里给出代码片段:
Image img = null;
try{
img = new Image(.....);//为拍照开辟堆区内存空间(Image只为说明意思,并不是准确的类名)
}
catch(OutOfMemoryError e){
System.gc();//调用GC,处理堆碎片
img = new Image(.....);//重新试图为拍照开辟堆区内存空间
}
我第一次看到这个代码片时,给我的印象是:槽点太多,没法吐。
首先我们先来说说Error这个东西,先明确一点,和Exception一样,Error也继承自Throwable,因此对其catch是没有任何语法错误的,但是我认为对它catch没有什么意义。
在大学初学Java时,就明确了Error与Exception的区别,相比于Exception,Error往往意味着严重的问题,它的出现往往意味着程序无法执行(没有修补的余地),尤其是内存不足这种错误,我觉得catch到了你也没法解决了。
之后的代码更为奇特,它显示地调用了gc,gc在整个JVM中占有不少分量,从名字上讲确实是垃圾处理,而且,它确实有清理堆区内存的作用,但gc绝对不是显示调用的。
我们看一下对于gc的文档:
这里注意两个我标注出来的词:
suggests为建议、提醒之意,也就是说它是一个消息过程而非处理过程。
made a best effort意为进了最大努力去做,言外之意是也有可能不会做。
事实上,在重写finalize()的试验中,我们知道即使是显示的调用gc,对象也未必会被回收,事实上,在内存足够的时候,gc很有可能不去回收,以保证整体的执行效率。
我个人认为,除了用于测试某些功能外,任何情况下都不应该显示地调用gc,如果你想主动地标记无用对象,赋值为null就可以了,如果觉得内存不够用,你可以修改jvm启动参数,gc就是为了将程序员从监视各种堆区内存释放的繁重工作中解放出来,完全不必要在显示地使用了。
最后还有一个问题,它不是技术层面的,是逻辑层面的,我写一个代码片段,看看有没有问题:
try{
访问硬盘
}
catch(硬盘错误 e){
向硬盘中写入“硬盘出错”日志。
}
不难发现,如果硬盘出错,那向硬盘写入日志这个操作是有潜在风险的,它极有可能执行不成功,这样,你的这个日志也就毫无意义。这和上面的代码片犯的是同一个错误,在内存不足时我们根本没有理由再去执行gc,更没有理由去企图分配更多的内存,这是一个本末倒置的错误,而它常见于IOException与close方法,将close单独写到捕获IOException的catch块中也是不合理的(起码还要加一个catch块,捕获close的异常)。
最后,我觉得还是把他遇到的这个问题的解决思路写一下,我觉得对于内存分配问题可以从这几方面着手:
对于没有缓冲区的对象,我们对其建立对应的缓冲区,也就是用各种Buffered类封装一下。
对于频繁调用堆区的过程(例如在一个按钮的点击过程中new了对象),应从结构上出发,将其改成单例模式。
可以更改jvm启动参数,扩大内存的分配(我觉得这是下策,如果结构有问题,再多内存也不行)。