最全的GC学习文章

共 6668字,需浏览 14分钟

 ·

2021-04-29 17:31

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达

76套java从入门到精通实战课程分享

需要有jvm内存模型的概念

什么是GC

GC就是垃圾回收,不是java独有的,甚至比java出现的还早


为什么要GC

像C语言是程序员自己管理内存的,很麻烦,java中自动GC,避免了OOM这种异常的出现,方便管理内存空间


GC的对象是什么

GC既然是管理内存的,也就和JVM挂了钩。而JVM中,并不是每一块空间都需要做GC的,不然太大了,而且也失去了瓜分内存模型的一大必要性。

GC的对象主要是JVM中的堆,部分虚拟机也对方法区中的废弃类废弃常量做回收,但主要还是堆。

因为本地方法栈、虚拟机栈、程序计数器等的内存要么是会随着线程进行,方法的入栈出栈等自动做回收的,要么就不需要回收。

而方法区的内存都是相对固定,因为存储的都是类或者方法的元数据信息,是一开始就定好的不会在运行中发生变化。

唯有堆里面是线程共享的对象,而且很动态,需要一套合理的GC规则来管理。

我们讨论的主要就是这个


GC线程

GC线程和业务线程显然是不能并行的,不然容易造成内存回收混乱

所以有了Stop-The-World–全局停顿的概念,也就是串行化,所有Java代码停止,native代码可以执行,但不能和JVM交互


如何确定一个对象为垃圾

引用计数法 Reference Counting

很好理解的一种算法,初衷就是,一个堆中的对象,没人引用就回收,有人引用就不回收。

具体的实现如下:

每当堆中的对象有一个引用的时候,引用就+1.当引用为0的时候,判断该对象为可回收的垃圾。

但是如果两个对象循环引用,比如下图中的实例1和实例2,这两个对象是无效的,但计数不为0无法回收,会发生内存泄漏


可达性分析算法/根搜索算法 GC Roots Tracing

吸取引用计数法的教训,不能做简单的引用数+1-1的操作

以一系列叫“GC Roots”的对象为起点开始向下搜索,走过的路径称为引用链(Reference Chain),当一个对象没有和任何引用链相连时,证明此对象是不可用的,用图论的说法是不可达的。那么它就会被判定为是可回收的对象。

如下图所示,这种算法下,只有1246这种直接被‘根’所引用的对象会判断为可达,35这种是不可达,可以被回收。


在Java语言中,可作为 GC Roots 的对象包括下面几种:

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

b. 方法区中类静态属性引用的对象。

c. 方法区中常量引用的对象。

d. 本地方法栈中 JNI(Native方法)引用的对象


显然,堆中的引用是不具备这个资格的,解决了循环引用的隐患


java中的四种引用

既然判断一个对象是否该被回收是通过引用判断的,那么了解一下java中的引用

  • 强引用,最常用的引用,只要这类引用还在,垃圾回收器就不会回收它指向的对象

  • 软引用,如果快溢出了,先回收这部分引用指向的对象

  • 弱引用,只要垃圾回收器工作,就会回收它

  • 虚引用,无法通过这种引用获取对象实例,它的唯一作用就是,它指向的对象被回收的话,会收到一个通知。

        当然,上述的引用计数法、可达性分析算法都是基于强引用的。

怎么回收一个对象

标记/清除算法 Mark-Sweep

这是一个最基本的GC算法。他根据根搜索算法,将所有可达对象都标记出来,然后对剩下的不可达对象做清楚。

但是有两个缺点,一个是标记和清除的效率都比较低,另一个是这样清除出来的空间过于碎片化,之后要分配一些比如数组之类的对象,可能会有影响。


复制算法 Copying

复制算法是将内存分为等大的两块,每次用一块A,空一块B,在GC时,将存活的可达对象都挪到B,对A做一个整体的GC。

这种做法解决了内存碎片化的问题,但是造成了一半的内存浪费,而且复制的时候效率也不高。不适用于存活对象较多的内存


标记整理算法 Mark-Compact

标记整理算法可以看作结合了标记/清除算法和复制算法的思想,它的标记阶段和标记清除算法一样,但是标记完不做直接清除,而是将可达对象挪到内存的一侧,避免碎片化,然后再做GC。

或者叫标记-移动-清除算法也行


分代收集算法

分代算法是现在的JVM厂商使用的主流算法,它结合了上述的算法思想,扬长避短

对象刚创建出来是在新生代,年龄达到15(默认)后到老年代,这样根据对象的存活时间设置到不同区域,不同区域采用不同算法

对于新生代,对象刚创建基本都是在这里(除了某些内存特别大的,会直接到老年代),这些对象在每次GC的时候(新生代的GC又叫做YoungGC、MinorGC、YGC),只有少量存活,所以对存活的对象使用复制算法即可,成本较低。

新生代内又分三个区:一个 Eden 区,两个 Survivor 区(S0、S1,又称From Survivor、To Survivor),大部分对象在 Eden 区中生成。

具体复制,对象刚创建基本都在新生代的Eden区,当 Eden 区满时会进行一次YCG,YGC后还存活的对象将被复制到两个 Survivor 区(中的一个);当这个 Survivor 区满时,此区的存活且不满足晋升到老年代条件的对象将被复制到另外一个 Survivor 区。

