JVM和垃圾回收相关知识点总结

231 阅读15分钟

JVM相关参数

# jvm 参数配置case
-Xmx2g -Xms2g -Xmn1g -XX:PermSize=128m -Xss256k
-XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection -XX:+UseFastAccessorMethods
-XX:+UseCMSInitiatingOccupancyOnly

-Xmx2g   堆所占最大内存2g
-Xms2g    堆最小内存2g
-Xmn1024m  新生代(new区)1024M
-XX:PermSize=128m  perm区128M
-XX:SurvivorRatio=5  新生代中eden:Survivor1=5:1并且Survivor1:Survivor2=1,此值默认为8(这里有IBM调研报告,Java程序在运行过程中80%的对象实例是朝生夕死的)
-XX:MaxTenuringThreshold=15  对象晋升老年代年龄条件15次(默认值15)
-XX:+UseCMSCompactAtFullCollection  jvm在FullGC之后整理内存空间清除碎片
-XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled  -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -
-XX:CMSInitiatingOccupancyFraction  老年代触发fgc百分比 1-100
-XX:+UserAdaptiveSizePolicy  选项之后,就不需要指定-Xmn、-XX:SurvivorRatio等参数,虚拟机可以根据当前系统的运行情况动态收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量

# 配置说明
堆内存最大值2G,最小值2G
老年代内存1G
新生代E区与交换区内存为默认比例(8:1),及E区800M,两个交换区个100M
老年代晋升年龄为默认值(15)

JVM 内存

Java运行时内存分为:

  • 程序计数器:程序计数器是一块线程私有的内存区域,它是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变程序计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常跳转、线程恢复等基础功能都需要依赖这个计数器来完成。每个线程都需要维护一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储。
  • java虚拟机栈:Java虚拟机栈是一块线程私有的内存区域,它总是和某个线程关联在一起,每当创建一个线程时,JVM就会为其创建一个对应的Java虚拟机栈,***-Xss 为jvm启动的每个线程分配的内存大小,用于存储Java方法执行时用到的局部变量表、操作数栈、动态链接、方法出口等信息***。每当一个方法执行时,创建一个栈帧置于栈顶。每当一个方法执行完成时,这个栈帧就会弹出栈帧的元素作为这个方法的返回值,并清除这个栈帧。Java虚拟机栈栈顶的栈帧就是当前正在执行的活动栈帧,也就是当前Java虚拟机正在执行的方法,程序计数器也会指向这个地址。只有活动的栈帧的局部变量可以被操作栈使用,当这个栈帧调用另一个方法时,与新方法对应的新栈帧也随之被创建,并放到Java虚拟栈的顶部,变为新的活动栈帧,当这个栈帧执行完成时这个栈帧移出Java栈,刚才的那个栈帧又变为活动栈帧,前面栈帧的返回值又变为这个栈帧的操作栈中的一个操作数。如果前面的栈帧没有返回值,那么当前的栈帧的操作栈的操作数就不会有任何变化。
  • 本地方法栈:本地方法栈是为JVM运行Native方法使用的空间,它也是线程私有的内存区域。
  • 堆:Java堆JVM所管理的内存中最大的一块,是JVM管理Java对象的核心存储区域,它是被所有线程共享的一块内存区域,在JVM启动时创建。Java堆是存储Java对象实例的地方,是Java应用程序与内存关系最密切的存储区域。每一个存储在堆中的Java对象多时这个对象的类的一个副本,它会复制包括继承自它父类的所有非静态属性。JVM堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可,可通过-Xmx设置最大Java堆的大小,-Xms设置初始化时Java堆大小
  • 方法区:方法区是线程共享的内存区域,它用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也就是我们通常所说的永久区,它的大小可通过参数—XX:PermSize、—XX:MaxPermSize进行设置。
    运行时数据区

垃圾回收算法

标记-清除算法

enter image description here

缺点:1.标记和清除两个过程的效率都不高;2. 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一个垃圾回收动作。

复制算法

解决标记-清除存在的效率问题,复制算法将内存划分为相等的两块,每次只使用其中一块。当这一块内存用完时,就将还存活的对象复制到另一块上面,然后将已经使用过的内存空间一次清理掉。 缺点:将内存缩小为了原来的一半,对内存空间耗费较大。在对象存活率较高时,需要进行多次复制操作,效率会变低。

标记-整理算法

将原有标记-清除算法进行改造,不是直接对可回收对象进行清理,而是让所有存活对象都向另一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

