JVM&GC浅析

 2023-02-09
原文作者:鲁大湿 原文地址:https://juejin.cn/post/6911577065762488334

1.JVM结构图

202301011652056131.png

2.运行时公有数据区

方法区:jvm有一个方法区,在所有jvm线程间共享,主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据,java7之前方法区是一个“逻辑部分“,后来为了与堆做区分,方法区也就是人们常说的永久代,jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中,jdk1.8中,已经不存在永久代(方法区),替代它的一块空间叫做“元空间”,和永久代类似,都是JVM规范对方法区的实现,但是元空间并不在虚拟机中,而是使用本地内存,元空间的大小仅受本地内存限制,但可以通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize来指定元空间的大小。

202301011652063652.png

202301011652070093.png

堆:当对象创建,实例产生,数组分配内存的时候都在这里,它是JVM管理的内存中最大的一块区域,堆内存和方法区都被所有线程共享,堆在jvm启动时创建,对象不需要显示释放,通过java的GC机制释放

3.运行时私有数据区

pc寄存器(程序计算器):jvm支持多线程运行,每个线程都有自己单独的pc存储器,任何时候一个线程只能运行一个方法的代码,如果方法不是native的,pc寄存器包含当前正在被执行的jvm指令地址,如果方法是native的,pc寄存器的值是未定义的

jvm栈:每一个jvm线程都有一个私有的jvm栈,每个jvm都有若干个栈帧,随着线程的创建而创建,栈中存储的是帧,保存局部变量和计算结果,主要用于出栈和入栈,每一个Java方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。

native方法栈:native方法不是用Java语言写的,jvm能加载native方法栈,所以不需要提供native方法需要的栈

4.内存溢出

堆内存溢出(OutOfMemoryErro):堆内存主要存放对象,数组等,当对象操作最大容量时,会在年轻代进行一次轻GC,如果轻GC后空间不足把该对象和新生代满足条件的对象放入老年代(一般年轻代达到15岁时候就就会寄存到老年代),如果老年代空间不足就会产生fullGC,之后如果再发生空间不足就会抛出OutOfMemoryError异常

虚拟机,方法栈溢出(StackOverflowError):虚拟机栈中的栈帧数量过多

5.垃圾回收

引用计数法:即每次产生对象时+1,并计录,销毁时-1,等到计数为0的时候,就可以回收,但是存在循环的问题,就是A对象引用了B对象,B对象又引用了A对象,即互为引用关系,会产生一直循环的情况,这个时候的引用计数法显然就不起作用了 如图所示,虽然a和b都已经为空了,但是因为互相引用(引用计数都为1),导致无法回收

202301011652074794.png

可达性计算:首先要了解一个东西,可作为GCRoot的:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象

  • 方法区中类静态属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象 通过GC形成一条引用链,直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为「垃圾」,会被 GC 回收。但是要注意出现可达不可用的情况的时候,对象是不会被GC回收的

    202301011652080105.png

6.垃圾回收算法

标记清除法:根据可达性算法算可以回收的对象,如图黄色部分,这种算法的弊端是会产生内存碎片

202301011652085276.png

复制算法:把堆等分成两块区域, A 和 B,区域 A 负责分配对象,区域 B 不分配, 对区域 A 使用以上所说的标记法把存活的对象标记出来(下图有误无需清除),然后把区域 A 中存活的对象都复制到区域 B(存活对象都依次紧邻排列)最后把 A 区对象全部清理掉释放出空间,这样就解决了内存碎片的问题了

202301011652089427.png

缺点:这种算法会浪费内存空间,每次回收要把存活的对象移动到另一半,效率比较低下

标记整理法:前面两步和标记清除法一样,不同的是它在标记清除法的基础上添加了一个整理的过程 ,即将所有的存活对象都往一端移动,紧邻排列(如图示),再清理掉另一端的所有区域,这样的话就解决了内存碎片的问题。

202301011652094988.png

缺点:这种算法每次清除垃圾都要频繁移动存活对象,效率低下

分代收集算法:首先了解四个区域,Eden(伊甸园区),S0(幸存者0区),S1(幸存者1区),old(老年代)比例为8:1:1,在伊甸园以下称E发生轻GC,如果对象存活,存储到S0,当下次E发生轻GC时,存活的对象包括S0的存活对象全部移动到S1区,反复执行,直到当S0或者S1的区域对象达到15岁时,把对象移动到old,当老年代满了时就发生fullGC

7.垃圾收集器

  • 在新生代工作的垃圾回收器:Serial, ParNew, ParallelScavenge
  • 在老年代工作的垃圾回收器:CMS,Serial Old, Parallel Old
  • 同时在新老生代工作的垃圾回收器:G1 以下有连线的是可以一起工作的收集器

