欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

使用Java创建内存泄漏

程序员文章站 2022-07-14 18:26:30
...

我刚刚接受采访,并被要求使用Java造成内存泄漏
不用说,我对如何开始创建它一无所知。

一个例子是什么?


#1楼

最近,我遇到了一种更细微的资源泄漏。 我们通过类加载器的getResourceAsStream打开资源,并且碰巧输入流句柄没有关闭。

嗯,你可能会说,真是个白痴。

好吧,使这一点有趣的是:这样,您可以泄漏基础进程的堆内存,而不是从JVM的堆中泄漏。

您需要的是一个jar文件,其中包含一个文件,该文件将从Java代码中引用。 jar文件越大,分配的内存越快。

您可以使用以下类轻松创建这样的jar:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

只需将其粘贴到名为BigJarCreator.java的文件中,然后从命令行编译并运行它:

javac BigJarCreator.java
java -cp . BigJarCreator

等:您在当前工作目录中找到一个jar存档,其中包含两个文件。

让我们创建第二个类:

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

此类基本上不执行任何操作,但会创建未引用的InputStream对象。 这些对象将立即被垃圾回收,因此不会增加堆大小。 对于我们的示例而言,从jar文件加载现有资源很重要,大小在这里很重要!

如果您不确定,请尝试编译并启动上述类,但请确保选择合适的堆大小(2 MB):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

您不会在这里遇到OOM错误,因为不保留任何引用,因此无论您在上面的示例中选择ITERATIONS多大,应用程序都将继续运行。 除非应用程序进入wait命令,否则进程(在顶部(RES / RSS)或进程浏览器中可见)的内存消耗会增加。 在上面的设置中,它将分配大约150 MB的内存。

如果您希望应用程序安全播放,请在创建它的位置关闭输入流:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

并且您的进程将不会超过35 MB,与迭代计数无关。

非常简单和令人惊讶。


#2楼

直到终止线程才收集线程。 它们是垃圾收集的根源 。 它们是为数不多的仅通过忘记它们或清除对它们的引用就不会回收的对象之一。

考虑:终止工作线程的基本模式是设置一些线程看到的条件变量。 线程可以定期检查变量并将其用作终止信号。 如果未将变量声明为volatile ,则线程可能看不到对变量的更改,因此它将不知道终止。 或者想象一下,如果某些线程想要更​​新共享库,但是在尝试锁定共享库时却死锁。

如果您只有少数几个线程,则这些错误可能很明显,因为您的程序将停止正常运行。 如果您有一个线程池根据需要创建更多线程,则可能不会注意到过时的/阻塞的线程,并且它们会无限期地累积,从而导致内存泄漏。 线程可能会在您的应用程序中使用其他数据,因此也将阻止收集直接引用的任何内容。

作为一个玩具示例:

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

leakMe调用System.gc() ,但是传递给leakMe的对象将永远不会消失。

(*编辑*)


#3楼

什么是内存泄漏:

  • 这是由错误错误的设计引起的
  • 这是浪费内存。
  • 随着时间的流逝,情况变得越来越糟。
  • 垃圾收集器无法清理它。

典型示例:

缓存对象是弄乱事物的一个很好的起点。

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

您的缓存越来越大。 很快,整个数据库就被吸入了内存。 更好的设计使用LRUMap(仅将最近使用的对象保留在缓存中)。

当然,您可以使事情复杂得多:

  • 使用ThreadLocal构造。
  • 添加更复杂的参考树
  • 或由第三方图书馆引起的泄漏。

经常发生的情况:

如果此Info对象具有对其他对象的引用,则该对象又具有对其他对象的引用。 从某种意义上讲,您还可以认为这是某种内存泄漏(由不良的设计引起)。


#4楼

采访者可能正在寻找像下面的代码这样的循环引用(顺便说一句,这种引用只是在使用引用计数的非常老的JVM中泄漏内存,现在不再如此)。 但这是一个非常模糊的问题,因此这是展示您对JVM内存管理的了解的绝佳机会。

class A {
    B bRef;
}

class B {
    A aRef;
}

public class Main {
    public static void main(String args[]) {
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
    } /* main */
}

然后,您可以解释为使用引用计数,以上代码将泄漏内存。 但是大多数现代JVM不再使用引用计数,大多数使用清除垃圾收集器,实际上它将收集此内存。

接下来,您可能会解释如何创建一个具有基础本机资源的对象,如下所示:

public class Main {
    public static void main(String args[]) {
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    }
}

