Java平台最突出的功能之一是其自动内存管理。 许多人错误地将此功能转换为Java中没有内存泄漏 。 但是,事实并非如此,我给人的印象是,现代Java框架和基于Java的平台,尤其是Android平台,越来越与这种错误的假设相矛盾。 为了对Java平台上的内存泄漏如何产生印象,请查看以下堆栈实现:
class SimpleStack {
private final Object[] objectPool = new Object[10];
private int pointer = -1;
public Object pop() {
if(pointer < 0) {
throw new IllegalStateException("no elements on stack");
}
return objectPool[pointer--];
}
public Object peek() {
if(pointer < 0) {
throw new IllegalStateException("no elements on stack");
}
return objectPool[pointer];
}
public void push(Object object) {
if(pointer > 8) {
throw new IllegalStateException("stack overflow");
}
objectPool[++pointer] = object;
}
}
此堆栈实现以数组形式存储其内容,并另外管理一个指向当前活动堆栈单元的整数。 每当元素从堆栈顶部弹出时,此实现都会导致内存泄漏。 更准确地说,堆栈将保留对数组顶部元素的引用,即使不再使用它也是如此。 (除非再次将其压入堆栈,否则将导致引用被完全相同的引用覆盖。)因此,即使在释放了对该对象的所有其他引用之后,Java也将无法对其进行垃圾回收。 由于堆栈实现不允许直接访问基础对象池,因此,在将新元素推入堆栈的相同索引之前,此不可访问的引用将阻止对引用对象进行垃圾回收。
幸运的是,这种内存泄漏很容易解决:
public Object pop() {
if(pointer < 1) {
throw new IllegalStateException("no elements on stack");
}
try {
return objectPool[pointer];
} finally {
objectPool[pointer--] = null;
}
}
当然,在日常Java开发中,内存结构的实现并不是一项非常常见的任务。 因此,让我们看一个更常见的Java内存泄漏示例。 这种泄漏通常是由常用的观察者模式引起的 :
class Observed {
public interface Observer {
void update();
}
private Collection<Observer> observers = new HashSet<Observer>();
void addListener(Observer observer) {
observers.add(observer);
}
void removeListener(Observer observer) {
observers.remove(observer);
}
}
这次,存在一种允许直接从基础对象池中删除引用的方法。 只要任何已注册的观察者在使用后都从外部取消注册,就不会在此实现中担心任何内存泄漏。 但是,请设想一个场景,在这种情况下,您或框架的用户在使用观察器后会忘记注销注册。 同样,观察者将永远不会被垃圾回收,因为观察者会一直引用它。 更糟糕的是,如果没有对这个现在无用的观察者的引用,就不可能从外部从观察者的对象池中删除观察者。
但是,这种潜在的内存泄漏也很容易解决,其中涉及使用弱引用 ,这是我个人希望程序员会更加意识到的Java平台功能。 简而言之,弱引用的行为类似于普通引用,但不会阻止垃圾回收。 因此,如果没有剩余的强引用,并且JVM执行了垃圾回收,则可以突然发现弱引用为null。 使用弱引用,我们可以像这样更改上面的代码:
private Collection<Observer> observers = Collections.newSetFromMap(
new WeakHashMap<Observer, Boolean>());
WeakHashMap是地图的现成实现,使用弱引用包装其键。 通过此更改,被观察者将不会阻止其观察者进行垃圾收集。 但是,您应该始终在Java文档中指出此行为! 如果您的代码用户想要像日志实用程序一样向您的观察者注册永久观察者,而他们不打算对其进行引用,则可能会造成很大的混乱。 例如,Android的OnSharedPreferencesChangeListener使用弱引用来监听,而没有记录此功能。 这可以让您彻夜难眠!
在本博客文章的开头,我建议当今的许多框架都需要其用户进行仔细的内存管理,并且我想就该主题至少给出两个示例来解释这一问题。
Android平台:
Android编程为核心应用程序类引入了生命周期编程模型。 总而言之,这意味着您无法控制自己创建和管理这些类的对象实例,而是可以在需要时由Android OS为您创建它们。 (例如,如果您的应用程序应该显示特定的屏幕。)以同样的方式,Android将决定何时不再需要特定的实例(例如,当用户关闭应用程序的屏幕时),并通知您有关信息。通过在实例上调用特定的生命周期方法进行删除。 但是,如果让对该对象的引用进入某些全局上下文,则Android JVM将无法按照其意图进行垃圾回收。 由于Android手机通常在内存方面受到限制,并且因为Android的对象创建和销毁例程甚至对于简单的应用程序都可能变得非常疯狂,因此您必须格外小心以清理引用。
不幸的是,对核心应用程序类的引用很容易消失。 在下面的示例中,您可以发现滑动参考吗?
class ExampleActivity extends Activity {
@Override
public void onCreate(Bundle bundle) {
startService(new Intent(this, ExampleService.class).putExtra("mykey",
new Serializable() {
public String getInfo() {
return "myinfo";
}
}));
}
}
如果您认为这是intent的构造函数中的this引用,那您是错误的。 该意图仅用作服务的启动命令,并且在服务启动后将被删除。 取而代之的是,匿名内部类将保留对其封闭类的引用,即ExampleActivity类。 如果接收的ExampleService保留对该匿名类的实例的引用,则结果还将保留对ExampleActivity实例的引用。 因此,我只能建议Android开发人员避免使用匿名类。
Web应用程序框架(特别是
Web应用程序框架通常在会话中存储半永久性用户数据。 无论您写入会话的什么内容,通常都会在内存中保留不确定的时间。 如果您在有大量访问者的情况下浪费了会话,则servlet容器的JVM迟早会打包。 Wicket框架是需要格外小心的一个极端示例:Wicket序列化用户以版本化状态访问的任何页面。 简单地说,这意味着如果网站的访问者之一单击您的欢迎页面十次,Wicket将以其默认配置在您的硬盘驱动器上存储十个序列化对象。 这需要格外小心,因为Wicket页面对象持有的所有引用都将导致这些引用对象与页面一起被序列化。 看一下这个不好的实践Wicket示例:
class ExampleWelcomePage extends WebPage {
private final List<People> peopleList;
public ExampleWelcomePage (PageParameters pageParameters) {
peopleList = new Service().getWorldPhonebook();
}
}
通过十次单击欢迎页面,您的用户仅将十本世界电话簿副本存储在服务器硬盘驱动器上。 因此,请始终在Wicket应用程序中使用LoadableDetachableModel ,它将为您提供参考管理。
跟踪Java应用程序中的内存泄漏可能很麻烦,因此,我想将JProfiler命名为有用的(但不幸的是非免费的)调试工具。 它允许您以堆转储的形式浏览Java正在运行的应用程序的内部。 如果内存泄漏对于您的应用程序来说是一个问题,我建议您尝试一下JProfiler。 有可用的评估许可证。
进一步的阅读 :如果要在自定义类加载器时看到另一个有趣的内存泄漏事件,请参阅Zeroturnaround博客 。
翻译自: https://www.javacodegeeks.com/2014/01/memory-leaks-and-memory-management-in-java-applications.html