在新生代采用复制算法,在老年代采用“标记-清除”或者“标记-整理”算法。新创建的对象被分配在新生代,如果对象经过几次回收后仍然存活,那么就把这个对象划分到老年代。老年代的收集频度不象年轻代那么频繁,这样就减少了每次垃圾回收所需要扫描的对象,从而提高了垃圾回收效率。

  • Young区分为Eden区和两个相同大小的Survivor区,其中所有新创建的对象都分配在Eden区域中,当Eden区域满后会触发minor GC 将Eden区仍然存活的对象复制到其中一个Survivor区域中,另外一个Survivor区中的存活对象也复制到这个Survivor区域中,并始终保持一个Survivor区时空的。一般建议Young区地大小为整个堆的1/4,使用ParNew 并行收集器。
  • Old区存放Young区Survivor满后触发minor GC后仍然存活的对象,当Eden区满后会将存活的对象放入Survivor区域,如果Survivor区存不下这些对象,GC收集器就会将这些对象直接存放到Old区中,如果Survivor区中的对象足够老,也直接存放到Old区中。如果Old区满了,将会触发Full GC回收整个堆内存。
  • Perm区主要存放类的Class对象和常量,如果类不停地动态加载,也会导致Perm区满。Perm区地垃圾回收也是有Full GC触发地

垃圾收集器

并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态; 并发:指用户线程与垃圾收集线程同时执行。

Serial 收集器

单线程串行收集器,它进行垃圾收集时,必须暂停其他所有的工作线程,直到它垃圾收集结束。

ParNew 收集器

ParNew收集器是Serial 收集器的多线程版本,俗称并行收集器,是新生代首选的收集器。 ParNew收集器是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC强制指定。使用-XX:ParallelGCThreads可以限制垃圾收集的线程数。

Parallel Scavenge 收集器

这也是一个并行的新生代垃圾收集器,不同于其他收集器(以尽可能缩短垃圾收集时用户线程的停顿时间为目的),它是唯一一个以达到一个可控制的吞吐量为目标的垃圾收集器。 Parallel Scavenge 收集器可以使用自适应调节策略,使用-XX:+UserAdaptiveSizePolicy选项之后,就不需要指定-Xmn、-XX:SurvivorRatio等参数,虚拟机可以根据当前系统的运行情况动态收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

Serial Old 收集器

该收集器使用标记-整理算法对老年代垃圾进行回收,它主要的两大用途:1. 配合Parallel Scavenge收集器;2. 作为CMS收集器在并发收集出现Concurrent Mode Failure时使用的后备预案。

Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。在注重吞吐量和CPU资源敏感的场合,优先考虑使用Parallel Scavenge + Parallel Old收集器的组合,切记Parallel Scavenge 是无法与CMS收集器组合使用的。

Concurrent Mark Sweep 收集器

cms gc由以下几个主要步骤构成:

  • initial-mark: 这步是stw的;
  • concurrent-mark: 这步是和应用并发运行;
  • preclean: 和应用并发运行;
  • remark: 这步是stw的;
  • concurrent-sweep: 这步是和应用并发运行;

CMS收集器是一款并发收集器,是一种以获取最短回收停顿时间为目标的收集器,它是基于标记-清除算法实现的,它整个过程包含四个有效的步骤:初始标记——>并发标记——>重新标记——>并发清除。 其中,初始标记、重新标记仍然需要"Stop the World",但是它们的速度都很快。初始标记只是标记一下GC Roots能直接关联到的对象,重新标记是为了修正并发标记期间因为用户线程继续运作而导致标记产生变动的那一部分对象的标记记录。并发标记是进行GC Roots Tracing的过程。

缺点:

  • CMS收集器对CPU资源非常敏感,在并发阶段,它虽然不会导致用户线程停顿,但是它会占用一部分CPU资源进行垃圾收集从而导致应用程序变慢,总吞吐量会降低。
  • 由于CMS并发清除阶段用户线程还在运行,伴随程序的运行必然还有的新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再进行清理。也是由于垃圾收集阶段用户线程还需要运行需要预留足够内存给用户线程使用,如果CMS运行期间预留内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,虚拟机只得临时启动Serial Old进行老年代垃圾收集,这样会导致长时间停顿
  • 由于CMS是一款采用标记-清除算法实现的垃圾收集器,收集结束时会有大量的空间碎片产生,空间碎片过多时,如果分配大对象找不到足够大的连续空间分配当前对象,就不得不提前触发一次Full GC。

G1收集器

g1收集器的相关介绍

相关参数总结

垃圾收集器设置的相关选项

