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

java.lang.OutOfMemoryError: Java heap space

程序员文章站 2022-06-13 21:29:04
...

java.lang.OutOfMemoryError: Java heap space


异常描述

java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2367) ~[na:1.7.0_55]
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:130) ~[na:1.7.0_55]
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:114) ~[na:1.7.0_55]
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:415) ~[na:1.7.0_55]
at java.lang.StringBuilder.append(StringBuilder.java:132) ~[na:1.7.0_55]
at java.lang.StringBuilder.append(StringBuilder.java:128) ~[na:1.7.0_55]
at java.util.AbstractMap.toString(AbstractMap.java:523) ~[na:1.7.0_55]
at com.xxx.xxx.redis.JedisManager.setHash(JedisManager.java:84)


异常代码

    // 向 Redis 中存入数据,并设置失效时间    
    protected boolean setHash(String key, Map<String, String> map, int exp) {
        boolean ret = jedisClient.setHash(key, exp, map);
        log.debug("Redis Cache Hash Object: [" + map.toString() + "]");
        return ret;
    }

业务场景

  • 增加检索功能,数据量很大,若通过DB 中的 like 操作,访问 DB 过于频繁,增加数据库压力;且无法完成多维度检索
  • 预先按照将多个维度按顺序将数据初始化到Redis中,比如 A分类 B品类 C城市 D名称 E型号
  • 通过 hscan redis_key cursor(游标,通常取0) count pageSize(分页每页数据数量) match regex(正则) 命令匹配关键字

问题场景

  • 测试环境没有复现出上述问题,因为初始化的数据量太小,没有达到预期的峰值;等发布生产后,初始化生产环境数据时,发现初始化的操作不仅超时而且需要查询大量的表中数据,占用数据库的连接,而且代码中出现很多很多的问题
  • 通过日志查询到上述问题,当时误以为是应用服务器空间溢出,赶紧检查服务是否正常;Redis服务器也无明显异常
  • 预期数据量很大,但开发过程中同事没有正确认识到问题,曾提示过,生产的数据量起码是测试的几百倍;要做适当的启动多个线程分批处理,将数据分散,但均以时间很紧为由拒绝了我的提议,导致发布生产时耽误了大量时间
  • 临时解决方案,将原本的多个初始化任务,拆分成多个,每次运营一部分,代码实现中将各个部分冗余在一起了,注释掉一部分发版,执行,再注释,再发。。。

问题分析

  • 从日志中分析,是 map.toString() 方法发生异常
  • 多态:方法传参 Map
public class Test1 {

    public static void main(String[] args) {

        Map<String,String> hashMap = new HashMap<>();
        map(hashMap); // 输出 hashMap

        Map<String,String> linkedHashMap = new LinkedHashMap<>();
        map(linkedHashMap); // 输出 linkedHashMap hashMap 
        // 因为 LinkedHashMap<K,V> extends HashMap<K,V> 
        // 所以 LinkedHashMap 也是 HashMap
    }

    public static void map(Map<String,String> map){
        if(map instanceof LinkedHashMap){
            System.out.println("linkedHashMap");
        }
        if(map instanceof HashMap){
            System.out.println("hashMap");
        }
    }
}
  • map.toString()
// 1.此处 map 为 hashMap ,查看hashMap的toString方法
        public final String toString() {
            return getKey() + "=" + getValue();
        }
// 该 toString 方法是 HashMap 内部类  Entry 的方法,而不是 HashMap 的
// HashMap 的继承关系
public class HashMap<K,V> extends AbstractMap<K,V>
// 此处调用 AbstractMap 的 toString 方法

// 2.AbstractMap toString()
    // 通过 StringBuilder 将 key 与 value 拼接
    public String toString() {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        if (! i.hasNext())
            return "{}";

        StringBuilder sb = new StringBuilder();
        sb.append('{');
        for (;;) {
            Entry<K,V> e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            sb.append(key   == this ? "(this Map)" : key);
            sb.append('=');
            sb.append(value == this ? "(this Map)" : value);
            if (! i.hasNext())
                return sb.append('}').toString();
            sb.append(',').append(' ');
        }
    }

// 3.StringBuilder append 
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

    // 调用 AbstractStringBuilder append 方法
    public AbstractStringBuilder append(String str) {
        if (str == null) str = "null";
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }

    void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }

// 4.Arrays copy()

    public static char[] copyOf(char[] original, int newLength) {
        char[] copy = new char[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

// 此处创建新的 char[] 数据出现栈溢出异常
  • 数据长度
public class Test {
    public static void main(String[] args) {
        char[] ch = new char[999999999];
        ch[0] = 'x';
        String s = new String(ch);
        System.out.println(s);
    }
}

问题总结

  • StringBuilder:append操作,每次append都在扩容,与+相比较创建对象的次数的区别
    • ”+” 号连接:每一次+操作相当于 new StringBuilder(“a”).append(“b”) ;每一次都会创建新的对象
    • 扩容: 添加的字符串的长度 + 原来的字符串中包含的元素个数 > 数组的容量(AbstractStringBuilder 内部维护了一个 char[] value)
  • 数组的长度:不是无限大小的,有限制;因为长度是int类型,上限 Integer.MAX_VALUE
  • 日志级别:设置了日志的基本级别为info级别,此处的debug是否执行
    • debug 执行,执行debug这行代码包括map.toString,但控制台不打印
  • 服务器内存使用情况查询
    • free
  • JVM工具使用(Java VirtualVM):本地模拟如果发生栈溢出,GC的过程