java8以后字符串常量池的位置,以及元空间的探秘,使用VisualVM进行实战验证 文章结论:根据变化情况,推出字符串常量池在堆的old区,字符串在Young的Eden区产生,调用intern()就是先看old区有没有,如果没有将这个对象存入old区中

解决方案

使用JDK1.8


2.3未关闭的流操作

忘记关闭流是一种非常常见的情况,当然,大多数开发人员都可以涉及到这种情况

@Test(expected = OutOfMemoryError.class)
public void givenURL_whenUnclosedStream_thenOutOfMemory()
  throws IOException, URISyntaxException {
    String str = "";
    URLConnection conn 
      = new URL("http://norvig.com/big.txt").openConnection();
    BufferedReader br = new BufferedReader(
      new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
    
    while (br.readLine() != null) {
        str += br.readLine();
    } 
    
    //
}
引用文本

java 分析内存溢出泄露问题 java内存泄漏解决办法_jvm


正如我们所看到的,堆的使用随着时间的推移逐渐增加 - 这是由于未关闭流引起的内存泄漏的直接影响。


从技术上讲,未关闭的流将导致两种类型的泄漏–低级资源泄漏和内存泄漏。


低级资源泄漏只是操作系统级资源的泄漏-例如文件描述符,打开的连接等。这些资源也可能泄漏,就像内存一样。


当然,JVM也使用内存来跟踪这些基础资源,这就是为什么这也会导致内存泄漏的原因。

怎么解决

我们始终需要记住手动关闭流,或者利用Java 8中引入的自动关闭功能:

try (BufferedReader br = new BufferedReader(
  new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
    // further implementation
} catch (IOException e) {
    e.printStackTrace();
}

在这种情况下,BufferedReader将在try语句的末尾自动关闭,而无需在显式的finally块中将其关闭。


2.4. 未关闭的连接

这种情况与前一种情况非常相似,主要区别在于处理未关闭的连接(例如,到数据库,到FTP服务器等)。 再次,不正确的实现可能造成很大的危害,从而导致内存问题。

让我们来看一个简单的例子:

@Test(expected = OutOfMemoryError.class)
public void givenConnection_whenUnclosed_thenOutOfMemory()
  throws IOException, URISyntaxException {
    
    URL url = new URL("ftp://speedtest.tele2.net");
    URLConnection urlc = url.openConnection();
    InputStream is = urlc.getInputStream();
    String str = "";
    
    //
}

URLConnection保持打开状态,结果是可以预料的是内存泄漏

java 分析内存溢出泄露问题 java内存泄漏解决办法_内存泄漏_02

请注意,垃圾收集器无法执行任何操作来释放未使用但有引用的内存。

如何解决

当然是关闭连接


2.5在HashSet中加入没有重写hashcode()和equals()的对象

一个可能导致内存泄漏的简单但非常常见的示例是将**未重写hashCode()或equals()**的对象HashSet与一起使用。
当我们将未重写hashcode()和equals()的对象加入HashSet中,HashSet会不断增长,而不是应该忽略重复
添加后,我们也将无法删除这些对象。

让我们创建一个不包含equals或hashCode的简单类:

public class Key {
    public String key;
    
    public Key(String key) {
        Key.key = key;
    }
}
@Test(expected = OutOfMemoryError.class)
public void givenMap_whenNoEqualsNoHashCodeMethods_thenOutOfMemory()
  throws IOException, URISyntaxException {
    Map<Object, Object> map = System.getProperties();
    while (true) {
        map.put(new Key("key"), "value");
    }
}

java 分析内存溢出泄露问题 java内存泄漏解决办法_jvm_03

请注意,垃圾收集器在1:40停止了回收内存的操作,我们注意到了内存泄漏; 此后,GC收集数量几乎下降了四倍。

如何解决

在这种情况下,解决方案很简单-提供hashCode()和equals()实现至关重要。
这里值得一提的工具是Lombok项目-它通过注释提供了许多默认实现,例如 @EqualsAndHashCode。


2.6[补充] 引用外部类的内部类

内部类和静态内部类简单介绍


内部类:就是我是你的一部分,我了解你,我知道你的全部,没有你就没有我。(所以内部类对象是以外部类对象存在为前提的)

静态内部类:就是我跟你没关系,自己可以完全独立存在,但是我就借你的壳用一下,来隐藏自己。

总而言之,每一个非静态内部类都有一个对包含它的类的隐式引用,就算外部类对象没用了也内部类也GC不掉
但是静态内部类没有。

考虑一个类,它声明了大量复杂笨重类的字段
现在,我们创建一个内部类,因为非静态内部类无法脱离外部类,所以需要先实例化外部类,通过外部类实例化内部类

Outer outer = new Outer("Nested"); // 实例化一个Outer
  Outer.Inner inner = outer.new Inner(); // 实例化一个Inner
  inner.hello();

实例化一个Inner Class不能脱离Outer实例。

现在内存模型长这样

java 分析内存溢出泄露问题 java内存泄漏解决办法_内部类_04


但是如果我们使用静态内部类

java 分析内存溢出泄露问题 java内存泄漏解决办法_jvm_05


发生这种情况是因为内部类对象隐式保留对外类对象的引用,从而使其成为垃圾收集的无效候选者。

在匿名类的情况下也是如此。例如
public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer("Nested");
        outer.asyncHello();
    }
}

class Outer {
    private String name;

    Outer(String name) {
        this.name = name;
    }

    void asyncHello() {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello, " + Outer.this.name);
            }
        };
        new Thread(r).start();
    }
}

观察asyncHello()方法,我们在方法内部实例化了一个Runnable。Runnable本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了Runnable接口的匿名类,并且通过new实例化该匿名类,然后转型为Runnable。

如何解决

在不使用外部类非静态成员变量的情况下,考虑使用静态内部类


参考地址:

  1. https://stackify.com/memory-leaks-java/ ↩︎
  2. https://www.baeldung.com/java-memory-leaks ↩︎