Java的GC不是万能神药!内存泄露照样能把你的应用活活耗死

发布时间:2026/7/21 4:33:02
Java的GC不是万能神药!内存泄露照样能把你的应用活活耗死 内存泄漏介绍Java具备一个极为关键的优点, 那就是, 在有助于实现自动内存管理的内置垃圾收集器, 简称为GC的助力之下达成。此GC以隐式的方式对内存的分配以及释放承担责任, 因此能够针对大多数内存泄漏问题予以处理。即便GC能够有效地去处理大部分的内存, 然而它却没办法确保为内存泄漏提供一个绝对不会出错的解决办法, GC是挺聪明的, 可并不是毫无瑕疵, 哪怕是在一个有着责任心的开发人员所构建的应用程序当中, 内存泄漏依旧会偷偷地出现。这种情形依旧有可能存在, 即应用程序制造大量冗余的对象, 进而致使关键的内存资源被耗尽, 有时还会致使整个应用程序走向失败。内存泄漏属于Java里一个实实在在的问题, 就本文而言, 我们会去知晓内存泄漏的潜在缘由, 怎样于运行时将它们辨认出来, 以及怎样在应用程序当中对它们进行处理。什么是内存泄露内存泄漏指堆里有不再用的对象, 然而垃圾回收器没能力把这些对象从内存里删掉, 那么就没必要对它们进行维护。内存泄漏是极为糟糕的, 原因在于它会致使内存资源被阻塞, 并且会随着时间的慢慢流逝而使得系统性能下降。倘若不予处理的话, 应用程序最终将会把自身的资源消耗一空, 最终会引发严重的、以致使的java.lang。堆内存中驻留着两种不同类型的对象, 一种是有引用的, 另一种是没有引用的。有引用的对象, 乃是在应用程序里仍存在活动引用的对象, 而没有引用的对象, 是不存在任何活动引用的对象。被未引用对象定期删除的垃圾回收器, 却从不收集仍在被引用的对象, 而这正是可能发生内存泄漏的所在之处。内存泄漏的症状让我们仔细看看这些场景以及如何处理它们。Java中的内存泄漏类型随便哪一个应用程序里, 内存泄漏出现存在好些缘由。这一章节当中, 我们会去谈及最为常见的那些。静态字段内存泄漏第一种可能导致潜在内存泄漏的情况是大量使用静态变量。在Java里, 静态字段的生命周期常常跟正在运行的应用程序的整个生命周期相契合, 除非类加载器具备进行垃圾收集的资格。让我们创建一个简单的Java程序来填充静态列表public class StaticTest { public static List list new ArrayList(); public void populateList() { for (int i 0; i 10000000; i) { list.add(Math.random()); } Log.info(Debug Point 2); } public static void main(String[] args) { Log.info(Debug Point 1); new StaticTest().populateList(); Log.info(Debug Point 3); } }list new ArrayList(); public void populateList() { for (int i 0; i 10000000; i) { list.add(Math.random()); } Log.info(Debug Point 2); } public static void main(String[] args) { Log.info(Debug Point 1); new StaticTest().populateList(); Log.info(Debug Point 3); } }此刻, 要是我们于程序运行之时剖析堆内存, 那么我们会瞧见在调试点1与2之间, 恰如所料那般, 堆内存有所增长。然而, 在我们于调试点3留存方法之际, 堆内存仍旧未被进行垃圾回收, 这就如同我们于响应里所目睹的那般:然而, 于上述的程序里头, 在第2行经由特定操作, 要是我们仅仅只是去除关键字, 那么这将会给内存运用带来极大的改变, 此VM响应呈现如下:直至调试点的头一部分跟我们于静态情形下所取得的成果近乎一样。可此次在我们脱离方法之后, 该列表的全部内存皆被垃圾回收, 缘由是我们对其不存在任何引用。所以, 我们得极其紧密地留意静态变量的运用情形。要是集合跟大对象被定义成是静态的话, 那它们就会在应用程序的整个存续期间都一直留存于内存里, 进而使得那些本可在其他地方得以运用的宝贵内存被阻塞住。如何预防通过未关闭的资源每当我们去建立起一个全新的连接之时, 或者去打开一个流的时候, JVM就会针对这些资源去分配内存。存在着一些示例, 其中涵盖了数据库连接, 包含着输入流, 还有会话对象。倘若忘记将这些资源关闭, 那么会造成内存被阻塞, 进而致使GC无法对它们进行访问。要是出现了异常, 使得程序执行难以抵达能够处理代码去关闭这些资源的语句, 这种情况甚至就会发生。在这两种情形之时, 资源留存的开放连接将会耗费内存, 倘若我们不加以处置, 它们或许会使性能降低, 甚至有可能致使。如何预防() 和() 实现不正确在展开新类的定义之际, 一类极为平常的疏漏情形乃是, 针对那个以及这个方法而言, 并未撰写出恰当的予以重写的方法。应用不少操作期间运用许多这些方法, 如有未恰当予以重新编写其结果的情况之下, 那它们就极有可能变成有着潜在之内存泄漏问题的源头所在了。让我们以一个普通的类为例并将其用作中的键public class Person { public String name; public Person(String name) { this.name name; } }现在我们将把重复的 对象插入使用此键的映射中。请记住映射不能包含重复的键Testpublic void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() { Map map new HashMap(); for(int i0; i100; i) { map.put(new Person(jon), 1); } Assert.assertFalse(map.size() 1); }map new HashMap(); for(int i0; i100; i) { map.put(new Person(jon), 1); } Assert.assertFalse(map.size() 1); }这里, 我们把人当作key, 因为Map不允许有重复的键, 所以我们作为键插入的大量重复对象, 不应该使内存增加。可是, 鉴于我们未曾界定正确的手段, 反复出现的对象将会累积起来进而增添内存, 这便是为啥在内存里我们见到多个对象的缘由。其中, 堆内存呈现如下表明的那样:倘若我们对及方法实施了正确的重写操作那么在这个映射里面仅存在单独的一个对象。让我们看看类的 和 的正确实现public class Person { public String name; public Person(String name) { this.name name; } Override public boolean equals(Object o) { if (o this) return true; if (!(o instanceof Person)) { return false; } Person person (Person) o; return person.name.equals(name); } Override public int hashCode() { int result 17; result 31 * result name.hashCode(); return result; } }在这种情况下以下断言是正确的Testpublic void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() { Map map new HashMap(); for(int i0; i2; i) { map.put(new Person(jon), 1); } Assert.assertTrue(map.size() 1); }map new HashMap(); for(int i0; i2; i) { map.put(new Person(jon), 1); } Assert.assertTrue(map.size() 1); }以及被正确重写过后, 那个同一程序的堆内存呈现出如下所示的情况:再一个事例是运用类似这般的ORM工具, 其借助以及方式去剖析对象进而把它们存放于缓存里。要是不把这些方法重新写, 内存泄漏的可能性就会特别高, 因没办法比较对象, 还会拿重复的对象去填充其缓存。如何预防引用外部类的内部类这种情形出现于非静态内部类匿名类的状况下, 对于初始化而言, 这些内部类一直都需要封闭类的实例。在默认情形下, 每一个并非静态的内部类, 皆存在对其包含类的隐式引用。要是我们于应用程序里使用这个内部类对象, 那么即便在包含类的对象超出范围以后, 它依旧不会被进行垃圾回收。思考一个类别, 此类别里存有针对大量体积庞大对象的引用, 而且还具备一个并非静态的内部类别。当下, 当我们去创建一个仅仅包含着内部类别的对象时, 其内存在模型呈现出如下这般的情况:但是如果只将内部类声明为那么相同的内存模型如下所示这种情况之所以发生, 是由于内部类对象会隐式持有对外部类对象的引用, 进而致使其成为垃圾回收的无效候选对象匿名类遭遇的也是状况相同的情形。