然后,您可以从技术上解释这是内存泄漏,但实际上,泄漏是由JVM中分配基础本机资源的本机代码引起的,而Java代码并未释放这些本机资源。

归根结底,对于现代JVM,您需要编写一些Java代码,以在JVM感知的正常范围之外分配本地资源。


#5楼

也许通过JNI使用外部本机代码?

使用纯Java,几乎是不可能的。

但这与“标准”类型的内存泄漏有关,当您无法再访问该内存时,它仍归应用程序所有。 您可以保留对未使用对象的引用,也可以在不关闭流的情况下打开流。


#6楼

一个简单的事情是使用带有不正确(或不存在)的hashCode()equals()的HashSet,然后继续添加“重复项”。 该集合只会不断增长,而您将无法删除它们,而不会忽略应有的重复项。

如果您希望这些错误的键/元素徘徊,可以使用静态字段,例如

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

#7楼

创建一个静态地图,并继续为其添加硬引用。 这些将永远不会被GC。

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}

#8楼

每当您保留对不再需要的对象的引用时,就会发生内存泄漏。 请参阅处理Java程序中的内存泄漏以获取有关内存泄漏如何在Java中表现出来以及如何处理的示例。


#9楼

如果您不了解JDBC ,那么以下是一个毫无意义的示例。 或至少JDBC希望开发人员在放弃它们,丢失对它们的引用之前关闭ConnectionStatementResultSet实例,而不是依赖finalize的实现。

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

上面的问题是Connection对象没有关闭,因此物理连接将保持打开状态,直到垃圾回收器出现并且无法访问为止。 GC将调用finalize方法,但是有些JDBC驱动程序没有实现finalize ,至少与实现Connection.close方式不同。 导致的行为是,由于将收集无法访问的对象而将回收内存,而与Connection对象关联的资源(包括内存)可能根本不会回收。

如果Connectionfinalize方法无法清除所有内容,则实际上可能会发现与数据库服务器的物理连接将持续多个垃圾回收周期,直到数据库服务器最终确定该连接未**为止(如果确实如此),则应将其关闭。

即使JDBC驱动程序要实现finalize ,也有可能在finalize期间引发异常。 最终的结果是,与finalt对象相关联的任何内存都不会被回收,因为finalize只能被调用一次。

上面在对象完成期间遇到异常的方案与可能导致内存泄漏的另一种方案-对象复活有关。 对象复活通常是通过从另一个对象最终确定对对象的强烈引用来有意识地完成的。 当对象复活被滥用时,将导致内存泄漏以及其他内存泄漏源。

您还可以想到更多示例,例如

  • 管理仅添加到列表而不删除列表的List实例(尽管您应该摆脱不再需要的元素),或者
  • 打开SocketFile ,但是在不再需要它们时不关闭它们(类似于上面涉及Connection类的示例)。
  • 关闭Java EE应用程序时不卸载Singleton。 显然,加载单例类的Classloader将保留对该类的引用,因此单例实例将永远不会被收集。 部署应用程序的新实例时,通常会创建一个新的类加载器,并且由于单例的原因,以前的类加载器将继续存在。

#10楼

这是在纯Java中创建真正的内存泄漏(运行代码无法访问但仍存储在内存中的对象)的好方法:

  1. 该应用程序创建一个长时间运行的线程(或使用线程池更快地泄漏)。
  2. 线程通过(可选的自定义) ClassLoader
  3. 该类分配大量内存(例如new byte[1000000] ),在静态字段中存储对它的强引用,然后在ThreadLocal存储对自身的引用。 分配额外的内存是可选的(泄漏类实例就足够了),但是它将使泄漏工作快得多。
  4. 该应用程序清除对自定义类或从其加载的ClassLoader所有引用。
  5. 重复。

由于在Oracle的JDK中实现ThreadLocal的方式,这会造成内存泄漏:

  • 每个Thread都有一个私有字段threadLocals ,它实际上存储线程本地值。
  • 此映射中的每个都是对ThreadLocal对象的弱引用,因此在该ThreadLocal对象被垃圾回收之后,其条目将从映射中删除。
  • 但是每个都是一个强引用,因此,当一个值(直接或间接)指向作为其ThreadLocal对象时,只要该线程存在,该对象就不会被垃圾回收或从映射中删除。

在此示例中,强引用链如下所示:

Thread对象→ threadLocals映射→示例类的实例→示例类→静态ThreadLocal字段→ ThreadLocal对象。

