ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【十五】不要问我JVM !—— OOM及性能优化案例与解决

2021-07-31 22:01:38  阅读:159  来源: 互联网

标签:泄漏 String Person OOM 对象 十五 JVM public 内存


一、内存泄漏案例分析(一)

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    // 入栈
    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    // 出栈
    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }

    // 扩容
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

分析
上述程序并没有明显的错误,但是这段程序有一个内存泄漏,随着GC活动的增加,或者内存占用的不断增加,程序性能的降低就会表现出来,严重时可导致内存泄漏,但是这种失败情况相对较少。

代码的主要问题在pop函数,下面通过这张图示展现:
在这里插入图片描述
当进行大量的pop操作时,由于引用未进行置空,gc是不会释放的,如下图所示:
在这里插入图片描述
解决方法
将代码中的pop()方法变成如下方法:

public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    Object result = elements[--size];
    elements[size] = null;
    return result;
}

二、Tomcat堆溢出分析

Tomcat是最常用的Java Servlet容器之一,同时也可以当做单独的Web服务器使用。Tomcat本身使用Java实现,并运行于Java虚拟机之上。在大规模请求时,Tomcat有可能会因为无法承受压力而发生内存溢出错误。这里根据一个被压垮的Tomcat的堆快照文件,来分析Tomcat在崩溃时的内部情况。

分析过程
图显示了Tomcat溢出时的总体信息,可以看到堆的大小为29.7MB。从统计饼图中得知,当前深堆最大的对象为StandardManager,它持有大约16.4MB的对象。
在这里插入图片描述
一般来说,我们总是会对占用空间最大的对象特别感兴趣,如果可以查看StandardManager内部究竟引用了哪些对象,对于分析问题可能会起到很大的帮助。因此,在饼图中单击StandardManager所在区域,在弹出菜单中选择“with outgoing references”命令,这样将会列出被StandardManager引用的所有对象。如图:
在这里插入图片描述
图显示了被StandardManager引用的对象,其中特别显眼的就是sessions对象,它占用了约17MB空间。
在这里插入图片描述
继续查找,打开sessions对象,查看被它引用的对象,如图所示。可以看到sessions对象为ConcurrentHashMap,其内部分为16个Segment。从深堆大小看,每个Segment都比较平均,大约为1MB,合计17MB。继续打开Segment,查看存储在sessions中的真实对象。如图所示,可以找到内部存放的为StandardSession对象。
在这里插入图片描述
在这里插入图片描述
通过OQL命令,查找所有的StardardSession。可以看到当前堆中含有9941个session,并且每一个session的深堆为1592字节,合计约15MB,达到当前堆大小的50%。由此,可以知道,当前Tomcat发生内存溢出的原因,极可能是由于在短期内接收大量不同客户端的请求,从而创建大量session导致。
在这里插入图片描述
为了获得更为精确的信息,可以查看每一个session的内部数据,在左侧的对象属性表中,可以看到所选中的session的最后访问时间和创建时间。
在这里插入图片描述
通过OQL命令和MAT的排序功能,可以找到当前系统中最早创建的session和最后创建的session。再根据当前的session总数,可以计算每秒的平均压力为:9941/(1403324677648-1403324645728)*1000=311次/秒。
在这里插入图片描述
由此推断,在发生Tomcat堆溢出时,Tomcat在连续30秒的时间内,平均每秒接收了约311次不同客户端的请求,创建了合计9941个session。

补充一:浅堆、深堆、保留集

浅堆(Shallow Heap)是指一个对象所消耗的内存。

在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节。根据堆快照格式不同,对象的大小可能会同8字节进行对齐。

以String为例:2个int值共占8字节,对象引用占用4字节,对象头8字节,合计20字节,向8字节对齐,故占24字节。这24字节为String对象的浅堆大小。它与String的value实际取值无关,无论字符串长度如何,浅堆大小始终是24字节。

保留集(Retained Set)
对象A的保留集指当对象A被垃圾回收后,可以被释放的所有的对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接或间接访问到的所有对象的集合。通俗地说,就是指仅被对象A所持有的对象的集合。

深堆(Retained Heap)
深堆是指对象的保留集中所有的对象的浅堆大小之和。
注意:浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。

深堆大小 = 当前对象的浅堆大小 + 对象中所包含对象的深堆大小

补充二:内存泄漏

何为内存泄露(Memory Leak)
在这里插入图片描述
在这里插入图片描述
可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。

严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。

但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也以叫做宽泛意义上的“内存泄漏”
在这里插入图片描述
对象X引用对象Y,X的生命周期比Y的生命周期长;

那么当Y生命周期结束的时候,X依然引用着Y,这时候,垃圾回收期是不会回收对象Y的;

如果对象X还引用着生命周期比较短的A、B、C,对象A又引用着对象a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。

