美文网首页
垃圾回收

垃圾回收

作者: 每皮1024 | 来源:发表于2021-01-12 09:43 被阅读0次

    一、垃圾的判断?

    1. 引用计数法(RC)
      缺点:不能解决循环引用问题
    2. 可达性(Root Searching)
      Root:线程栈变量、静态变量、常量池、JNI指针(java native interface)

    二、垃圾回收算法

    1. 标记清除(Mark Sweep)
      方法:找到垃圾后直接清除,表示可以用了
      缺点:碎片
    2. 拷贝(Copy)
      方法:将内存一分为二,GC时将有用对象复制到另一边
      缺点:空间浪费
    3. 标记压缩(Mark Compact)
      方法:将有用对象转移,整理到一起
      缺点:需要挪对象,效率偏低

    三、垃圾回收器

    1. 垃圾回收器图示

    image.png
    1. 前面6种是分代模型,G1逻辑分代物理不分代,ZGC逻辑物理均分代
    2. jdk1.8默认Parrallel Scanvenge+Parrallel Old组合,jdk1.9默认G1
      2.1. 可使用命令java -XX:+PrintCommandLineFlags -version来判断当前使用的
      回收器
    3. 垃圾回收器一般分为Serial,Parrallel,CMS(上面三种竖向组合)。上述有这么多的垃圾回收器,是因为随着内存变大,不断地需要满足垃圾回收的要求。
      3.1 Serial:a stop-the-world进入safe point(STW:此时工作线程不能干活),copying collector which uses a single GC Thread
      3.2 Serial Old:a stop-the-world, mark-sweep-compact collector that uses a single GC thread
      ** 上面这两种Serial垃圾回收器组合,在内存变大时,清理时间会变长,STW时间太长会导致工作线程长期不可工作,也就是卡顿
      3.3 Parallel Scaveng:a stop-the-world, copying collector which uses multiple GC threads
      3.4 Parallel Old: a compacting collector that uses multiple GC threads
      ** 与Serial的区别主要在于,当STW时,有多个线程来帮助进行清理,提高了效率
      ** 但是,GC线程数量不可无限增加,当GC线程数量太多时,CPU会耗费精力在线程切换(context switch),所以当内存越来越大时依然会出现卡顿现象
      3.5 ParNew:描述同Parallel Scavenge,区别在于it has enhancements that make it usable with CMS。For example, "ParNew" does the synchronization needed so that it can run during concurrent phases of CMS

      3.6 CMS image.png
      • concurrent mark sweep,并发,做到了垃圾回收线程和工作线程同时进行
      • a mostly concurrent, low-pause collector
      • 4 phases:
        (1)initial mark:stw,然后标记根对象(所以耗时很短)
        (2)concurrent mark:并发标记根对象树(此时GC线程过滤对象树,但工作线程同时又在改变对象树,所以可能会出现错标现象)
        (3)remark:再次stw,修正错标问题(注意:CMS/g1/ZGC的区别主要在这一步:CMS和g1使用的是三色标记,而ZGC使用的是颜色指针color pointer)TODO:学习算法
        (4)concurrent sweep:经过第三次的修正错标后,并发进行清理

    1.1 错标问题的解决(CMS/g1/ZGC)

    三色标记法:黑色表示对象和对象的所有成员变量都已经遍历过了,灰色表示仅遍历了对象但其成员变量还没有遍历完,白色表示还没有遍历到

    1. 情况一:如下图,当标记完A为黑后,B->D的引用消失,而A->D的引用增加,此时GC标记线程不会再去遍历A,则D会被认为是没有引用而将被回收
      • image.png
      • CMS解决方案:Incremental Update,通过将A修改为灰色,下次标记时则会再次对A进行遍历。将A变灰条件:跟踪引用的变化,并对A的颜色进行修改(写屏障) image.png

        ** 此方法存在缺陷1:GC标记可能是多线程同时标记一个对象,假设A有1和2两个成员变量,m1线程刚标记完1正要标记2(m1认为A是灰的),此时1指向了D对象触发了m2线程将A设置为灰(写屏障),然后m1继续标记完2后将A设置为黑色,这种情况下就会出现漏标。所以CMS remark阶段,必须从头到尾扫一遍。
        ** 此方法存在缺陷2:当CMS(mark sweep)由于碎片导致放不下,会变成Serial Old

    • g1解决方案:SATB snapshot at the beginning。就是在一个栈(RSet remember set表示的是谁指向了我)里保存消失的引用,最终扫描时会将这些消失引用指向的对象再次扫描如果真的是垃圾才回收。 image.png

    2. 分代模型

    也即堆内存逻辑分区(不适用不分代垃圾收集器),新生代:老年代默认内存比例1:2

    • 新生代:大量死去、少量存活,采用复制算法
      • 标记清除和标记压缩都会要从头扫,会扫到大部分无用的对象
    • 老年代:存活率高,采用MS或者MC
      • 如果使用拷贝算法,大部分对象都要被拷贝

    2.1 从新生代到老年代

    image.png
    1. 对象如果能在栈中放得下,则放入栈中,在栈退出pop时会直接释放,不需要垃圾回收的介入,此种方式最为高效
    2. 如果该对象栈内放不下,则根据该对象的大小(可用-XX:PretenureSizeThreshold)判断,大对象直接进入老年代,等待FullGC时回收
    3. 如果该对象大小介于上述两者中间,则进入TLAB
      • Thread Local Allocation Buffer:(可取消)代表的是线程在Eden区单独的空间,当该线程需要分配对象空间时,优先在此区域分配,避免多线程竞争问题
    4. 新生代又分为eden、s1、s2区,一个对象首先进入eden区,回收一次会被拷贝到s1或者s2区(并且年龄+1),当年龄超过特定值时进入老年代
      • 年龄值设定
        • 可通过-XX:MaxTenuringThreshold配置,表示超过多少年龄就进入老年代
        • 各个垃圾回收器有默认值:一般垃圾回收器是15,CMS的默认值是6
    • GC概念及发生的地方
      • MinorGC/YGC:年轻代空间“耗尽”时时触发
      • MajorGC/FullGC:老年代“无法继续分配空间”时触发,新生代老年代同时进行回收
      • 注意:以上的耗尽是概念,会根据垃圾回收器的不同而不同

    四、GC Tuning

    1. 根据需求进行JVM规划和预调优
    2. 优化运行JVM环境(慢、卡顿)
    3. 解决JVM运行过程中出现各种问题(OOM)

    1. 常见

    1. 命令
    top
    jps # java process, 当前系统内跑着的java的进程号
    jinfo <process id> # 打印该进程关于java的信息(java版本等)
    jstat -gc <process id> # 打印gc的所有内容空间
    jstack <process id> # 打印线程及状态
    jmap -histo <process id> | head -20 # 列举出该进程下,类对应了多少的对象;后面的可选,列出前20行
    arthas # 阿里开源神器
    

    arthas

    jad # 反编译
    redefine
    
    1. 图形化界面(但上线后一般不允许远程连接)
    • bin文件夹下有jvisualVM(自带)可以观察本地的进程情况,还有jconsole
    • jprofiler(收费)

    2. 实战

    • 内存溢出:表示的是不断地建立对象,然后达到了JVM内存的上限Xmx
    • 内存泄漏:表示的是有内存不再使用了,但由于仍然有引用,导致一直释放不掉。此行为最终会导致内存溢出
    1. 如何定位频繁的FGC
      可能是有对象不断占用内存
      1.1 首先可以通过jmap -histo <process id>查看类对应的对象数量
      1.2 但是生产环境如果直接使用会有性能影响(会产生STW):(1)测试环境压测(2)如有高可用,做好隔离,在另一台机器测(3)tcp copy流量到测试机器(4)-XX:+HeapDumpOnOutOfMemoryError,然后使用MAT/jhat/jvisualvm进行dump文件分析 (5)jmap -dump:format=b,file=xxx pid: (6)arthas - heapdump --live /path/to/file
    2. 如何定位OOM
    3. 如何定位直接内存
      3.1 NMT打开 --XX:NativeMemoryTracking=detail
      3.2 perf工具
      3.3 gperftools
    4. 如何定位JVM进程静悄悄挂掉,即没有dump文件
      4.1 JVM 自身oom导致:heap dump on oom,这种最容易解决
      4.2 JVM自身故障:-XX:ErrorFile=/var/log/hs_err_pid<pid>.log,超级复杂文件,包括crash线程信息、safepoint信息、锁信息、native code、cache、编译时间、gc相关记录、jvm内存映射等等
      4.3 被Linux OOM killer杀死:(1)日志位于/var/log/messages (2)egrep -i 'killed process' /var/log/messages
      4.4 硬件或内核问题:dmesg | grep java
    5. 如果一个Java进程CPU突然暴增(50%~90%)
      首先使用arthas-dashboard找到哪个线程占用CPU很高;如果是业务线程,则看业务逻辑;如果是JVM线程,则可能是GC问题:例如OOM造成的不断地FGC,消耗CPU资源导致暴增,此时看GC日志。
      5.1 普通linux命令:top -Hp找出进程线程占比CPU最高,jstack
      5.2 arthas - dashboard thread/thread <thread id> 可以看到线程堆栈
    6. 死锁如何排查?
      6.1 jstack观察线程情况
      6.2 或者用arthas - thread -b(找到那个因为拿着锁block最多线程的线程)

    [书签: p6 10:31]

    参考

    相关文章

      网友评论

          本文标题:垃圾回收

          本文链接:https://www.haomeiwen.com/subject/nalsaktx.html