ClassLoader在创建泄漏中并没有真正发挥作用,它只是由于以下附加引用链而使泄漏更糟:示例类→ ClassLoader →它已加载的所有类。在许多JVM实现中,甚至更糟。在Java 7之前,因为类和ClassLoader是直接分配到permgen中的,所以根本不会进行垃圾回收。)

这种模式的一个变种是,如果您频繁地重新部署恰巧使用ThreadLocal的应用程序,而应用程序容器(例如Tomcat) ThreadLocal某种方式指向自身,那么它可以像筛子一样泄漏内存。 发生这种情况的原因可能很多,而且很难调试和/或修复。

更新 :由于很多人一直在要求它,因此以下示例代码展示了这种行为


#11楼

ArrayList.remove(int)的实现可能是可能的内存泄漏的最简单示例之一,以及如何避免它的发生:

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

如果您自己实现它,您是否会考虑清除不再使用的数组元素( elementData[--size] = null )? 该引用可能会使一个巨大的对象保持活动状态...


#12楼

GUI代码中的一个常见示例是在创建窗口小部件/组件并将侦听器添加到某个静态/应用程序作用域对象时,然后在销毁窗口小部件时不删除侦听器。 不仅会发生内存泄漏,而且还会降低性能,因为无论您在听什么火灾事件,所有旧的监听器都将被调用。


#13楼

与PermGen和XML解析有关,我有一个很好的“内存泄漏”。 我们使用的XML解析器(我不记得是哪个解析器)对标记名称进行了String.intern()处理,以使比较更快。 我们的一位客户是个很棒的主意,它不以XML属性或文本形式存储数据值,而是以标记名的形式存储数据,因此我们拥有一个文档,例如:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

实际上,他们没有使用数字,而是使用更长的文本ID(大约20个字符),这些ID是唯一的,每天以10-15百万的速度出现。 每天将产生200 MB的垃圾,不再需要,也不再进行GC(因为它在PermGen中)。 我们将permgen设置为512 MB,因此大约需要两天的时间才能看到内存不足异常(OOME)。


#14楼

除了被遗忘的侦听器,静态引用,哈希映射中的伪造/可修改键的标准情况,或者只是线程卡住而没有任何机会终止其生命周期的标准情况,下面将有一个不明显的情况,即Java泄漏。

  • File.deleteOnExit() -始终泄漏字符串, 如果字符串是子字符串,则泄漏会更加严重(底层的char []也被泄漏) - 在Java 7中,子字符串还复制了char[] ,因此后者不适用 ; @Daniel,不过不需要投票。

我将集中讨论线程,以大体上显示非托管线程的危险,甚至不希望摆动。

  • Runtime.addShutdownHook而不是不删除...,即使由于ThreadGroup类中关于未启动线程的错误(可能无法收集未启动的线程)导致的错误,即使使用removeShutdownHook也可能有效地泄漏了ThreadGroup。 JGroup在GossipRouter中泄漏。

  • 创建(而不是启动) Thread与上述类别相同。

  • 创建线程将继承ContextClassLoaderAccessControlContext ,以及ThreadGroup和任何InheritedThreadLocal ,所有这些引用都是潜在的泄漏,包括类加载器加载的整个类,所有静态引用以及ja-ja。 在具有超简单ThreadFactory接口的整个jucExecutor框架中,效果尤其明显,但是大多数开发人员都不知道潜伏的危险。 另外,很多库都根据请求启动线程(太多了行业流行的库)。

  • ThreadLocal缓存; 在许多情况下,这些都是邪恶的。 我敢肯定,每个人都已经看到了很多基于ThreadLocal的简单缓存,这是个坏消息:如果线程在类ClassLoader上下文中的寿命超过预期,那将是一个很好的泄漏。 除非确实需要,否则不要使用ThreadLocal缓存。

  • 当ThreadGroup本身没有线程,但仍保留子ThreadGroups时,调用ThreadGroup.destroy() 。 严重的泄漏将阻止ThreadGroup从其父级中移除,但是所有子级都变得无法枚举。

  • 使用WeakHashMap和值(in)直接引用键。 如果没有堆转储,这是很难找到的。 这适用于所有扩展的Weak/SoftReference ,这些扩展可能会将硬引用保留回受保护的对象。

  • 使用具有HTTP(S)协议的java.net.URL并从(!)加载资源。 这很特殊, KeepAliveCache在系统ThreadGroup中创建了一个新线程,该线程泄漏了当前线程的上下文类加载器。 当不存在活动线程时,将在第一个请求时创建该线程,因此您可能会很幸运,或者只是泄漏。 Java 7中已经修复了该泄漏,并且正确创建线程的代码删除了上下文类加载器。 还有更多的情况( 像ImageFetcher 也修复了创建类似线程的问题)。

  • 使用InflaterInputStream通过new java.util.zip.Inflater()在构造函数( PNGImageDecoder例如),而不是调用end()充气的。 好吧,如果您传入new的构造函数,就没有机会了……是的,如果手动将其作为构造函数参数传递,则在流上调用close()不会关闭充气机。 这不是真正的泄漏,因为它将由终结器释放……在其认为必要时。 直到那一刻,它严重消耗了本机内存,可能导致Linux oom_killer毫无惩罚地杀死进程。 主要问题是Java中的终结处理非常不可靠,G1恶化到7.0.2。 故事的寓意:尽快释放本机资源; 终结器太差了。

  • java.util.zip.Deflater相同。 由于Deflater在Java中占用大量内存,因此这一情况要糟得多,即,始终使用15位(最大)和8个内存级别(最大9个)分配数百KB的本机内存。 幸运的是, Deflater并未得到广泛使用,据我所知JDK不包含任何误用。 如果您手动创建DeflaterInflater始终调用end() 。 最后两个最好的部分: 您无法通过可用的常规配置工具找到它们。

