java.lang.OutOfMemoryError: Java heap space

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的过程
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值