`

java优化占用内存的方法(二)

    博客分类:
  • Java
 
阅读更多
(转)垃圾收集几乎是每个开发人员都喜爱的一个 Java™ 平台特性,它简化了开发,消除了所有种类的潜在代码错误。可尽管垃圾收集一般来说可以让您无需进行资源管理,有时候您还是必须自己进行一些内务处理。



显式地释放资源

Java 程序中使用的绝大多数资源都是对象,垃圾收集在清理对象方面做得很好。因此,您可以使用任意多的 String。垃圾收集器最终无需您的干预就会算出它们何时失效,并收回它们使用的内存。



另一方面,像文件句柄和套接字句柄这类非内存资源必须由程序显式地释放,比如使用 close()、destroy()、shutdown() 或 release() 这样的方法来释放。有些类,比如平台类库中的文件句柄流实现,提供终结器(finalizer)作为安全保证,以便当垃圾收集器确定程序不再使用资源而程序却忘了释放资源时,终结器还可以来做这个释放工作。但是尽管文件句柄提供了终结器来在您忘记了时为您释放资源,最好还是在使用完之后显式地释放资源。这样做可以更早地释放资源,降低了资源耗尽的可能。



对于有些资源来说,一直等到终结(finalization)释放它们是不可取的。对于重要的资源,比如锁获取和信号量许可证,Lock 或 Semaphore 直到很晚都可能不会被垃圾收集掉。对于数据库连接这样的资源,如果您等待终结,那么肯定会消耗完资源。许多数据库服务器根据许可的容量,只接受一定数量的连接。如果服务器应用程序为每个请求都打开一个新的数据库连接,然后用完之后就不管了,那么数据库远远未到终结器关闭不再需要的连接,就会到达它的最高容量。



多数资源都不会持续整个应用程序的生命周期,相反,它们只被用于一个活动的生命周期。当应用程序打开一个文件句柄读取文件以处理文档时,它通常读取文件后就不再需要文件句柄了。

在最简单的情况下,资源在同一个方法调用中被获取、使用和释放,代码如下所示

Java代码
01.//不正确地在一个方法中获取、使用和释放资源 —— 不要这样做  
02.public static Properties loadPropertiesBadly(String fileName)  
03.            throws IOException {  
04.        FileInputStream stream = new FileInputStream(fileName);  
05.        Properties props = new Properties();  
06.        props.load(stream);  
07.        stream.close();  
08.        return props;  
09.    } 
[java] view plaincopyprint?
01.//不正确地在一个方法中获取、使用和释放资源 —— 不要这样做  
02.public static Properties loadPropertiesBadly(String fileName) 
03.            throws IOException { 
04.        FileInputStream stream = new FileInputStream(fileName); 
05.        Properties props = new Properties(); 
06.        props.load(stream); 
07.        stream.close(); 
08.        return props; 
09.    } 
//不正确地在一个方法中获取、使用和释放资源 —— 不要这样做
public static Properties loadPropertiesBadly(String fileName)
            throws IOException {
        FileInputStream stream = new FileInputStream(fileName);
        Properties props = new Properties();
        props.load(stream);
        stream.close();
        return props;
    }


不幸的是,这个例子存在潜在的资源泄漏。如果一切进展顺利,流将会在方法返回之前被关闭。但是如果 props.load() 方法抛出一个 IOException,那么流则不会被关闭(直到垃圾收集器运行其终结器)。解决方案是使用 try...finally 机制来确保流被关闭,而不管是否发生错误,代码如下所示(不过他并不完善)

Java代码
01.//正确地在一个方法中获取、使用和释放资源  
02. public static Properties loadProperties(String fileName)   
03.            throws IOException {  
04.        FileInputStream stream = new FileInputStream(fileName);  
05.        try {  
06.            Properties props = new Properties();  
07.            props.load(stream);  
08.            return props;  
09.        }  
10.        finally {  
11.            stream.close();  
12.        }  
13.    } 
[java] view plaincopyprint?
01.//正确地在一个方法中获取、使用和释放资源  
02. public static Properties loadProperties(String fileName)  
03.            throws IOException { 
04.        FileInputStream stream = new FileInputStream(fileName); 
05.        try { 
06.            Properties props = new Properties(); 
07.            props.load(stream); 
08.            return props; 
09.        } 
10.        finally { 
11.            stream.close(); 
12.        } 
13.    } 
//正确地在一个方法中获取、使用和释放资源
public static Properties loadProperties(String fileName)
            throws IOException {
        FileInputStream stream = new FileInputStream(fileName);
        try {
            Properties props = new Properties();
            props.load(stream);
            return props;
        }
        finally {
            stream.close();
        }
    }


注意,资源获取(打开文件)是在 try 块外面进行的;如果把它放在 try 块中,那么即使资源获取抛出异常,finally 块也会运行。不仅该方法会不适当(您无法释放您没有获取的资源),finally 块中的代码也可能抛出其自己的异常,比如 NullPointerException。从 finally 块抛出的异常取代导致块退出的异常,这意味着原来的异常丢失了,不能用于帮助进行调试。



使用 finally 来释放在方法中获取的资源是可靠的,但是当涉及多个资源时,很容易变得难以处理。下面考虑这样一个方法,它使用一个 JDBC Connection 来执行查询和迭代 ResultSet。该方法获得一个 Connection,使用它来创建一个 Statement,并执行 Statement 以得到一个 ResultSet。但是中间 JDBC 对象 Statement 和 ResultSet 具有它们自己的 close() 方法,并且当您使用完之后,应该释放这些中间对象。然而,进行资源释放的 “明显的” 方式并不起作用,如下所示:

Java代码
01.//不成功的释放多个资源的企图 —— 不要这样做  
02.public void enumerateFoo() throws SQLException {  
03.        Statement statement = null;  
04.        ResultSet resultSet = null;  
05.        Connection connection = getConnection();  
06.        try {  
07.            statement = connection.createStatement();  
08.            resultSet = statement.executeQuery("SELECT * FROM Foo");  
09.            // Use resultSet  
10.        }  
11.        finally {  
12.            if (resultSet != null)  
13.                resultSet.close();  
14.            if (statement != null)  
15.                statement.close();  
16.            connection.close();  
17.        }  
18.    }  
19. 
20.  
[java] view plaincopyprint?
01.//不成功的释放多个资源的企图 —— 不要这样做  
02.public void enumerateFoo() throws SQLException { 
03.        Statement statement = null; 
04.        ResultSet resultSet = null; 
05.        Connection connection = getConnection(); 
06.        try { 
07.            statement = connection.createStatement(); 
08.            resultSet = statement.executeQuery("SELECT * FROM Foo"); 
09.            // Use resultSet  
10.        } 
11.        finally { 
12.            if (resultSet != null) 
13.                resultSet.close(); 
14.            if (statement != null) 
15.                statement.close(); 
16.            connection.close(); 
17.        } 
18.    } 
19. 
20.  
//不成功的释放多个资源的企图 —— 不要这样做
public void enumerateFoo() throws SQLException {
        Statement statement = null;
        ResultSet resultSet = null;
        Connection connection = getConnection();
        try {
            statement = connection.createStatement();
            resultSet = statement.executeQuery("SELECT * FROM Foo");
            // Use resultSet
        }
        finally {
            if (resultSet != null)
                resultSet.close();
            if (statement != null)
                statement.close();
            connection.close();
        }
    }


这个 “解决方案” 不成功的原因在于,ResultSet 和 Statement 的 close() 方法自己可以抛出 SQLException,这会导致后面 finally 块中的 close() 语句不执行。您在这里有几种选择,每一种都很烦人:用一个 try..catch 块封装每一个 close(),可以使用嵌套 try...finally 块,或者编写某种小型框架用于管理资源获取和释放。 

Java代码
01.//可靠的释放多个资源的方法  
02.public void enumerateBar() throws SQLException {  
03.        Statement statement = null;  
04.        ResultSet resultSet = null;  
05.        Connection connection = getConnection();  
06.        try {  
07.            statement = connection.createStatement();  
08.            resultSet = statement.executeQuery("SELECT * FROM Bar");  
09.            // Use resultSet  
10.        }  
11.        finally {  
12.            try {  
13.                if (resultSet != null)  
14.                    resultSet.close();  
15.            }  
16.            finally {  
17.                try {  
18.                    if (statement != null)  
19.                        statement.close();  
20.                }  
21.                finally {  
22.                    connection.close();  
23.                }  
24.            }  
25.        }  
26.    } 
[java] view plaincopyprint?
01.//可靠的释放多个资源的方法  
02.public void enumerateBar() throws SQLException { 
03.        Statement statement = null; 
04.        ResultSet resultSet = null; 
05.        Connection connection = getConnection(); 
06.        try { 
07.            statement = connection.createStatement(); 
08.            resultSet = statement.executeQuery("SELECT * FROM Bar"); 
09.            // Use resultSet  
10.        } 
11.        finally { 
12.            try { 
13.                if (resultSet != null) 
14.                    resultSet.close(); 
15.            } 
16.            finally { 
17.                try { 
18.                    if (statement != null) 
19.                        statement.close(); 
20.                } 
21.                finally { 
22.                    connection.close(); 
23.                } 
24.            } 
25.        } 
26.    } 
//可靠的释放多个资源的方法
public void enumerateBar() throws SQLException {
        Statement statement = null;
        ResultSet resultSet = null;
        Connection connection = getConnection();
        try {
            statement = connection.createStatement();
            resultSet = statement.executeQuery("SELECT * FROM Bar");
            // Use resultSet
        }
        finally {
            try {
                if (resultSet != null)
                    resultSet.close();
            }
            finally {
                try {
                    if (statement != null)
                        statement.close();
                }
                finally {
                    connection.close();
                }
            }
        }
    }
我们都知道应该使用 finally 来释放像数据库连接这样的重量级对象,但是我们并不总是这样细心,能够记得使用它来关闭流(毕竟,终结器会为我们做这件事,是不是?)。很容易忘记在使用资源的代码不抛出已检查的异常时使用 finally。如下代码展示了针对绑定连接的 add() 方法的实现,它使用 Semaphore 来实施绑定,并有效地允许客户机等待空间可用:

Java代码
01.//绑定连接的脆弱实现 —— 不要这样做  
02.public class LeakyBoundedSet<T> {  
03.    private final Set<T> set = ...  
04.    private final Semaphore sem;  
05.    public LeakyBoundedSet(int bound) {  
06.        sem = new Semaphore(bound);  
07.    }  
08.    public boolean add(T o) throws InterruptedException {  
09.        sem.acquire();  
10.        boolean wasAdded = set.add(o);  
11.        if (!wasAdded)  
12.            sem.release();  
13.        return wasAdded;  
14.    }  
15.} 
[java] view plaincopyprint?
01.//绑定连接的脆弱实现 —— 不要这样做  
02.public class LeakyBoundedSet<T> { 
03.    private final Set<T> set = ... 
04.    private final Semaphore sem; 
05.    public LeakyBoundedSet(int bound) { 
06.        sem = new Semaphore(bound); 
07.    } 
08.    public boolean add(T o) throws InterruptedException { 
09.        sem.acquire(); 
10.        boolean wasAdded = set.add(o); 
11.        if (!wasAdded) 
12.            sem.release(); 
13.        return wasAdded; 
14.    } 
15.} 
//绑定连接的脆弱实现 —— 不要这样做
public class LeakyBoundedSet<T> {
    private final Set<T> set = ...
    private final Semaphore sem;
    public LeakyBoundedSet(int bound) {
        sem = new Semaphore(bound);
    }
    public boolean add(T o) throws InterruptedException {
        sem.acquire();
        boolean wasAdded = set.add(o);
        if (!wasAdded)
            sem.release();
        return wasAdded;
    }
}


LeakyBoundedSet 首先等待一个许可证成为可用的(表示连接中有空间了),然后试图将元素添加到连接中。添加操作如果由于该元素已经在连接中了而失败,那么它会释放许可证(因为它不实际使用它所保留的空间)。

与 LeakyBoundedSet 有关的问题没有必要马上跳出:如果 Set.add() 抛出一个异常呢?如果 Set 实现中有缺陷,或者 equals() 或 hashCode() 实现(在 SortedSet 的情况下是 compareTo() 实现)中有缺陷,原因在于添加元素时元素已经在 Set 中了。当然,解决方案是使用 finally 来释放信号量许可证,这是一个很简单却容易被遗忘的方法。这些类型的错误很少会在测试期间暴露出来,因而成了定时 bomb,随时可能爆炸。如下代码展示了 BoundedSet 的一个更加可靠的实现:

Java代码
01.//使用一个 Semaphore 来可靠地绑定 Set  
02.public class BoundedSet<T> {  
03.    private final Set<T> set = ...  
04.    private final Semaphore sem;  
05.    public BoundedHashSet(int bound) {  
06.        sem = new Semaphore(bound);  
07.    }  
08.    public boolean add(T o) throws InterruptedException {  
09.        sem.acquire();  
10.        boolean wasAdded = false;  
11.        try {  
12.            wasAdded = set.add(o);  
13.            return wasAdded;  
14.        }  
15.        finally {  
16.            if (!wasAdded)  
17.                sem.release();  
18.        }  
19.    }  
20.} 
[java] view plaincopyprint?
01.//使用一个 Semaphore 来可靠地绑定 Set  
02.public class BoundedSet<T> { 
03.    private final Set<T> set = ... 
04.    private final Semaphore sem; 
05.    public BoundedHashSet(int bound) { 
06.        sem = new Semaphore(bound); 
07.    } 
08.    public boolean add(T o) throws InterruptedException { 
09.        sem.acquire(); 
10.        boolean wasAdded = false; 
11.        try { 
12.            wasAdded = set.add(o); 
13.            return wasAdded; 
14.        } 
15.        finally { 
16.            if (!wasAdded) 
17.                sem.release(); 
18.        } 
19.    } 
20.} 
//使用一个 Semaphore 来可靠地绑定 Set
public class BoundedSet<T> {
    private final Set<T> set = ...
    private final Semaphore sem;
    public BoundedHashSet(int bound) {
        sem = new Semaphore(bound);
    }
    public boolean add(T o) throws InterruptedException {
        sem.acquire();
        boolean wasAdded = false;
        try {
            wasAdded = set.add(o);
            return wasAdded;
        }
        finally {
            if (!wasAdded)
                sem.release();
        }
    }
}




对于具有任意生命周期的资源,我们要回到 C 语言的时代,即手动地管理资源生命周期。在一个服务器应用程序中,客户机到服务器的一个持久网络连接存在于一个会话期间(比如一个多人参与的游戏服务器),每个用户的资源(包括套接字连接)在用户退出时必须被释放。好的组织是有帮助的;如果对每个用户资源的角色引用保存在一个 ActiveUser 对象中,那么它们就可以在 ActiveUser 被释放时(无论是显式地释放,还是通过垃圾收集而释放)而被释放。



具有任意生命周期的资源几乎总是存储在一个全局集合中(或者从这里可达)。要避免资源泄漏,因此非常重要的是,要识别出资源何时不再需要了并可以从这个全局集合中删除了。此时,因为您知道资源将要被释放,任何与该资源关联的非内存资源也可以同时被释放。



确保及时的资源释放的一个关键技巧是维护所有权的一个严格层次结构,其中的所有权具有释放资源的职责。如果应用程序创建一个线程池,而线程池创建线程,线程是程序可以退出之前必须被释放的资源。但是应用程序不拥有线程,而是由线程池拥有线程,因此线程池必须负责释放线程。当然,直到它本身被应用程序释放之后,线程池才能释放线程。

维护一个所有权层次结构有助于不至于失去控制,其中每个资源拥有它获得的资源并负责释放它们。这个规则的结果是,每个不能由垃圾收集单独收集的资源(即这样的资源,它直接或间接拥有不能由垃圾收集释放的资源)必须提供某种生命周期支持,比如 close() 方法。



如果说平台库提供终结器来清除打开的文件句柄,这大大降低了忘记显式地关闭这些句柄的风险,为什么不更多地使用终结器呢?原因有很多,最重要的一个原因是,终结器很难正确编写(并且很容易编写错)。终结器不仅难以编写正确,终结的定时也是不确定的,并且不能保证终结器最终会运行。并且终结还为可终结对象的实例化和垃圾收集带来了开销。不要依赖于终结器作为释放资源的主要方式。





垃圾收集为我们做了大量可怕的资源清除工作,但是有些资源仍然需要显式的释放,比如文件句柄、套接字句柄、线程、数据库连接和信号量许可证。当资源的生命周期被绑定到特定调用帧的生命周期时,我们通常可以使用 finally 块来释放该资源,但是长期存活的资源需要一种策略来确保它们最终被释放。对于任何一个这样的对象,即它直接或间接拥有一个需要显式释放的对象,您必须提供生命周期方法 —— 比如 close()、release()、destroy() 等 —— 来确保可靠的清除。
分享到:
评论

相关推荐

    Java占用内存的研究.pdf

    特别地,Java虚拟机(JVM)管理内存的方式使得开发者需要了解一些基本的内存占用知识,以便在进行内存优化和避免内存溢出时能够做出更合理的决策。 首先,文档指出了Java中基本数据类型和对象在内存中的占用大小。在...

    JAVA进程占用高内存缘由分析与优化方法_.docx

    JAVA进程占用高内存缘由分析与优化方法 在 Java 进程中,高内存占用是一个常见的问题,本文将通过 jmap 和 ps 命令来分析 Java 进程的内存占用情况,并讨论可能的优化方法。 1. Java 进程的内存占用分析 使用 ...

    从 Java 代码到 Java 堆 理解和优化您的应用程序的内存使用

    【标题】:深入理解Java内存使用与优化:从代码到Java堆 【描述】:本文旨在帮助Java开发者深入了解从编写代码到Java堆的内存管理过程,以便更好地优化应用程序的内存使用。通过分析Java代码中的内存开销,以及讨论...

    测试java对象占用内存大小的例子

    在Java编程语言中,了解一个对象占用的内存大小是非常重要的,尤其是在优化性能或者处理大量对象时。本示例主要探讨如何测试Java对象占用的内存大小,以便更好地理解内存使用情况。 首先,`SizeOf.java`可能是一个...

    JAVA技巧(Java多线程运行时,减少内存占用量).pdf

    根据提供的文件内容,该文件主要讨论了在Java多线程环境下如何减少内存占用量。文件内容并不完整,且存在 OCR 扫描错误,但我会尝试从中提取与Java多线程和内存管理相关的知识点,并加以详细解释。 ### Java多线程...

    java-sizeof-0.0.4:一个查看java对象占用内存大小

    本文将深入探讨Java中的对象内存占用,以及如何使用"java-sizeof-0.0.4"工具来查看Java对象在内存中的大小。 在Java中,内存主要分为堆内存(Heap)和栈内存(Stack)。对象通常存储在堆内存中,而基本类型的变量和...

    LINUX类主机JAVA应用程序占用CPU、内存过高分析手段

    当用户量过大,或服务器性能不足以支持大用户量,但同时又得不到扩容的情况下,进行性能分析,并对系统、应用、程序进行优化显得尤为重要,也是节省资源的一种必不可少的手段。目前大多数运维产品都基于JAVA语言开发...

    计算一个Java对象占用字节数的方法

    在Java编程语言中,了解一个对象占用的内存字节数对于优化内存使用和理解程序性能至关重要。本篇文章将深入探讨如何计算Java对象占用的内存字节数,以及影响这一数值的因素。 首先,Java对象在堆内存中由四个部分...

    java字符串内存计算

    本文将深入探讨如何在Java中计算字符串所占用的内存空间,包括现有的计算方法、其局限性以及具体的计算公式。 #### 计算内存占用的传统方法及其局限性 在Java中,直接获取一个对象所占用的内存大小并非易事。常见...

    Java内存使用系列一Java对象的内存占用Java开发J

    在Java编程语言中,了解对象的内存占用是优化应用程序性能的关键。这个“Java内存使用系列一Java对象的内存占用”主题旨在深入探讨Java对象在内存中的表现,以及如何有效地管理这些资源。Java开发人员需要理解内存...

    JAVA 进程在64位LINUX下占用巨大内存的分析.doc

    JAVA 进程占用巨大内存的分析 在 64 位 LINUX 系统上,JAVA 进程的内存...JAVA 进程在 64 位 LINUX 下占用巨大内存是一个非常复杂的问题,但通过合理的配置和优化,我们可以解决这个问题,提高系统的性能和可靠性。

    java内存泄漏解决

    #### 二、Java内存模型与内存区域 Java虚拟机(JVM)管理着多种不同类型的内存区域,包括堆内存(Heap Memory)、方法区(Method Area)、永久代(Permanent Generation Space)等。不同的内存区域有着不同的作用和特点: ...

    为什么Java程序占用的内存比实际分配的多

    5. **本地代码(Native code)**:Java字节码可以被编译成本地机器代码,这部分代码也会占用内存。此外,JVM自身也需要内存来存放运行时数据,如垃圾收集器的数据结构。 了解了这些因素后,我们可以尝试估算Java...

    Java内存占用.pdf

    Java内存管理是Java程序性能优化的关键方面,主要关注JVM(Java虚拟机)如何使用和管理内存。在Java中,内存分为堆内存和原生内存两大部分。堆内存是Java对象的主要存储区域,由JVM负责管理,包括新生代、老年代等...

    java 对象 内存 大小

    在Java编程语言中,了解对象内存大小是优化内存使用、提高程序性能的关键步骤。当我们谈论“Java对象内存大小”时,我们通常指的是一个Java对象在内存中占据的空间,包括对象头、实例字段以及可能的对齐填充。这个...

    完美解决java读取excel内存溢出问题.rar

    总结来说,解决Java读取Excel内存溢出问题,关键在于合理利用资源、优化代码逻辑以及选择适合的API,如Apache POI的SXSSF。通过这些方法,我们可以在不显著增加系统资源负担的情况下,高效地处理大Excel文件。

    java内存机制及异常处理

    解决这些问题的方法包括但不限于调整JVM参数以增大内存分配、优化代码以减少内存占用、及时关闭不再使用的资源(如数据库连接)以及使用内存分析工具检测和修复内存泄漏。正确理解和运用Java内存机制以及异常处理...

    weblogic内存占用过大调优

    ### WebLogic内存占用过大调优方案详解 ...通过上述方法,我们可以有效地降低WebLogic服务器的内存占用,提高系统的稳定性和响应速度。同时,合理的监控和持续优化也是确保系统长期稳定运行的关键。

    java实现内存动态分配

    Java的垃圾回收机制(Garbage Collection, GC)负责自动回收不再使用的对象所占用的堆内存。当一个对象不再有引用指向它时,该对象成为垃圾,GC会在适当的时候将其回收,释放内存。在实验中,虽然没有直接涉及垃圾...

Global site tag (gtag.js) - Google Analytics