(我可以根据要求添加更多遇到的时间浪费者。)

祝你好运,保持安全; 泄漏是邪恶的!


#15楼

这里的大多数示例都是“太复杂”。 他们是极端情况。 在这些示例中,程序员犯了一个错误(例如,不重新定义equals / hashcode),或者被JVM / JAVA的一个极端情况(带有静态类的负载...)所困扰。 我认为这不是面试官想要的例子,甚至不是最常见的情况。

但是确实存在内存泄漏的简单情况。 垃圾收集器仅释放不再引用的内容。 作为Java开发人员,我们不关心内存。 我们在需要时分配它,并使其自动释放。 精细。

但是任何长期存在的应用程序都倾向于具有共享状态。 它可以是任何东西,静态函数,单例……通常,非平凡的应用程序倾向于制作复杂的对象图。 只是忘记将引用设置为null或更经常忘记从集合中删除一个对象就足以导致内存泄漏。

当然,如果处理不当,则所有类型的侦听器(如UI侦听器),缓存或任何长期存在的共享状态都可能导致内存泄漏。 应该理解的是,这不是Java的极端情况,也不是垃圾收集器的问题。 这是一个设计问题。 我们设计为长时间生存的对象添加一个侦听器,但是在不再需要时不删除该侦听器。 我们缓存对象,但是我们没有策略从缓存中删除它们。

我们可能有一个复杂的图,它存储了计算所需的先前状态。 但是以前的状态本身链接到之前的状态,依此类推。

就像我们必须关闭SQL连接或文件一样。 我们需要将适当的引用设置为null并从集合中删除元素。 我们将拥有适当的缓存策略(最大内存大小,元素数或计时器)。 所有允许通知侦听器的对象都必须提供addListener和removeListener方法。 并且当这些通知器不再使用时,它们必须清除其侦听器列表。

确实确实有可能发生内存泄漏,并且完全可以预测。 无需特殊的语言功能或特殊情况。 内存泄漏要么表明某些东西可能丢失,要么表明设计问题。


#16楼

静态字段保存对象参考[特别是最终字段]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

在冗长的String上调用String.intern()

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

(未关闭)打开的流(文件,网络等...)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

未封闭的连接

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

JVM的垃圾收集器无法访问的区域 ,例如通过本机方法分配的内存

在Web应用程序中,某些对象存储在应用程序范围内,直到显式停止或删除该应用程序为止。

getServletContext().setAttribute("SOME_MAP", map);

错误或不合适的JVM选项 ,例如防止未使用的类垃圾回收的IBM JDK上的noclassgc选项

请参阅IBM jdk设置


#17楼

答案完全取决于访问者认为他们要问的内容。

在实践中是否有可能使Java泄漏? 当然可以,其他答案中也有很多例子。

但是可能会问多个元问题?

  • 理论上“完美”的Java实现是否容易受到泄漏的影响?
  • 候选人是否了解理论与现实之间的区别?
  • 候选人是否了解垃圾收集的工作原理?
  • 还是应该在理想情况下进行垃圾收集?
  • 他们是否知道可以通过本机界面调用其他语言?
  • 他们知道以其他语言泄漏内存吗?
  • 候选人甚至不知道什么是内存管理以及Java幕后发生了什么吗?

