🚩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)
- eden(8/10),可以通过
- 老年代 (2/3)
- 大对象
- 长生命周期对象,
-XX:MaxTenuringThreshold
调整,默认 15,取值 0~15 - from+to 空间不足
- OutOfMemoryError:堆空间不足,调整 Xmx
- -Xms 初始堆空间,-Xmx 堆空间上限,建议设置相等(物理内存的 1/4),在考虑其他项目的内存使用情况时,尽量大。
- 4核8G 实践:一般配置 -Xms2g -Xmx2g -XX:ParallelGCThreads=4
- 新生代(1/3)
- 🌟方法区(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过程
- Java 应用不断创建对象,通常都是分配在 Eden 区域,当其空间占用达到一定阈值时,触发 minor GC。仍然被引用的对象存活下来,被复制到 JVM 选择的 Survivor 区域,而没有被引用的对象则被回收。
- 经过一次 Minor GC,Eden 就会空闲下来,直到再次达到 Minor GC 触发条件,这时候,另外一个 Survivor 区域则会成为 to 区域,Eden 区域的存活对象和 From 区域对象,都会被复制到 to 区域,并且存活的年龄计数会被加 1。
- 类似第二步的过程会发生很多次,直到有对象年龄计数达到阈值,这时候就会发生所谓的晋升(Promotion)过程,超过阈值的对象会被晋升到老年代。
STW为什么需要停顿所有的Java执行线程呢?
- 确保一致性快照,让整个执行系统看起来像冻结在某个时间点上了,如果出现分析过程中对象引用关系还在不断变化,则分享结果的准确性是无法保证的。
- STW事件和采用哪款GC无关,所有的GC都是有这个事件
- 越优秀,回收效率越高的垃圾回收器,尽可能地缩短暂停时间。
- STW是JVM在后台自动发起和自动完成的。是在用户不可见的情况下,把用户正常的线程全部停掉,再去把不用的对象都干掉。
G1 GC的好处?
-xx:+UseG1GC
优势:
- 并行与并发 - 多个垃圾收集线程同时工作(并行),垃圾收集线程与用户线程交替执行(并发),分成三个阶段:
- 年轻代垃圾回收
- 并发标记
- 混合收集(Eden + from -> to,old -> old’)
- 分代收集 -
G1
将堆空间划分为若干个区域(Region
),每个区域都可以充当 eden、s0、s1 和 humongous(大对象),它不要求年轻代,老年代是连续的。- mixed gc:部分区域,G1 特有
- minor gc:新生代,时间短(STW)
- full gc:新生代+老年代,时间长(STW)
- 复制算法:
G1
内存回收使用Region
作为基本单位,对内存空间进行整理。 - 可预测的停顿时间模型 - 可以让用户明确指定在一个长度为
M
毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N
毫秒。
缺点:相比于CMS,G1还不具备全方位、压倒性优势。比如,G1为了垃圾收集产生的内存占用
以及程序运行时的额外执行负载都比CMS要高。