对象每经历一次复制,年龄加 1,达到晋升年龄阈值后,转移到老年代。

在新生代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到老年代,该区域中对象存活率高。老年代的垃圾回收通常使用“标记-整理”算法。

还有一点就是,新生代和Eden和s0、s1的大小一般是8:1:1,比如以一个大小为9的对象创建,是直接担保进入老年代的。


GC事件

根据垃圾收集回收的区域不同,垃圾收集主要分为:

  • Young GC

  • Old GC

  • Full GC

  • Mixed GC

  • Young GC

新生代内存的垃圾收集事件称为 Young GC(又称 Minor GC),当 JVM 无法为新对象分配在新生代内存空间时总会触发 Young GC。

比如 Eden 区占满时,新对象分配频率越高,Young GC 的频率就越高。

Young GC 每次都会引起全线停顿(Stop-The-World),暂停所有的应用线程,停顿时间相对老年代 GC 造成的停顿,几乎可以忽略不计。而且触发频繁,需要一种高效的回收算法。

Old GC 、Full GC、Mixed GC

Old GC:只清理老年代空间的 GC 事件,只有 CMS 的并发收集是这个模式。

Full GC:清理整个堆的 GC 事件,包括新生代、老年代、元空间等 。当老年代或者持久带满了,或者System.gc被显式的调用都会触发Full GC。

Mixed GC:清理整个新生代以及部分老年代的 GC,只有 G1 有这个模式


垃圾收集器

收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现

先说左边的

serial、parNew、Parallel Scavenage是年轻代收集器

下面的so、cms、po(简称)是老年代收集器

当然是可以组合使用的

比如JDK1.8用的就是PS+PO,这是一个吞吐量优先的组合

pn+cms则是响应时间优先的组合

serial和so都是单线程串行的,回收的时候要stop-the-world,基本被淘汰了


至于右边的

G1是包含了年轻代和老年代的收集器

ZGC是一个jdk11的试验品

Epsilon是一个调试工具


GC日志

GC日志是替换的不是追加的


通过以下的命令参数来设置GC日志的输出:

-XX:+PrintGC 输出GC日志

-XX:+PrintGCDetails 输出GC的详细日志

-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)

-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)

-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息

-Xloggc:…/logs/gc.log 日志文件的输出路径


IDEA中查看GC日志

比如,拿2021版的最新的idea举例

新建一个demo:

/**
 * @Author: luhui
 * @Date: 2021/4/25 20:35
 */
public class GcDemo {
    public static void main(String[] args) {
        int _1m = 1024 * 1024;
        byte[] data = new byte[_1m];
        // data成为垃圾
        data = null;
        // 调用一次full gc
        System.gc();
    }
}

然后设置参数


一开始没有填写VM参数的地方

点击左上角modify options,然后选择add VM options


就出现了。

之后我们运行demo,就会看到GC日志了


分析GC日志

[GC (System.gc()) [PSYoungGen: 6232K->960K(75776K)] 6232K->968K(249344K), 0.0013575 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 960K->0K(75776K)] [ParOldGen: 8K->747K(173568K)] 968K->747K(249344K), [Metaspace: 3074K->3074K(1056768K)], 0.0061529 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

=

[GC (System.gc())【发生了一次young gc】 [PSYoungGen: 【PS说明使用的收集器是Parallel Scavenage】 6232K->952K(75776K)]【年轻代内存从6232回收成952,总共75776】 6232K->960K(249344K),【这里是堆内存】 0.0675951 secs] [Times: user=0.00 sys=0.00, real=0.08 secs]

[Full GC (System.gc())【发生了一次full gc】 [PSYoungGen: 952K->0K(75776K)] [ParOldGen: 8K->753K(173568K)]【老年代】 960K->753K(249344K), [Metaspace: 3156K->3156K(1056768K)]【元空间,也就是方法区,jdk1.8前的永久代】, 0.0042467 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]


这是系统最后的内存快照:

可以看到eden、from、to区域的大小和使用率等

Heap
 PSYoungGen      total 75776K, used 1951K [0x000000076ba00000, 0x0000000770e80000, 0x00000007c0000000)
  eden space 65024K, 3% used [0x000000076ba00000,0x000000076bbe7c68,0x000000076f980000)
  from space 10752K, 0% used [0x000000076f980000,0x000000076f980000,0x0000000770400000)
  to   space 10752K, 0% used [0x0000000770400000,0x0000000770400000,0x0000000770e80000)
 ParOldGen       total 173568K, used 753K [0x00000006c2e00000, 0x00000006cd780000, 0x000000076ba00000)
  object space 173568K, 0% used [0x00000006c2e00000,0x00000006c2ebc400,0x00000006cd780000)
 Metaspace       used 3176K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 344K, capacity 388K, committed 512K, reserved 1048576K

GC日志分析工具

GC easy

http://gceasy.io/

当然网页可以翻译成中文


GCViewer

github上有,自己下载启动

推荐GCEasy,毕竟在线的嘛,内存能省一点是一点

————————————————

版权声明:本文为CSDN博主「一袋米呦扛几楼」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:

https://blog.csdn.net/qq_31363843/article/details/116136676





锋哥最新SpringCloud分布式电商秒杀课程发布

👇👇👇

👆长按上方微信二维码 2 秒





感谢点赞支持下哈 

浏览 13
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报