内存泄漏与内存溢出的关系
内存泄漏(Memory Leak):申请了内存用完了不释放,比如一共有1024M的内存,分配了512M的内存一直不回收,那么可以用的内存只有512M了,仿佛泄露掉了一部分。

内存溢出(Out of Memory):申请内存时,没有足够的内存可以使用;

可见,内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出。

泄漏的分类

  • 经常发生:发生内存泄露的代码会被多次执行,每次执行,泄露一块内存;
  • 偶然发生:在某些特定情况下才会发生
  • 一次性:发生内存泄露的方法只会执行一次;
  • 隐式泄漏:一直占着内存不释放,直到执行结束;严格的说这个不算内存泄漏,因为最终释放掉了,但是如果执行时间特别长,也可能会导致内存耗尽。

内存泄漏的8种情况
静态集合类
静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

public class MemoryLeak {
    static List<Object> list = new ArrayList<>();

    public void oomTest() {
        // 局部变量
        Object obj = new Object();
        list.add(obj);
    }
}

单例模式
单例模式和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和JVM的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

内部类持有外部类
内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。

这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。

各种连接,如数据库连接、网络连接和IO连接等

在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。

否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

public class MemoryLeak {
    public static void main(String[] args) {
        Connection conn = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection("url", "", "");
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("......");
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        } finally {
            // 1. 关闭结果集Statement
            // 2. 关闭声明的对象ResultSet
            // 3. 关闭连接Connection
        }
    }
}

变量不合理的作用域
一个变量的定义的作用范围大于其使用范围,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。

public class UsingRandom {
    private String msg;

    public void receiveMsg() {
        // 从网络中接受数据保存到msg中
        readFromNet();
        // 将msg保存到数据库中
        saveDB();
    }
}

如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。

实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间。

改变哈希值
改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。

否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。这也是 String 为什么被设置成了不可变类型,我们可以放心地把 String 存入 HashSet,或者把String当做HashMap的key值;

当我们想把自己定义的类保存到散列表的时候,需要保证对象的hashCode不可变。

public class ChangeHashCode {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();
        Person p1 = new Person(1001, "AA");
        Person p2 = new Person(1002, "BB");

        set.add(p1);
        set.add(p2);

        // 导致了内存的泄漏
        p1.name = "CC";
        // 删除失败
        set.remove(p1);

        // [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}]
        System.out.println(set);

        set.add(new Person(1001, "CC"));
        // [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}]
        System.out.println(set);

        set.add(new Person(1001, "AA"));
        // [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}]
        System.out.println(set);
    }
}

@Data
class Person {
    int id;
    String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;

        Person person = (Person) o;

        if (id != person.id) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }

    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

缓存泄漏
内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存中,测试环境只有几百条数据,但是生产环境有几百万的数据。

对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有被其他对象引用那么此map会自动丢弃此值。

public class MapTest {
    static Map<String, String> wMap = new WeakHashMap<>();
    static Map<String, String> map = new HashMap<>();

    public static void main(String[] args) {
        init();
        testWeakHashMap();
        testHashMap();
    }

    public static void init() {
        String ref1 = new String("obejct1");
        String ref2 = new String("obejct2");
        String ref3 = new String("obejct3");
        String ref4 = new String("obejct4");
        wMap.put(ref1, "cacheObject1");
        wMap.put(ref2, "cacheObject2");
        map.put(ref3, "cacheObject3");
        map.put(ref4, "cacheObject4");
        System.out.println("String引用ref1,ref2,ref3,ref4 消失");

    }

    public static void testWeakHashMap() {
        System.out.println("WeakHashMap GC之前");
        System.out.println(wMap);
        try {
            System.gc();
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("WeakHashMap GC之后");
        System.out.println(wMap);
    }

    public static void testHashMap() {
        System.out.println("HashMap GC之前");
        System.out.println(map);
        try {
            System.gc();
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("HashMap GC之后");
        System.out.println(map);
    }
}
String引用ref1,ref2,ref3,ref4 消失
WeakHashMap GC之前
{obejct2=cacheObject2, obejct1=cacheObject1}
WeakHashMap GC之后
{}
HashMap GC之前
{obejct4=cacheObject4, obejct3=cacheObject3}
HashMap GC之后
{obejct4=cacheObject4, obejct3=cacheObject3}

上面代码演示WeakHashMap如何自动释放缓存对象,当init函数执行完成后,局部变量符串引用ref1,ref2,ref3,ref4都会消失,此时只有静态map中保存中对字符串对象的引用,可以看到,调用gc之后,HashMap的没有被回收,而WeakHashMap里面的缓存被回收了。

监听器和回调
内存泄漏另一个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。

需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为WeakHashMap中的键。

标签:泄漏,String,Person,OOM,对象,十五,JVM,public,内存
来源: https://blog.csdn.net/weixin_41231928/article/details/119281379

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有