在上一篇文章中我们讲了引用外部类的内部类导致内存溢出的问题以及如何解决,本节继续分析其他可能引起java内存泄露的场景:
通过 finalize() 方法
终结器finalizers的使用是潜在内存泄漏问题的另一个来源。每当类的finalize()
方法被重写时,该类的对象不会立即被垃圾回收。相反,GC将它们排队等待最后确定,这将在稍后的时间点发生。
另外,如果我们的应用程序不能更快地完成或最终处理一个错误,那么如果我们的应用程序不能更快地完成一个错误,那么我们的应用程序就不能完成。
为了证明这一点,让我们考虑一下,我们已经为一个类重写了finalize()
方法,并且该方法需要一点时间来执行。当此类的大量对象被垃圾回收时,在VisualVM中,它看起来像:
但是,如果我们只删除重写的finalize()
方法,则同一个程序会给出以下响应:
如何预防?
我们应该避免使用终结器
内部字符串
当Java 7从PermGen转移到HeapSpace时,Java字符串池经历了一次重大变化。但是对于在版本6及以下运行的应用程序,我们在处理大字符串时应该更加注意。
如果我们读取一个巨大的字符串对象,并在该对象上调用intern()
,那么它将进入字符串池,该池位于PermGen(永久内存)中,只要我们的应用程序运行,它就会一直留在那里。这会阻塞内存并在我们的应用程序中造成内存泄漏。
JVM 1.6中这个例子的PermGen在VisualVM中如下所示:
与此相反,在一个方法中,如果我们只是从文件中读取一个字符串,而不是对其进行内接,那么PermGen看起来像:
如何预防?
- 解决这个问题最简单的方法是升级到最新的Java版本,因为从JavaVersion7开始,字符串池被移到HeapSpace
- 如果要处理大型字符串,请增大PermGen空间的大小,以避免任何潜在的OutOfMemoryError:
-XX:MaxPermSize=512m
使用ThreadLocals
ThreadLocal是一种构造,它使我们能够将状态隔离到特定线程,从而允许我们实现线程安全。
当使用这个结构时,每个线程都将持有一个对其ThreadLocal变量副本的隐式引用,并将维护自己的副本,而不是在多个线程之间共享资源,只要线程是活动的。
尽管ThreadLocal变量有很多优点,但是它的使用还是有争议的,因为如果使用不当,它们会导致内存泄漏。Joshua Bloch曾经评论过线程本地用法:
线程池的草率使用与线程局部变量的草率使用可能会导致意外的对象保留,正如在许多地方所指出的那样。但把责任推到线程本地上是没有道理的。
threadlocal导致内存泄漏
一旦保持线程不再活动,threadlocal
就应该被垃圾回收。但是当threadlocal
与现代应用服务器一起使用时,问题就出现了。
现代应用服务器使用一个线程池来处理请求,而不是创建新的请求(例如apache tomcat中的Executor
)。此外,它们还使用单独的类加载器。
由于应用程序服务器中的线程池遵循线程重用的概念,因此它们永远不会被垃圾回收,而是被重用以服务于另一个请求。
现在,如果任何类创建了一个ThreadLocal
变量,但没有显式地删除它,那么即使在web应用程序停止之后,该对象的副本也将保留在工作线程中,从而防止对象被垃圾回收。
如何预防?
- 当线程局部变量不再使用时,最好清理它们——ThreadLocals提供
remove()
方法,它删除当前线程对于这个变量的值 - 请勿使用
ThreadLocals.set(null)
清除值-它实际上并不清除值,而是查找与当前线程相关联的映射,并将键值对分别设置为当前线程和null - 最好将ThreadLocal视为需要在finally块中关闭的资源,以确保它始终处于关闭状态,即使在出现异常的情况下:
try {
threadLocal.set(System.nanoTime());
//... further processing
}
finally {
threadLocal.remove();
}
解决内存泄漏的其他方法
虽然在处理内存泄漏时没有一刀切的解决方案,但是我们可以通过一些方法将这些泄漏最小化。
启用分析
Java探查器是监视和诊断应用程序内存泄漏的工具。它们分析应用程序内部发生的事情—例如,如何分配内存。
使用探查器,我们可以比较不同的方法,并找到可以最佳利用资源的领域。
在本教程我们一直在使用javavisualvm。请查看我们的Java探查器指南,了解不同类型的探查器,如任务控制、JProfiler、YourKit、Java VisualVM和Netbeans探查器。
详细的垃圾收集
通过启用详细的垃圾收集,我们可以跟踪GC的详细跟踪。要实现这一点,我们需要在JVM配置中添加以下内容:
-verbose:gc
通过添加此参数,我们可以看到GC内部发生的详细情况:
使用引用对象以避免内存泄漏
我们还可以使用Java中内置的引用对象java.lang.ref处理内存泄漏的包。使用java.lang.ref
包,而不是直接引用对象,我们使用对对象的特殊引用,以便于对它们进行垃圾回收。
引用队列旨在让我们知道垃圾回收器执行的操作。
Eclipse内存泄漏警告
对于jdk1.5及更高版本的项目,每当遇到明显的内存泄漏情况时,Eclipse都会显示警告和错误。因此,在Eclipse中开发时,我们可以定期访问“Problems”选项卡,并对内存泄漏警告(如果有)保持警惕:
Benchmarking基准测试
我们可以通过执行基准测试来测量和分析Java代码的性能。这样,我们就可以比较不同方法的性能来完成相同的任务。这可以帮助我们选择更好的方法,也可以帮助我们保存记忆。
有关基准测试的更多信息,请访问我们的Java微基准标记教程。
代码评审
最后,我们总是采用经典的、老派的方法来完成简单的代码遍历。
在某些情况下,即使是这种看起来微不足道的方法也有助于消除一些常见的内存泄漏问题。
结论
通俗地说,我们可以认为内存泄漏是一种通过阻塞重要内存资源而降低应用程序性能的疾病。而且,与所有其他疾病一样,如果得不到治愈,随着时间的推移,它会导致致命的应用程序崩溃。
内存泄漏很难解决,找到它们需要对Java语言进行复杂的掌握。在处理内存泄漏时,没有一刀切的解决方案,因为泄漏可以通过各种各样的事件发生。
但是,如果我们求助于最佳实践并定期执行严格的代码遍历和分析,那么我们就可以将应用程序中内存泄漏的风险降到最低。
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/911.html
暂无评论