🚩JVM与GC

JVM

🌟JVM 调优工具

  • jmap:生成堆转存储快照,dump 文件(保存线程、堆栈调用、异常信息),用 VisualVM 去加载 xxx.hprof 文件
    • 看堆信息:jmap -heap <pid>
    • 生成快照 :jmap -dump:format=b,file=heap.hprof <pid>
  • jconsole:什么都有
  • jps:JVM运行期间的进程状态信息
  • jstack:用于生成当前时刻线程快照,线程快照是当前虚拟机内每一条线程正在执行的方法堆栈的集合。主要目的是为了定位线程阻塞。
  • jstat:进程内的堆栈信息
    • 垃圾回收统计jstat -gc <pid> 使用量(-gcutil 关注使用率)
  • jmx:cpu情况、线程情况、手动gc
  • 🌟Arthas 燃尽图/火焰图:横条越长,代表使用的越多,从下到上是调用堆栈信息。看顶层的哪个函数占据的宽度最大。只要有"平顶"(plateaus),就表示该函数可能存在性能问题。
    • 采样方法:profiler start --duration 30 --file profile.svg --event alloc --d 306954
      • duration 采样时长
      • file 生成文件名
      • event 观测内容:内存分配 alloc,cpu

什么是内存溢出和内存泄漏?/ OutOfMemoryError

  • 内存溢出 Out Of Memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。可能原因:
    • 虚拟机栈:StackOverFlowError,递归方法死循
    • 元空间方法区:OutOfMemoryError:Metaspace
      • 反射类加载,动态代理生成的类加载
      • 启动参数内存值设定得过小
    • :OutOfMemoryError,用 jmap + VisualVM 排查,或设置启动参数输出日志。
      • 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
      • 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
  • 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,当一个对象不再被使用时,如果没有及时将其引用置为 null 或者手动释放,会导致应用程序崩溃、性能下降、数据丢失等严重后果
    • e.g. ThreadLocal 没有及时调用 remove()