202301011652105969.png

新生代收集器

Serial收集器:此收集器是单线程的,意味着只会使用一个CPU或一个收集线程来完成垃圾回收,看起来单线程垃圾收集器不太实用,不过我们需要知道的任何技术的使用都不能脱离场景,在 Client 模式下,它简单有效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 单线程模式无需与其他线程交互,减少了开销,STW 时间可以控制在一百多毫秒内,只要不是频繁发生,这点停顿是可以接受的,所以对于运行在 Client 模式下的虚拟机,Serial 收集器是新生代的默认收集器

ParNew收集器:ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程,与Serial相比,它主要工作在server模式下,多线程可以让垃圾回收得很快,在多 CPU 的情况下,由于 ParNew 的多线程回收特性,毫无疑问垃圾收集会更快,也能有效地减少 STW 的时间,提升应用的响应速度。

Parallel Scavenge收集器:Parallel Scavenge 收集器也是一个使用复制算法,多线程,工作于新生代的垃圾收集器,看起来功能和 ParNew 收集器一样,它有啥特别之处吗,关注点不同,此收集器更多关注的是吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间),CMS是为了用户更好的交互,减少GC的时间,而Parallel更适合做后台运算等不需要太多用户交互的任务。 Parallel Scavenge 收集器提供了两个参数来精确控制吞吐量,分别是控制最大垃圾收集时间的 -XX:MaxGCPauseMillis 参数及直接设置吞吐量大小的 -XX:GCTimeRatio(默认99%)

老年代收集器

Serial Old收集器:上面我们知道 Serial 收集器是工作于新生代的单线程收集器,与之相对地,Serial Old 是工作于老年代的单线程收集器

Parallel Old 收集器:Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理法,两者组合示意图如下,这两者的组合由于都是多线程收集器,真正实现了「吞吐量优先」的目标

CMS收集器:是SWT最短的收集器,如果应用很重视服务速度,可以考虑使用CMS, CMS 虽然工作于老年代,但采用的是标记清除法,可以认为 CMS 收集器的内存回收过程是与用户线程一起并发执行的,当然人无完人,CMS也是存在他的弊端的:

  • 吞吐量:CMS收集器对CPU非常敏感,比如原本我有十个线程处理用户请求,使用CMS却需要分出三个线程来进行回收处理,吞吐量会下降30%, CMS默认回收线程数是:(CPU数量+3)/ 4,那么如果CPU数量只有一两个,那么吞吐量是直接下降到50%的,显然是不可行的
  • 无法处理浮动垃圾:可能出现 「Concurrent Mode Failure」而导致另一次 Full GC 的产生,在处理垃圾的时候用户线程同时可以执行,所以清理的同时垃圾也在同时产生,而新产生的这部分垃圾叫做浮云垃圾,在垃圾收集阶段用户线程也要继续进行,这就意味着CMS不能像其他收集器一样在老年代收集满了之后再进行回收,可以通过XX:CMSInitiatingOccupancyFraction进行设置达到68%,切记不能设置太高,不然产生Concurrent Mode Failure ,就会启动Serail Old重新进行老年代收集,理所当然SWT时间也就更长了
  • 会产生大量的内存碎片:因为CMS采用的是标记清除方法,这种垃圾收集算法最大的弊端就是会产生不连续的内存碎片,如果无法找到足够的空间来分配内存,就会触发FullGC,非常影响性能,可以用 -XX:+UseCMSCompactAtFullCollection设置,默认是开启的,可以用来合并整理内存碎片,内存整理会导致SWT时间增长,XX:CMSFullGCsBeforeCompation 用来设置执行多少次不压缩的 Full GC 后跟着带来一次带压缩的。

G1收集器:是面向服务端的垃圾收集器,是目前最牛逼的垃圾收集器,解决了CMS的弊端,其运用的算法是标记整理算法,可以消除内存碎片,并且不会像CMS那样牺牲吞吐量,在 STW 上建立了可预测的停顿时间模型,用户可以指定期望停顿时间,G1 会将停顿时间控制在用户设定的停顿时间以内,传统的收集器如果发生 Full GC 是对整个堆进行全区域的垃圾收集,G1通过分配成各个 Region ,方便 G1 跟踪各个 Region 里垃圾堆积的价值大小(回收所获得的空间大小及回收所需经验值),这样根据价值大小维护一个优先列表,根据允许的收集时间,优先收集回收价值最大的 Region,也就避免了整个老年代的回收,也就减少了 STW 造成的停顿时间。同时由于只收集部分 Region,可就做到了 STW 时间的可控。