深入理解 JVM 和 GC -- 内存调优

运行时数据区

  • 首先弄清楚一点:一个java进程开启一个jvm,进程又可分成多线程运行
  • 进程在 jvm 上运行时,数据保存在运行时数据区,运行时数据区包括堆、方法栈、虚拟机栈、本地方法区和程序计数器,其中堆和方法栈是线程共享的,虚拟机栈、本地方法区和程序计数器是线程私有的
    运行时数据区

堆(线程共享)

  • 存储程序运行时用到的所有对象,而线程虚拟机栈的局部变量表保存的是对象的引用指针;堆空间分为新生代、老年代、元数据(jdk1.8之后,1.7之前称为永久代),元数据实际上不属于堆空间
    JMM
  1. 为什么分代? 对象的生命周期不一样,GC过后有些被回收有些存活下来,存活下来的对象也不必与新来的对象一起GC,需要另外空间来存放存活下来的对象

  2. 为什么Eden:servivor是8:1:1而不是9:1? Minor GC => 新生代 Major GC => 老年代 一次 Major GC 所花时间 > 一次 Minor GC 所花时间 所以我们希望新生代的对象尽量不要被放到老年代,即 GC
    之后应有98%的对象被回收,如果是9:1的话,对象很容易存活到老年代,所以希望对象在新生代进行更多次的GC以达到目标

  3. 对象分配比例配置:-XX:SurvivorRatio=8

新生代

  • 新生代占堆空间的1/3,分为Eden区(8/10)、From区(1/10)、To区(1/10)
  • 新创建的对象会被放入Eden区,当Eden区空间不足时,虚拟机会做一次 Minor GC,大部分对象会被垃圾回收器回收,剩下的存活的对象放入 From 区,这是复制回收算法
  • 当 From区满时,进行 Minor GC 会对 From 区也做 GC,存活的会进入 To 区,此时 From 区与 To 区角色互换
  • 当 Eden 区再次满的时候, Minor GC 会把存活下来的对象放入 To 区(因为From 区与 To 区已经角色互换)

老年代

  • 当 Minor GC之后 新生代满或放不下新对象时,会触发担保机制,把存活的对象放入老年区
  • 当整个堆空间放满之后,会进行 Full GC,Full GC = Minor GC + Major GC,System.gc() 引起的是 Full GC

元空间

  • JDK1.7之前: 永久代
  • JDK1.8之后: 元空间(直接内存),这样设计是为了解决永久代可能溢出的问题,可以动态扩容,属于堆外内存,可能会挤压堆内内存,所以根据需求定义其大小

堆空间大小的设定

通过GC日志获取多次 full GC 存活下来的活跃数据的平均值
总堆大小 = 活跃数据 * 4
新生代 = 活跃数据 * 1.5
老年代 = 总堆大小 - 新生代
(虚拟机有默认值和自定调整)

内存规整

当多线程进行对Eden区存放堆数据的时候会出现线程安全问题(指针碰撞)
指针碰撞
栈上分配:线程在Eden区有自己的Buffere(可调整),可避免过多的锁,当Buffere不足时会清除到堆内存

堆的调整

-Xms …m 堆的起始大小(start)
-Xmm …m 堆的最大值(max)
-Xmn …m 堆的新生代大小(new)
-Xss …m 每个线程的栈大小

方法区(线程共享)

线程共享,保存类信息、静态变量、常量(jdk1.7)

程序计数器(线程私有)

  • 指向当前线程所执行的jvm指令的地址
  • 当线程挂起时,可以保存线程运行的状态,以便下次继续执行

虚拟机栈(线程私有)

  • 线程私有,保存当前线程运行方法所需要的数据、指令和返回地址,一个方法对应一个栈帧,每调用一个方法会压入一个栈帧,方法执行完出栈
  • 每一个栈帧包含局部变量表、操作数栈、动态链接、方法出口等
    虚拟机栈

局部变量表

保存当前线程执行的方法的局部变量,宽度为4字节(32位),当线程要对变量操作时,会从栈顶的局部变量表开始找,找不到再往下找

操作数栈

  • 保存线程执行指令相关的操作数
  • 操作数栈和局部变量表是紧密联系的,例如 int c = a + b 在虚拟机底层执行的指令:
    iload_1
    iload_2
    iadd
    istore_3
    指令的执行过程

动态链接

保存线程方法运行时引用的类库方法的接口地址

方法出口

方法执行完成的返回地址,正常返回:return,异常:执行异常处理

