JVM相关学习记录与总结(内存&GC&类加载&工具)

来源:互联网 发布:遗传算法解决线路优化 编辑:程序博客网 时间:2024/05/01 07:40

一、内存

分布

1 总分布图

java_mem

2 堆内存分布

java

OOM

  • 类别
    • 堆内存不足
    • 本地内存不足
    • 加载类的Perm区内存不足
  • 原因
    • 内存泄漏
    • 空间设置太小
    • 大量长期的大对象

内存泄漏

  • 症状
    • 系统越来越慢,并伴随着CPU使用率过高。这主要是因为随着内存的泄漏,可用的内存越来越小,垃圾回收器频繁进行垃圾回收(FullGC一次接一次,每次耗时几秒甚至几十秒)。而垃圾回收是一个CPU密集型操作,频繁的GC会导致CPU持续居高不下,在有内存泄漏的场合,到了最后必然是伴随着CPU使用率几乎为100
    • 系统运行一段时间,系统抛出OOM异常
    • 虚拟机core dump
  • 常见的原因
    • 自定义类加载器。每个对象有一个它class的引用,相应也有它classloader的引用。反过来,类加载器也拥有它加载的所有class的引用,一旦类加载器内存泄漏,意味着一堆对象泄漏。
    • 数组减长度,但内容不减。
    • Threadlocal变量不释放。
    • 静态hash做缓存只增不减。
    • 资源(连接池)不关闭。

大小的经验推荐

空间java参数推荐设值堆内存-Xms & -Xmx稳定阶段,FullGC发生过后老年代空间占用大小的3-4倍永久代-XX:PermSize & -XX:MaxPermSize稳定阶段,FullGC发生过后永久代空间占用大小的1.2-1.5倍年轻代-Xmn稳定阶段,FullGC发生过后老年代空间占用大小的1-1.5倍老年代堆总内存-年轻代-永久代稳定阶段,FullGC发生过后老年代空间占用大小的2-3倍

二、GC

CMS

  • 标记方式:通过bitmap
  • 触发原因:
    • 正常触发
    • 设置UseCMSInitiatingOccupancyOnly+CMSInitiatingOccupancyFraction=80后只有当老年代空间占用达到80%时才触发CMS
    • 设置CMSClassUnloadingEnabled后同时会清理perm区域
  • 过程
    1. 初始标记:为了收集应用程序的对象引用需要暂停应用程序线程,只标记root对象(bitmap里面)【stop the world】。
    2. 并发标记:从第一阶段收集到的对象引用开始,遍历所有其他的活对象引用。
    3. 并发预清理:改变当运行第二阶段时,由应用程序线程产生的对象引用,以更新第二阶段的结果。
    4. 重标记:由于第三阶段是并发的,对象引用可能会发生进一步改变。因此,应用程序线程会再一次被暂停以更新这些变化,并且在进行实际的清理之前确保一个正确的对象引用视图。主要遍历young区和old/perm区的卡表(card table)【stop the world】。
    5. 并发清理:单线程处理,所有不再被应用的对象将从堆里清除掉,不压缩,所以会有碎片。
    6. 并发重置:收集器做一些收尾的工作,以便下一次GC周期能有一个干净的状态。
  • 与fullGC的关系
    1. FullGC指对老年代/永久代的stop the world的GC
    2. FullGC的时间和次数分别指老年代GC时stop the world的时间和次数
    3. CMS不等于FullGC,CMS分为多个阶段,只有stop the world的阶段被计算到了FullGC的次数和时间,而和业务线程并发的GC的次数和时间是不被认为为Full GC

ParNew(CMS处理年轻代的算法)

  1. 根据对象的引用关系,从root对象开始,只处理活的对象,采用move&copy的策略将活的对象都拷贝到to区域;old区对young区对象的引用是通过card table,遍历card table将可以引用到的活的对象也复制到to区域。
  2. Eden满时触发,多线程处理。

Mark-Sweep-Compact(CMS处理老年代的算法,进行碎片压缩)

过程

  1. 标记所有活的对象阶段。这个阶段根据root来地柜地标记所有活的对象,基本上等级于CMS的初始和并发标记的,但是是单线程的。
  2. 计算活着的对象新的内存地址阶段。根据阶段1的结果,单线程遍历每个generation的每个内存区域的每个对象,计算活着的对象应该被compact到哪里,并把计算结果保存到header的mark上。
  3. 修改对象内部引用。顺序遍历每个generation每个内存区域每个对象,对于每个活着的对象,修改内部引用指向正确的内存地址。
  4. 移动对象。根据2把活着的对象copy到正确的内存位置。

触发

  • promotion failed
  • concurrent mode failure
  • System.gc()

GC的参数配置