我正在将您的元问题阅读为“在这种采访情况下我可以使用的答案是什么”。 因此,我将专注于面试技巧而不是Java。 我相信您比在需要知道如何使Java泄漏的地方更容易重复这种情况,即在面试中不知道问题的答案。 因此,希望这会有所帮助。

您可以为面试开发的最重要技能之一是学会积极倾听问题并与面试官一起提取意图。 这不仅可以让您以他们想要的方式回答他们的问题,而且还表明您具备一些至关重要的沟通技巧。 当涉及到许多同样有才华的开发人员之间进行选择时,我会雇用一名在每次响应之前都会进行聆听,思考和理解的开发人员。


#18楼

我可以从这里复制答案: 导致Java内存泄漏的最简单方法?

“在计算机科学中,内存泄漏(或在这种情况下为泄漏)是在计算机程序消耗内存但无法将其释放回操作系统时发生的。” (*)

简单的答案是:您不能。 Java会执行自动内存管理,并将释放您不需要的资源。 您无法阻止这种情况的发生。 它将始终能够释放资源。 在具有手动内存管理功能的程序中,这是不同的。 您无法使用malloc()在C中获得一些内存。 要释放内存,您需要malloc返回的指针并对其调用free()。 但是,如果您再也没有指针(被覆盖或超过了生存期),那么很遗憾,您将无法释放该内存,从而导致内存泄漏。

到目前为止,所有其他答案在我的定义中并不是真正的内存泄漏。 它们都旨在快速地用毫无意义的东西填充内存。 但是在任何时候,您仍然可以取消引用创建的对象,从而释放内存->无泄漏。 acconrad的答案非常接近,但我不得不承认,因为他的解决方案实际上是通过迫使其无限循环来“崩溃”垃圾收集器)。

长答案是:您可以通过使用JNI编写Java库来获得内存泄漏,该库可以进行手动内存管理,从而导致内存泄漏。 如果调用此库,则Java进程将泄漏内存。 或者,您可以在JVM中出现错误,以便JVM释放内存。 JVM中可能存在错误,甚至可能有一些已知的错误,因为垃圾回收并不是那么简单,但是那仍然是一个错误。 通过设计,这是不可能的。 您可能会要求一些受此类错误影响的Java代码。 抱歉,我不知道,在下一个Java版本中也可能不再是bug。


#19楼

我认为一个有效的示例可能是在线程池化的环境中使用ThreadLocal变量。

例如,使用Servlet中的ThreadLocal变量与其他Web组件进行通信,使线程由容器创建并在池中维护空闲线程。 如果未正确清除ThreadLocal变量,则该变量将一直存在,直到可能相同的Web组件覆盖它们的值为止。

当然,一旦确定问题就可以轻松解决。


#20楼

我认为还没有人说过:您可以通过重写finalize()方法来使对象复活,以便finalize()在某个地方存储对此的引用。 垃圾收集器将仅在对象上被调用一次,因此之后该对象将永远不会被破坏。


#21楼

