首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
宝塔
V2EX  ›  Java

[虚心请教] JDK8 默认的配置环境下,10G 的堆内存 Full GC 一次要 16s 左右,该如何优化?处理一个请求内存耗费大约 900M

  •  1
     
  •   summer7 · 32 天前 · 3794 次点击
    这是一个创建于 32 天前的主题,其中的信息可能已经有所发展或是发生改变。
    第 1 条附言  ·  32 天前
    补充描述:
    1、程序简要描述
    从底层数据库(非传统意义的数据库,像 limit 分页、存储过程等常见特性是不支持的)实时查询数据(暂定每次 30000,后期可能还会增大结果数),对查询数据做解析( json 解析以及字符串切割操作,必须操作),之后将解析后的数据存入 redis
    2、目前的实现方式:
    resultSet.next 遍历完存入一个 List 中,对 List 用并行流进行解析为 List<Map>结构(测试机器 32 核 cpu 跑满,30000 条数据需 2-4s,但是碰上 GC,耗时基本就是 20s+,观察 GC 日志是发生了 Full GC,16s+)
    ps:楼下有老哥提出 next 遍历出一条就解析一条,这个待我一试
    3、目前仅能采用这种单机应用的模式,甚至后期上线还可能是配置较低虚拟机,比开发测试机器差的很多的那种。。。
    4、另有关于 JDBC 的一些疑问,还请大佬指点(迷惑了好久)
    ResultSet rs = ps.executeQuery("xxxxx sql");
    while( rs.next){
    //取字段
    }
    问题 1:executeQuery 方法返回 ResultSet 时是否代表着数据库端已经将 sql 执行完毕?
    问题 2:ResultSet 对象存储的是什么?应该不是全部的 sql 执行结果吧。( debug 看了下当前用的数据库 resultSet.next 每次只是向数据库服务端发请求获取一部分数据放入本地内存,不知其他数据库是否也是这样?还是说每种数据库有不同的方式)
    第 2 条附言  ·  32 天前
    看到各位老哥的回复,在此统一感谢一下,谢谢大家。


    根据各位的回复思考大概总结了下,在现有单机的情况下大概有以下几种方案觉得是可行的:
    1、next 遍历一条解析一条
    2、调整 young 和 old 的大小(准备把 young 调大一点测试验证一下)
    3、用 G1 垃圾收集器(刚刚突击了解了一下)
    4、堆外内存(还需要了解一下)


    其他的方案
    1、拆表(目前底层存储不允许,所有数据存在一张超大表中)
    2、分布式(这个在目前的条件不允许,只能用单机应用)
    3、换语言(这个嗯。。。有时间可以一试)



    再再再补充描述:
    Full GC 一次基本所有内存就全回收了( FullGC 一次 16s+),与刚启动占用内存基本一致。
    Full GC 原因观察:一小部分是因为 MetaSpace 满了(存疑,为什么这个也会涨),大部分原因是内存要吃满导致的。这个预计调整 young 大小可以减缓一大部分。
    63 回复  |  直到 2019-10-17 16:52:14 +08:00
        1
    xuanbg   32 天前
    拆分服务。。。。。。
        2
    IamNotShady   32 天前
    一个请求 900M ? 换成 G1 试试
        3
    chendy   32 天前   ♥ 1
    一个请求 900M…要么极端需求要么设计不合理…
        4
    jimrok   32 天前
    把数据拆出来,放在 redis 或者 memcached 里面。否则堆会太大,一旦需要扫描堆就会比较耗时。
        5
    reus   32 天前
    换最新版 jvm 和最先进的 GC 算法

    不换就不要奢求太多。
        6
    coolcfan   32 天前
    也许可以从生命周期的角度检查下请求处理过程中消耗的内存。
        7
    lihongjie0209   32 天前
    那也就是说你的这个服务的并发数量 = 10 ??
        8
    summer7   32 天前 via Android
    @lihongjie0209 是的,这个项目基本等于没有并发。 10 并发也到不了。
        9
    summer7   32 天前 via Android
    @chendy 需求大概是这样的:从数据库查出 30000 条数据,数据部分字段会包含长文本,单条数据约 200+字段,查出后需要对数据解析,据观察内存涨幅约等于 900m
        10
    summer7   32 天前 via Android
    @jimrok 数据流程大概是这样,第一步 jdbc 查库,第二步:对每条数据做解析,第三步:入 redis。 目前问题主要集中在解析这个过程 32 核机器跑满也需要 2-4s,稍微来个并发(不到 10 个),10g 的堆因为 gc 导致解析时间会上升到 20s+
        11
    yidinghe   32 天前
    1、确认有没有内存泄漏,也就是多次 GC 后程序还是能正常运行并且长期运行;
    2、能够对老年代进行非 STW 回收的垃圾收集器只有 G1。建议换 G1 ;
    3、进一步对内存使用进行剖析,减少不必要的对象持有;
    4、如果缓存数据占大头,那么换用 Redis/memcached 等独立进程的缓存方案。
        12
    yidinghe   32 天前
    “从数据库查出 30000 条数据,数据部分字段会包含长文本” 这时候应该改为读取一条处理一条的方式。
        13
    summer7   32 天前
    @jimrok 补充:是单次请求解析需 2-4s
        14
    misaka19000   32 天前
    用 C 语言重写
        15
    learnshare   32 天前
    这需求或实现逻辑并不合理吧
        16
    Immortal   32 天前
    单条数据约 200+字段= = 猛老哥
        17
    u823tg   32 天前
    换语言? 我胡说的
        18
    memedahui   32 天前
    @IamNotShady 我记得 java8 默认就是 G1 吧...还是我记错了
        19
    lihongjie0209   32 天前
    @misaka19000 #14 你用 c 语言管理 10G 内存试试, 没啥区别的。
        20
    lihongjie0209   32 天前
    @summer7 #9 一次查 3000 条试试?
        21
    hikikomorimori   32 天前
    考虑 Nosql?
        22
    lihongjie0209   32 天前
    @hikikomorimori #21 不管底层存储怎么样, 加载 30000 条数据就需要这么多的内存
        23
    babyvox5th   32 天前
    补充一下技术优化之外的,SSD 上 3000MB.
        24
    ipwx   32 天前 via Android
    不该想办法维护中间结果的表,降低每次请求计算量么
        25
    bk201   32 天前
    取少点不行吗?
        26
    bobuick   32 天前
    假设你其他措施都做了, 然后单次要是 900m 是必要的数据. 然后也无法重新设计, 然后请求完数据后内存里这些数据就不用保存了的话, 不是应该在 young 区被回收么. 把 young 设的足够大一些. 老年代应该保持一天都不到一次的水平
        27
    summer7   32 天前 via Android
    @ipwx 嗯,以前 hbase 做存储时已经是结构化的数据了,但是后来换了数据库必须要调用方去解析这些数据。其实整体数据流程很简单,jdbc 查询,查完解析入库。
        28
    bookit   32 天前
    用 C 写一遍,最简单的那种,看需要多少内存
        29
    sadfQED2   32 天前 via Android
    考虑 nosql?另外必须实时吗,不需要实时的话定时脚本计算,然后存缓存呢。最后,如果都不行就升级机器? 80 核以上,500+G 内存那种?
        30
    sadfQED2   32 天前 via Android
    @sadfQED2 楼上的换语言没什么意义啊,数据已经那么大了,你用什么语言加载到内存都那么大,除非改算法
        31
    BBCCBB   32 天前
    先用 G1 试试,然后增大堆内存再试试
        32
    l8g   32 天前
    1. 一次查询 3W 条记录,可以拆分一下
    2. 调整一下 Young 和 Old
        33
    summer7   32 天前
    @bobuick 感谢回复。young 设置大一点我会验证试一下的。这是第一次遇到这种 GC 问题,不知像互联网那些高并发项目,Full GC 频率的合理范围是多少呢?还请指教
        34
    summer7   32 天前
    @babyvox5th 数据 JDBC 查询完,就是内存操作了目前。
        35
    summer7   32 天前
    @l8g 感谢回复。楼上也有大佬提到修改 young 和 old,我会试一下的。 说起来,拆分查询也是一个头疼的事情,目前底层存储库不支持这种比如 limit 这种拆分查询
        36
    summer7   32 天前
    @BBCCBB 感谢回复。G1 会尝试一下的。
        37
    summer7   32 天前
    @bobuick 目前我单纯的本地写个 for 循环去触发查询接口,基本上每调用 9 次就会触发一次 Full GC,之后内存迅速下降,基本等于刚启动时候的内存。 改改参数看吧,也是第一次遇到这种问题
        38
    summer7   32 天前
    @sadfQED2 感谢回复。数据要求实时查询。加机器配置这个事情,难呀。
        39
    summer7   32 天前
    @yidinghe 感谢回复。
    1、确认后没有内存泄漏,一次 FullGC 之后内存基本和刚启动时一样.但是一次 10 并发,基本就要 Full GC 一次,楼下也有大佬提出修改 young 和 old,我试一下看看,将 young 设置大一点。
    2、对于取一条数据解析一条,嗯。。其实一直有个疑惑,while(resultSet.next ())的时候,这个 resultSet.next 是不是就相当于一个游标,其实数据还是存在于数据库端的?还是说不同数据库有不同的实现方式?
        40
    shakoon   32 天前
    如果能在数据库上实现,用视图把表拆小、用存储过程进行你所说的解析,就试一下看看
        41
    semut   32 天前
    一次请求 30000 条数据,设计不太合理,可以简单说说这个任务的目的,看下有没有其他方案实现
        42
    phantomzz   32 天前
    长文本存 elasticsearch,其他 200 字段该拆表拆表,将 30000 条数据水平分割到不同 机器 /进程 流式处理,全部处理完毕再汇聚。
        43
    Jonz   32 天前
    关注下进展
        44
    uyhyygyug1234   32 天前
    @phantomzz 我感觉也是按照老哥的说法。

    900MB 3w 条数据 每条数据 30k

    这边 magic number 为啥要取 3w
    3w 条之间本身是否有关系,可否分割,分布式处理
        45
    af463419014   32 天前
    用堆外内存

    处理的数据单独放在堆外内存,用完手动释放,可以跟 JVM 的 GC 分开
    JVM 堆内存就不需要 10G 这么大了
        46
    pangliang   32 天前
    3 万条数据为啥要一次性读入内存? 用了 orm 吧? 查完库只能返回一个 3 万的 list, 然后遍历 list?
    直接 jdbc 里查, 完了用 jdbc 的 result.next 去循环直接处理, 不要拼到 list 再遍历;
    这样就不会有 3 万行在内存了
    如果还是不行, 检查下 jdbc 返回数据的方式; 可以设置, next 一次返回一行
    php 都有, java 不可能没有
        47
    SoloCompany   32 天前 via iPad
    加并发控制,限制 slow operation 的并发数,预留 30%以上的空闲 heap
        48
    mxalbert1996   32 天前 via Android
    @sadfQED2 换语言的意义在于减少 GC 的时间啊
        49
    summer7   32 天前 via Android
    @phantomzz 其实在之前用 hbase 存储是分表的,但是每个表也是 200+字段的,之后底层数据库弃用了 hbase,把所有表数据汇聚在一张超大表中,数据量相当的大。
    感觉“合久必分”这句话太适合描述这种存储方式变化了
        50
    summer7   32 天前
    @summer7 合久必分,分久必合
        51
    summer7   32 天前
    @pangliang 感谢老哥的回复。源于对 JDBC 理解的不是很深,目前我的做法是 next 遍历完,30000 条数据存入 list 再交给具体的解析方法解析。 也有其他大佬和老哥你一样说,可以 next 遍历时一条一条解析,或许这个就是解决问题的办法吧,待我一试。
        52
    neoblackcap   32 天前
    为什么需要一次读 3W 条数据?流式处理嘛,你业务逻辑不改,换 JVM 也未必有成效。
    毕竟假如全部都是新生代对象,但是在 GC 被触发的时候,这些对象很有可能还是活的,有其他业务代码在使用这它们。你这个整体并发一样上不去。
    这个东西还是得结合你的业务逻辑进行分析才行
        53
    Buffer2Disk   32 天前
    改架构吧,这业务设计就不合理。。。
        54
    swulling   32 天前 via iPhone
    每次请求结束后都主动触发一次 GC 吧😄
        55
    v2orz   31 天前
    换语言和堆外内存方案建议别搞
    换语言作用不大
    堆外内存,既然你们都设计成这样了,想想堆外内存管理估计也很难做好,不建议去踩
    一次少查点数据或者流式处理就差不多了,GC 换 G1
        56
    softtwilight   31 天前
    把 resultSet 包装为流,一条一条解析,十分省内存;
    return StreamSupport.stream(new Spliterators.AbstractSpliterator<Record>(
    Long.MAX_VALUE,Spliterator.ORDERED) {
    @Override
    public boolean tryAdvance(Consumer<? super Record> action) {
    try {
    if(!resultSet.next()) return false;
    action.accept(createRecord(resultSet));
    return true;
    } catch(SQLException ex) {
    throw new RuntimeException(ex);
    }
    }
    }, false).onClose(() -> closeConnectionAnd...())
        57
    haochih   31 天前
    jdbc 默认情况下的读会把 sql 语句的结果全部加载到内存中,这种大数据量的读可以考虑采用 stream read。详情参见 https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-implementation-notes.html 中 ResultSet 一节。
        58
    tairan2006   31 天前
    这架构有问题,数据拆开,或者改成流处理
        59
    sorra   31 天前
    JDBC ResultSet 有 vendor 差异性,默认情况下:
    - MySQL 和 PostgreSQL 会一次加载所有行到 JVM
    - Oracle 每次只加载 10 行到 JVM,DB 这边维持一个会话和偏移量,JVM 可以不停地推进偏移量,直到读完所有行
    这个行为可配置(ResultSet 可以 setFetchSize),详情见 vendor 的文档

    如果 vendor 支持 setFetchSize,你可以流式处理数据,不要都堆到内存里才全部处理

    如果 vendor 不支持 setFetchSize 和 LIMIT,也可以想想能不能在 SQL 语句上想想办法
        60
    sorra   31 天前
    Statement 也可以 setFetchSize,为了防止来不及,可以在 Statement 就设上
        61
    jimrok   31 天前
    你的数据里有大文本是不太能降低内存的,大文本被装在进 java 要转换成 String 的对象,这些对象再进行解析,肯定要再产生若干小对象,gc 的负担肯定重。你这种还是提前做好,例如你要从小说里面找某个情节,那你把整篇小说读进来再查找肯定耗费内存。最后提前做好索引,这样根据索引,直接找到段落,返回段落信息就不需要消耗很多内存了。
        62
    lazyfighter   31 天前
    可以写个定时任务建立中间表,甚至最终数据表,这样你的并发也能上来,把数据处理拆出来
        63
    TJT   31 天前
    流处理。
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1188 人在线   最高记录 5043   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.3 · 29ms · UTC 17:30 · PVG 01:30 · LAX 09:30 · JFK 12:30
    ♥ Do have faith in what you're doing.