内存泄漏和内存溢出
内存泄露:申请的内存空间没有被正确释放,导致内存空间被占用,并且之后也不会使用。
内存溢出:申请的内存空间超过了空闲内存空间,即内存不够使用。
所以说,内存泄漏可能会导致内存溢出,我们需要注意有可能会导致内存泄漏的情况。
常见的内存泄露原因:
-
静态变量引用:
在JDK8中,没有永久代的概念了,静态集合也都有可能被垃圾回收。但是通常,静态集合被设计为缓存数据或者维护全局状态,其内容可能会随着应用的运行而动态变化,并且可能被多个线程同时访问和修改。由于缓存数据的特点,如果使用不当就会导致内存泄露例子。比如下面这个例子,即使这些元素已经不再需求,list中元素也没有被清空或者移除,它们仍然会一直存在与list,从而占用内存。import java.util.ArrayList; import java.util.List; public class StaticListLeakExample { private static List<String> list = new ArrayList<>(); public static void add(String value) { list.add(value); } public static int size() { return list.size(); } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000000; i++) { add("value" + i); if (size() % 100 == 0) { System.out.println("size: " + size()); Thread.sleep(1000); // 模拟其他操作 } } } }
为了避免这种情况,我们可以在不再需要静态集合中的元素时,手动将其从集合中移除,或者使用弱引用等更加安全和灵活的解决方案,以便及时释放内存并提高应用程序的性能。
以下是使用弱引用解决内存泄漏问题的示例代码:
import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; public class WeakListExample { private static List<WeakReference<String>> list = new ArrayList<>(); public static void add(String value) { list.add(new WeakReference<>(value)); } public static int size() { return list.size(); } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000000; i++) { add("value" + i); if (size() % 100 == 0) { System.out.println("size: " + size()); Thread.sleep(1000); // 模拟其他操作 } } } }
在上述示例中,我们使用
java.lang.ref.WeakReference
类来包装String
类型的元素,将其添加到静态集合list
中。因为WeakReference
是一种弱引用,即只要该对象没有被强引用所持有,垃圾回收器就可以随时回收它。这意味着,如果list
中的某个元素不再被任何强引用所持有,那么它就会被自动回收。需要注意的是,由于弱引用可能会被垃圾回收器提前回收,因此当我们从
WeakReference
对象中获取元素时,需要先判断该元素是否已经被回收,以避免出现NullPointerException
等异常。具体来说,我们可以使用WeakReference
类的get()
方法来获取元素,并判断其返回值是否为null。总之,使用弱引用可以避免静态集合导致的内存泄漏问题,但也需要注意弱引用可能被提前回收的特点,并在代码中做好相应的处理。
-
单例模式
单利模式是一种常见的设计模式,它确保一个类只有一个实例,并提供全局访问点。但是如果这个单例对象持有了其他对象的引用,而这些被引用的对象不再使用却仍然存在于内存,就会导致内存泄露。
-
数据库连接、IO、Socket等连接
创建的连接不再使用时,需要调用close方法关闭连接,只有连接被关闭,GC才会回收对应的对象(Connection,Statement、ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被GC回收。
try { Connection conn = null; Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("url", "", ""); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("...."); } catch (Exception e) { }finally { //不关闭连接 }
-
变量不合理的作用域
一个变量的定义作用于大于其使用范围,很可能存在内存泄露;或不再将使用对象设置为null,很可能导内存泄露的发生。
public class Simple { Object object; public void method1(){ object = new Object(); //...其他代码 //由于作用域原因,method1执行完成之后,object 对象所分配的内存不会马上释放 object = null; } }
-
ThreadLocal使用不当
从上图可以看出来,hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在一个外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null,而value还存在着强引用,只有thread线程退出以后,value的强引用链条才会断掉。如果当前线程一直不结束掉,那么这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
key 使用强引用:
当hreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用:
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
正确使用:
-
及时清理ThreadLocal的引用
当一个线程结束时,其中的 ThreadLocal 副本会被释放,但它所持有的对象可能仍然存在于内存中。因此,为了避免内存泄漏,需要及时清理对应的 ThreadLocal 引用。这通常可以通过在使用完毕后将 ThreadLocal 变量设为 null 来实现。
-
避免创建过多的ThreadLocal对象
每个ThreadLocal对象都会占用一定的内存空间,并且会在每个线程上创建一个副本。因此如果创建过多的ThreadLocal对象,就会占用大量的内存空间,甚至导致内存溢出问题。可以考虑将多个相关的变量整合到同一个ThreadLocal对象中减少对象数据。
-
使用静态的ThreadLcoal变量,要注意不要直接将其定义为静态成员变量,因为这样会让该变量跨越多个类加载器而导致内存泄露。可以考虑使用
ThreadLocal.withInitial()
方法来创建静态的ThreadLocal变量。Java 虚拟机中,每个类加载器都有自己的命名空间(namespace),不同的类加载器之间可以加载同名但不同版本的类。当一个线程在一个类加载器中创建了静态的 ThreadLocal 变量,并将其定义为该类的静态成员变量时,如果这个线程后续在另一个类加载器中加载了同名但不同版本的类,并且访问了该类的静态成员变量(包括 ThreadLocal 变量),就会出现内存泄漏问题。
这是因为,在多个类加载器中加载的同名类,虽然名称相同但实际上是不同的类类型,它们拥有各自独立的静态成员变量和 ThreadLocal 变量副本。而对于一个线程来说,它只能访问到在同一类加载器中加载的同名类的静态成员变量和 ThreadLocal 变量副本,而无法访问其他类加载器中的变量副本。因此,如果在一个类加载器中创建了静态的 ThreadLocal 变量,并且直接将其定义为静态成员变量,就会导致该变量跨越多个类加载器而产生内存泄漏问题
-
避免在循环中重复创建 ThreadLocal 对象。ThreadLocal应当设计为全局使用。
-
-
Hash值发生变化
对象 Hash 值改变,使用 HashMap、HashSet 等容器中时候,由于对象修改之后的 Hah 值和存储进容器时的 Hash 值不同,所以无法找到存入的对象,自然也无法单独删除了,这也会造成内存泄漏。说句题外话,这也是为什么 String 类型被设置成了不可变类型。
在 Java 中,
HashMap
和HashSet
的元素的hash
值是由元素的key
和value
共同决定的。具体来说,对于一个HashMap
或HashSet
中的元素,其hash
值的计算方式如下:- 如果该元素的
key
或value
为null
,那么它的hash
值也为0
。 - 如果该元素的
key
或value
不为null
,首先会调用它们的hashCode()
方法计算出它们各自的hash
值。 - 然后将两个
hash
值通过异或运算(^
)组合起来作为该元素的最终hash
值。
这种方式可以保证
HashMap
或HashSet
中的每个元素都有唯一的hash
值,并且能够尽量避免碰撞(即不同元素拥有相同的hash
值)。 - 如果该元素的
快来和博主一起学习~
文章评论