使用在任何servlet容器(Tomcat,Jetty,Glassfish等)中运行的任何Web应用程序。 连续10或20次重新部署该应用程序(仅触摸服务器的自动部署目录中的WAR可能就足够了。

除非有人对此进行了实际测试,否则几经重新部署后,您很可能会收到OutOfMemoryError的错误,因为该应用程序在执行之后并未对其进行清理。 通过此测试,您甚至可能在服务器中发现错误。

问题是,容器的寿命比应用程序的寿命更长。 您必须确保可以对容器可能对应用程序的对象或类的所有引用进行垃圾收集。

如果只有一个参考幸免于Web应用程序的取消部署,则相应的类加载器以及结果将无法垃圾收集Web应用程序的所有类。

由您的应用程序启动的线程,ThreadLocal变量,日志记录附加程序是导致类加载器泄漏的一些常见嫌疑人。


#22楼

面试官可能一直在寻找圆形参考解决方案:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

这是引用计数垃圾收集器的经典问题。 然后,您要礼貌地解释JVM使用了一种更复杂的算法,没有此限制。

-韦斯·塔勒


#23楼

我认为没有人使用内部类示例很有趣。 如果您有内部课程; 它固有地维护对包含类的引用。 当然,从技术上讲,这不是内存泄漏,因为Java最终会清除它; 但这会导致课程停留时间比预期的更长。

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

现在,如果您调用Example1并获得一个Example2来丢弃Example1,则您仍然固有地具有指向Example1对象的链接。

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

我还听到有谣言说,如果您的变量存在的时间超过特定的时间; Java假定它将一直存在,并且如果再也无法在代码中实现,则实际上将永远不会尝试对其进行清理。 但这是完全未经证实的。


#24楼

有很多不同的情况,内存会泄漏。 我遇到的一个地方,暴露了一张不应在其他地方公开和使用的地图。

public class ServiceFactory {

private Map<String, Service> services;

private static ServiceFactory singleton;

private ServiceFactory() {
    services = new HashMap<String, Service>();
}

public static synchronized ServiceFactory getDefault() {

    if (singleton == null) {
        singleton = new ServiceFactory();
    }
    return singleton;
}

public void addService(String name, Service serv) {
    services.put(name, serv);
}

public void removeService(String name) {
    services.remove(name);
}

public Service getService(String name, Service serv) {
    return services.get(name);
}

// the problematic api, which expose the map.
//and user can do quite a lot of thing from this api.
//for example, create service reference and forget to dispose or set it null
//in all this is a dangerous api, and should not expose 
public Map<String, Service> getAllServices() {
    return services;
}

}

// resource class is a heavy class
class Service {

}

#25楼

您可以使用sun.misc.Unsafe类使内存泄漏。 实际上,此服务类在不同的标准类中使用(例如,在java.nio类中)。 您不能直接创建此类的实例 ,但是您可以使用反射来实现

代码无法在Eclipse IDE中进行编译-使用命令javac对其进行javac (在编译过程中,您会收到警告)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}

#26楼

这是通过http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29进行的简单介绍。

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

因为子字符串是指原始的内部表示形式(长度更长),所以原始形式保留在内存中。 因此,只要您有一个StringLeaker处于运行状态,就可以将整个原始字符串存储在内存中,即使您可能会认为只是保留一个单字符字符串。

避免存储对原始字符串的不必要引用的方法是执行以下操作:

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

为了增加.intern() ,您还可以.intern()子字符串:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

这样即使将StringLeaker实例丢弃后,原始长字符串和派生子字符串也将保留在内存中。


#27楼

正如很多人所建议的那样,资源泄漏很容易引起-就像JDBC示例一样。 实际的内存泄漏要难一些-特别是如果您不依靠JVM的损坏位来为您做的话...

创建具有很大占用空间的对象然后无法访问它们的想法也不是真正的内存泄漏。 如果没有人可以访问它,那么它将被垃圾回收,如果有东西可以访问它,那么这不是泄漏。

但是, 曾经起作用的一种方法-我不知道它是否仍然起作用-是拥有三层深的圆形链条。 正如对象A中对对象B的引用,对象B中对对象C的引用,对象C中对对象A的引用。GC非常聪明,足以知道两条深链-如A <-> B -如果A和B无法通过其他任何方式访问,但无法处理三通链,则可以安全地收集它...


#28楼

我最近遇到了由log4j引起的内存泄漏情况。

Log4j具有称为嵌套诊断上下文(NDC)的机制,该机制可区分来自不同源的交错日志输出。 NDC工作的粒度是线程,因此它将日志输出与不同线程分开。

为了存储特定于线程的标记,log4j的NDC类使用一个Hashtable,该哈希表由Thread对象本身作为键(而不是说线程ID),因此直到NDC标签保留在内存中的所有挂起线程的对象对象也保留在内存中。 在我们的Web应用程序中,我们使用NDC标记带有请求ID的日志输出,以区分日志和单个请求。 将NDC标签与线程相关联的容器,在从请求返回响应时也将其删除。 在处理请求的过程中,产生了一个子线程,出现了以下问题:

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

因此,NDC上下文与生成的内联线程相关联。 线程对象是此NDC上下文的关键,它是内联线程,该线程内悬挂了hugeList对象。 因此,即使在线程完成其正在执行的操作之后,NDC上下文Hastable仍保持对hugeList的引用,从而导致内存泄漏。


#29楼

每个人总是忘记本机代码路由。 这是泄漏的简单公式:

  1. 声明本机方法。
  2. 在本机方法中,调用malloc 。 不要打free电话。
  3. 调用本机方法。

请记住,本机代码中的内存分配来自JVM堆。


#30楼

您可以通过在该类的finalize方法中创建该类的新实例来创建移动内存泄漏。 如果终结器创建了多个实例,则可获得加分。 这是一个简单的程序,根据您的堆大小,它会在几秒钟到几分钟之间的某个时间泄漏整个堆:

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}