本地方法栈(线程私有)

线程私有,保存nactive方法,底层用c/c++实现的、没有实现类的方法

GC-垃圾回收

垃圾回收机制是针对堆空间中存放的对象数据的, 进行垃圾回收可以在一定程度上节省内存空间,以便放入更多的数据

判断算法

引用计数法

给每一个对象增加一个被应用计数标志以判断该对象是否可被清楚,但可能会出现循环引用,所以JVM不用这个算法

可达性分析

GC Root:

  • 虚拟机栈中局部变量表引用的对象
  • 方法区中静态变量和常量引用的对象
  • 本地方法栈中JNI引用的对象
    可达性分析
    当带对象节点不可达时,会进入finalize()方法,不一定会立即被回收,看有没有重写finalize()方法

回收算法

标记-清除算法

对不可达的对象进行标记和清除,但是会产生内存碎片
标记-清除算法

复制-回收算法

对不可达对象进行回收,存活下来的对象放入servivor区
复制-回收算法

标记-整理算法

对不可达对象进行标记、清除和整理,因为进行了在 GC 的时候整理所以不会产生内存碎片
标记-整理算法

垃圾集收器

  • 虽然每个进程开启一个 jvm,但同一个 jdk 默认垃圾集收器相同
  • 新生代的垃圾集收器:serial / parNew / parallel 都用的是复制回收算法
  • 老年代的垃圾集收器:CMS(标记 - 清除) / serial Old(标记 - 整理) / parallel Old(标记 - 整理)
    垃圾集收器

新生代(用复制回收算法)

serial

单线程执行垃圾回收代码,垃圾回收时其他线程不能执行,即应用线程停顿(stop-the-world)
serial

parNew

多线程执行垃圾回收代码,垃圾回收时其他线程不能执行,在多核心CPU的情况下停顿时间会比 serial 少
parallel
配置线程个数:-XX:parallelGCThreads=n

parallel

主要关注吞吐量,吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾回收时间)

-XX:MaxGCPauseMillise=n // 控制GC停止时间
-XX:GCTimeRatio=n // 控制GC运行时间的比率
-XX:UseAdaptivesizePolicy // 开启全局维护策略

老年代

CMS (用标记 - 清除算法)

  • 主要关注减小回收停顿时间(stop-the-world),但增加了回收时间,这是 CMS 与 parallel的区别
  • 先进行GC Root可达标记
  • 再进行并发标记,在并发标记的同时业务线程能并发运行
  • 接下来 stop-the-world 重新整理,因为并发标记的过程用户线程也在执行
  • 接着进行并发清除
    CMS
    -XX:CMSInititiongOccupancyFraction // 碎片整理
    -XX:+UseCMSCompactAtFullCollection // Full GC 时开启压缩整理
    -XX:CMSFullGCsBeforeCompaction // 设置多少次 Full GC 之后开启压缩

serial Old (用标记 - 整理算法)

CMS备用预案,触发担保机制后老年代空间不足时,会进行 Full GC

parallel Old (用标记 - 整理算法)

查看当前JVM的垃圾集收器

-XX:+PrintFlagsFinal
UseGC

-XX:+PrintCommandLineFlags
UseGC

GC 日志

输出日志

-Xloggc: /…gc.log
分析 gc.log 的信息,例如:
解读:[GC类型 GC前占用空间 -> GC后占用空间(总空间), GC耗时]
gc.log

-XX: +PrintGCTimeStamps
-XX: +PrintGCTimeStamps

-XX: +PrintGCDetails
-XX: +PrintGCDetails

日志文件控制

-XX: -UseGCLogFileRotation
-XX: GCLogFileSize=8k

监视日志文件命令

tail -f gc.log // GC日志
tail -f catalina.log // 应用日志

JDK自带监控工具

jconsole // java 监视和管理控制台
jconsole

jps // 拿到pid
jps

jmap -heap pid // heap使用情况
jmap -heap pid

jstat -gcutil pid 毫秒 // GC数据统计(Old GC没有变化且还在增长 => 内存泄漏)
gcutil

jstat -gccause pid 毫秒 // 引发GC的原因
gccause

jvisualVM // 查看线程运行状况和dump等
jvisualVM
dump

jstack pid > p.txt // 导出线程的dump

MAT

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/…error.hprof

  • 查找内存泄漏原因:
  • GC日志: xxx -> xxx(xxx) 有没有回收
  • MAT: Retained Heap 和 GC Root 指向
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章