1 堆配置相关

  • -Xms4g //最小堆
  • -Xmx4g //最大堆
  • -Xmn2g //young区大小
  • -XX:SurvivorRatio=10 //eden与survivor区大小比例

2 perm配置相关

  • -XX:PermSize=256m
  • -XX:MaxPermSize=256m

3 压缩指针

  • -XX:UseCompressedOops //64位下的内存设置,如果放到32位上(一般就4G),有时空间就超了,压缩指针可以一定程度帮助压缩来适应32位的大小

4 CMS配置相关

  • -XX:+UseConcMarkSweepGC
  • -XX:+UseCMSCompactAtFullCollection //cms时进行碎片压缩
  • -XX:CMSMaxAbortablePrecleanTime=5000
  • -XX:+CMSClassUnloadingEnabled //CMS时也会清理perm区
  • -XX:CMSInitiatingOccupancyFraction=80 //指定老年代占比多少(这里是80%)时触发CMS
  • -XX:+UseCMSInitiatingOccupancyOnly //结合XX:CMSInitiatingOccupancyFraction一起使用

5 GC线程配置相关

  • -XX:ParallelGCThreads//设置并发阶段GC的线程的数量

6 GC LOG相关

  • -Xloggc:${LOGS}/gc.log
  • -XX:+PrintGCDetails
  • -XX:+PrintGCDateStamps

7 其他

  • -XX:+HeapDumpOnOutOfMemoryError
  • -XX:HeapDumpPath=${LOGS}/java.hprof

三、类加载

过程

classloader_process

其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段),而解析阶段则不一定(它在某些情况下可以在初始化阶段之后再开始),这是为了支持Java语言的运行时绑定。

零碎知识点

  • 显示与隐式:new是隐式加载,触发<init><clinit>;Class.forName是显式,触发<clinit>
  • 双亲委派:先交给父加载器加载,如果加载不了,就由自己来加载,如果所有都不能加载就抛出ClassNotFoundException异常。

类初始化

  • 触发条件
    • 使用new关键字实例化对象的时候。读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
    • 对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
    • 当初始化一个类时,发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()的方法的那个类),虚拟机会先初始化这个主类。
  • 内部过程
    • 就是执行,编译器将所有类变量初始化语句和静态代码块都收集到中

常见错误

  • ClassNotFoundException
    • 从自定义类加载器到系统类加载器,到扩展类加载器,到引导类加载器,在不同路径下都找不到那个class
    • 如果那个class的确存在,也有两个可能导致这个异常,一是jar冲突了,实际加载的那个class不是你想要的;二是不同的类加载器隔离了,当前类A引用了类B,类A的加载器加载了类A后,因为调用也去加载类B,但是类B指定只能是被ClassLoaderB来加载的,这是加载不到类B也会报这个异常。
  • NoClassDefFoundError
    • 这个出现的场景一般是类A引用了类B,类A加载器加载类B时加载失败(不存在或者加载器隔离),这时会报ClassNotFoundException异常,并连带引发NoClassDefFoundError这个错误。也就是说在编译时这个类是能够被找到的,但是在执行时却没有找到。
    • 【摘自java虚拟机规范】如果 Java 虚拟机曾经试图在 D 的验证或解析阶段、但又还没有进 行初始化时加载 C 类,当用于加载 C 的初始类加载器抛出 ClassNotFoundException 实例时,Java 虚拟机在D中必须抛出NoClassDefFoundError异常,它的 cause 字段中就保存了那个ClassNotFoundException异常实例。
  • LinkageError
    • 此类已经在ClassLoader加载过了,重复的加载会造成这个异常
    • 由于JVM的这个保护机制,使得在JVM中是没办法直接更新一个已经load了的Class的,只能是创建一个新的ClassLoader来加载更新了的Class,然后将新的请求转入到这个ClassLoader中来获取类,这也是JVM中不好实现动态更新的原因之一,而其他更多的原因是对象状态的复制、依赖的设置等等

工具

  • jstack
    • jstack java_pid > jstack.out //输出堆栈信息
  • jmap
    • jmap -histo java_pid > mem.out//查看内存使用情况,某个类有多少实例,占用多少内存等
  • top
    • top -H -p java_pid //可以查看本地线程资源占用情况
    • printf("0x%x",nid)//将本地线程id转成16进制,方便在线程堆栈中搜索出对应的java线程
  • jps
    • jps -v //输出java_pid
  • jstat
    • jstat -gc/gcutil/gccapacity/gccause java_pid time_interval times//输出gc状态,可以指定输出次数和输出的时间间隔
  • jinfo
    • 获取java配置信息

参考

追风堂公共课:《初探CMS GC算法原理和实例分析》 JVM交流答疑圈-问答归档(20140228)
线上jvm参数分析
理解Hotspot JVM CMS垃圾回收器

0 0
原创粉丝点击