参数 描述
UseSerialGC 虚拟机运行在Client模式上的默认值,打开此开关后,使用Serial + Serial Old收集器组合进行内存回收
UseParNewGC 打开此开关后,使用ParNew + Serial Old收集器组合进行内存回收
UseConcMarkSweepGC 打开此开关后,使用ParNew + CMS + Serial Old收集器组合进行内存回收,Serial Old将作为CMS收集器出现Concurrent Mode Failure时的后备收集器使用
UseParallelGC 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Parallel Old收集器组合进行内存回收
UseParallelOldGC 打开此开关后,使用Parallel Scavenge + Parallel Old收集器组合进行内存回收

垃圾收集器设置的相关参数

参数 描述
SurvivorRatio 新生代中Eden区域与Survivor区域的容量比值,默认为8:1
PretenureSizeThreshold 新生代直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenuringThreshold 晋升到老年代的年龄。一个对象坚持过一次Minor GC后,年龄就增加1,当超过阀值就进入老年代
UseAdaptiveSizePolicy 动态调整Java内存各个区域的大小和进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活下来的极端情况
ParallelGCThreads 设置并行GC时进行内存回收的线程数
GCTimeRatio GC占总时间的比率,仅在Parallel Scavenge收集器时生效
MaxGCPauseMillis 最大GC停顿时间,仅在Parallel Scavenge收集器时生效
CMSInitialOccupancyFraction 设置CMS收集器在老年代空间被使用多少后触发垃圾收集,默认值为68%,仅在CMS收集器时生效
UseCMSCompactAtFullCollection 设置CMS收集器在完成垃圾收集后是否进行一次内存碎片整理,仅在CMS收集器时生效
CMSFullGCsBeforeCompaction 设置CMS收集器在完成若干次垃圾收集后再进行一次内存碎片整理,仅在CMS收集器时生效

内存分配和回收策略

  1. 对象优先在eden分配
  2. 大对象直接进入老年代,虚拟机提供-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样做可以避免在Eden和两个Survivor区域之间发生大量的内存复制操作;
  3. 长期存活对象将进入老年代,对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置,当对象达到这个年龄后就将进入老年代;
  4. 动态对象年龄判定,如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄;
  5. 空间分配担保,在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年大最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行一次Minor GC。如果小于或者没有设置HandlePromotionFailure,则要进行一次Full GC。 取平均值进行比较其实仍然是一种动态概率的手段,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败。如果出现了HandlePromotionFailure失败,则会重新发起一次Full GC,大部分情况都会将HandlePromotionFailure打开,避免过于频繁的Full GC。

GC触发的条件

  • 老年代区域使用内存超限
  • 内存碎片引起新生代晋升失败(Young GC触发的条件)
  • 永久代区域内存空间满
  • 调用**System.gc()**方法

老年代内存增长的原因

  • 大对象直接在老年代空间分配
  • 生存年龄超限移动到老年代
  • Young GC时交换空间不足
  • Full GC导致年龄未超限的实例移动到老年代

案例分析

问题排查常用方法

安装JDK
sudo yum install java-1.8.0-openjdk-devel.x86_64
jstat -gc 4079 5s 1000

## 使用jstat查看gclog (jstat参数说明:-gcutil :gc日志 , 25515: java进行id, 1s:间隔1秒打印一次,10:共打印10条,可选参数 )
jstat -gcutil 25515 1s 10

## 使用jmap命令,dump内存快照 (jmap参数说明: -dump:生成快照, file:指定文件名称, 25515 :java进程id)
jmap  -dump:format=b,file=/home/admin/dumfile1 25515

jmap -histo 2083   // 查看进程2083加载的实例个数统计及类名称
jmap -histo:live 2083 // 查看进程2083加载的实例个数统计及类名称

## 使用 Eclipse Memory Analyzer分析快照文件

为我的团队宣传一下,团队现在有大量hc,寻技术大拿!

职位描述

  1. 负责信息流、互动、直播等在线服务的架构设计及需求开发工作;
  2. 负责小程序、开放平台网关、授权、OpenApi等系统的架构设计及需求开发工作;
  3. 负责服务端的业务需求分析、架构设计,并承担主力核心设计、研发工作,并确保高质量;
  4. 负责基础架构改造、服务化、沉淀工具、效能提升等工作。

职位要求

  1. 5年以上互联网服务端开发经验,有高并发大流量业务开发和架构优化经验,熟悉所负责业务的整个链路;
  2. 编程基本功扎实,熟悉常用数据结构和算法,MySQL数据库及主流NoSQL数据库;
  3. 优秀的逻辑思维能力,较强的抽象概括和总结能力;
  4. 有较好的沟通交流能力,善于主动思考和行动,乐于解决具有挑战性的问题,对待技术有强烈兴趣;
  5. 有较强的推动协作能力和一定的团队管理经验,承压能力强;