JVM 内存模型

  • 堆(Heap):线程共享。所有的对象实例以及数组都要在堆上分配。 GC 主要管理的对象。
    • 新生代(1/3)
      • eden(8/10),可以通过 -XXSurvivorRatio 调整,默认 8
      • from、to 区域(各 1/10)
      • TLAB:Thread Local Allocation Buffer,为每个线程分配的一个私有缓存区域,否则,多线程同时分配内存时,为避免操作同一地址,可能需要使用加锁等机制,进而影响分配速度(start、top、end)
    • 老年代 (2/3)
      • 大对象
      • 长生命周期对象,-XX:MaxTenuringThreshold调整,默认 15,取值 0~15
      • from+to 空间不足
    • OutOfMemoryError:堆空间不足,调整 Xmx
    • -Xms 初始堆空间,-Xmx 堆空间上限,建议设置相等(物理内存的 1/4),在考虑其他项目的内存使用情况时,尽量大。
      • 4核8G 实践:一般配置 -Xms2g -Xmx2g -XX:ParallelGCThreads=4
  • 🌟方法区(Method Area):线程共享。存储类信息、常量、静态变量、JIT编译器编译后的代码,JVM 启动时创建,关闭时释放
    • jdk 7 位于永久代,jdk8 移动到了 metaspace 元空间,防止内存溢出。
    • OutOfMemoryError:Metaspace
    • 运行时常量池:常量池中的符号地址(#1、#2..)变为真实地址
  • 虚拟机栈(JVM Stack):线程私有,由多个栈帧组成。存储局部变量表、方法对象指针
    • 如果局部变量引用的对象,并逃离方法的作用范围,需要考虑线程安全。
    • StackOverFlowError:递归调用导致栈帧过多 or 栈帧过大。
    • -Xss 128k(默认 1M,太大了)
  • 本地方法栈(Native Method Stack):线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。
  • 程序计数器(Program Counter Register):线程私有,指向下一条要执行的(字节码)指令。
  • 【不由 JVM 管理】直接内存:是 JVM 的系统内存,在 NIO 操作时,用于数据缓冲区,它分配回收成本高,读写性能高(少一次缓冲区复制)。

GC

🌟Java常见的垃圾收集器和对应的GC 算法?

可达性分析:以 GC Root 为起点,标记出所有要回收的对象,然后进行清除。

GC新生代老年代特点
SerialGC(JDK1.2 默认)复制算法标记-整理适用于单核 CPU
ParallelGC(JDK1.3默认)复制算法标记-整理高吞吐量,新老年代GC 并行执行
CMS GC(JDK5 引入)复制算法标记-清除低延迟,因为使用了增量标记和并发标记来减少 STW 时间
G1(Garbage 1st,JDK9 默认)region 之间复制,不要求年轻代、老年代是连续的-兼顾吞吐量和延迟
ZGC(JDK11 引入)复制标记-整理,着色指针+读屏障技术通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,使得 GC 最大停顿时间不超过 10ms,但仅支持 Linux 64 位平台
  • 复制算法
    • 优点:高效、无碎片
    • 缺点:内存利用率低
  • 标记-整理算法:无内存碎片
  • 标记-清除算法:高效,但出现内存碎片化问题

ZGC 的着色指针使用一个额外的标记位来标记对象是否存活,将其存储在对象头中的 unused_bits 字段中,可以快速标记对象存活信息,实现内存压缩和快速的垃圾回收;读屏障技术在对象引用读取时进行内存屏障,可以减少STW 时间,提高应用程序的性能和可靠性。

Minor/Young + Major = Full GC

GC过程

  1. Java 应用不断创建对象,通常都是分配在 Eden 区域,当其空间占用达到一定阈值时,触发 minor GC。仍然被引用的对象存活下来,被复制到 JVM 选择的 Survivor 区域,而没有被引用的对象则被回收。
  2. 经过一次 Minor GC,Eden 就会空闲下来,直到再次达到 Minor GC 触发条件,这时候,另外一个 Survivor 区域则会成为 to 区域,Eden 区域的存活对象和 From 区域对象,都会被复制到 to 区域,并且存活的年龄计数会被加 1。
  3. 类似第二步的过程会发生很多次,直到有对象年龄计数达到阈值,这时候就会发生所谓的晋升(Promotion)过程,超过阈值的对象会被晋升到老年代。

STW为什么需要停顿所有的Java执行线程呢?

  1. 确保一致性快照,让整个执行系统看起来像冻结在某个时间点上了,如果出现分析过程中对象引用关系还在不断变化,则分享结果的准确性是无法保证的。
  2. STW事件和采用哪款GC无关,所有的GC都是有这个事件
  3. 越优秀,回收效率越高的垃圾回收器,尽可能地缩短暂停时间
  4. STW是JVM在后台自动发起和自动完成的。是在用户不可见的情况下,把用户正常的线程全部停掉,再去把不用的对象都干掉。

G1 GC的好处?

-xx:+UseG1GC 优势:

  1. 并行与并发 - 多个垃圾收集线程同时工作(并行),垃圾收集线程与用户线程交替执行(并发),分成三个阶段:
    1. 年轻代垃圾回收
    2. 并发标记
    3. 混合收集(Eden + from -> to,old -> old’)
  2. 分代收集 - G1将堆空间划分为若干个区域(Region),每个区域都可以充当 eden、s0、s1 和 humongous(大对象),它不要求年轻代,老年代是连续的
    1. mixed gc:部分区域,G1 特有
    2. minor gc:新生代,时间短(STW)
    3. full gc:新生代+老年代,时间长(STW)
  3. 复制算法G1内存回收使用Region作为基本单位,对内存空间进行整理。
  4. 可预测的停顿时间模型 - 可以让用户明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

缺点:相比于CMS,G1还不具备全方位、压倒性优势。比如,G1为了垃圾收集产生的内存占用以及程序运行时的额外执行负载都比CMS要高。

0%