|
|
51CTO旗下网站
|
|
移动端
创建专栏

Java中GC原理及GC日志剖析

学习Java的我们都知道垃圾收集(GC),大部分人把这项技术当作是Java语言的伴生产物。事实上,GC的历史比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。那我们今天就研究下垃圾收集原理。

作者:赵亮|2019-11-27 14:41

 一.概述

学习Java的我们都知道垃圾收集(GC),大部分人把这项技术当作是Java语言的伴生产物。事实上,GC的历史比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。那我们今天就研究下垃圾收集原理。

二.对象已死吗?

Java的垃圾回收主要是对堆内存的回收,里面存放着Java几乎所有的对象实例,垃圾回收之前是要确定哪些还“存活”,哪些已经“死去”。

1.引用计数器法

给对象添加一个引用计数器,每当有地方对他进行引用时计数器值➕1;当引用失效时,计数器值就➖1,任何时候计数器值为0的时候表示对象不可能在使用的。

2.可达性分析算法

通过一系列称为“GC Roots”的对象作为起点,从这些节点往下搜索,搜索所走过的路径称为“引用链”,当一个对象到“GC Roots”没有任何引用链相连时,则证明对象是不可用的。

代码示例:

GC日志如下:

我们很明显的看到GC日志中6092K->456K,意味着虚拟机并没有因为这两个对象互相引用而不回收他们,所以Java虚拟机使用的是可达性分析算法标记的。

其实即使被可达性分析算法标记的不可达对象也不是一定会被回收的,虚拟机会对这些对象进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”。如果对象被判定有必要执行,finalize()方法是并在finalize()中与“GC Roots”建立关联,则此对象不会被回收了。

三.垃圾回收算法

我们知道了虚拟机怎么标记一个对象是否可用,那他怎么进行回收的呢?其实堆内存可以分为新生代和老年代,新生代又被划分为一个Eden和两个Survivor区域,他们的比例为8:1:1,不同的垃圾收集器厂商对这两个区域给出了不同的算法。

1.新生代——复制算法

新生代对象的特点就是,大部分对象在一次GC中会被回收掉,所以使用的是复制算法:新生代每次创建对象的时候只会使用一个Eden和其中的一块Survivor,在垃圾回收时将存活的对象复制到另外一块Survivor区域,最后清理掉Eden和刚才的Survivor区域。

2.老年代——标记-整理算法

老年代一般保存的是一些大对象,或者不被经常回收的对象,根据特点使用的标记-整理算法:如同名字一样,算法分为“标记”和“整理”两个阶段:首先先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象进行整理,将被标记的对象都向一端移动,然后直接清理掉边界以外的内存。

四.HotSpot算法实现

上面说了我们怎么标记对象“死亡”和怎么进行垃圾回收的,但在HotSpot虚拟机在实现这些算法上是必须对算法的执行效率进行考量的。

1.安全点

在可达性分析中对执行的时间的敏感体现在GC停顿上,其意思是在整个分析的过程中看起来就像被冻结在某一个时间点上的,不可以出现分析的过程中引用关系在不断变化,如果这点得不到保证则分析的结果的准确性就得不到保障。这点是导致在GC进行时需要停顿所有的Java执行线程。

当执行系统停顿下来后,虚拟机并不需要全部上下文和全局所有的位置,虚拟机通过一个OopMap的数据结构在类加载的时候将对象的偏移量数据信息记录下来,所以GC扫描是直接得到这些信息的。其实这些通过指令被加入进行记载对象信息的OopMap位置也叫做安全点,程序执行时并非所有点都可以停下来开始GC的,只有在到达安全点才能停顿。安全点机制程序执行中,在不太长的时间内会遇到可进入GC的安全点。在实际中会遇到在GC时有线程不再执行,例如线程被挂起了。这是我们需要安全区域去解决。

2.安全区域

安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。在代码执行到安全区域时,首先表示这直接进去安全区域,这样虚拟机在这段时间GC时就不用管那些标记为安全区域的线程了。当离开安全区域时首先得判断GC分析是否完成,没完成则需要等待。

五.理解GC日志

这是上图打印的GC日志

[GC (System.gc()) [PSYoungGen: 6092K->448K(38400K)] 6092K->456K(125952K), 0.0051702 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

其中,PSYoungGen表示的是新生代GC不同垃圾收集器新生代名称不一样,6092K->448K(38400K)表示新生代大小的变化,6092K->456K(125952K)表示堆内存的大小变化,后面表示用时。

[Full GC (System.gc()) [PSYoungGen: 448K->0K(38400K)] [ParOldGen: 8K->378K(87552K)] 456K->378K(125952K), [Metaspace: 3050K->3050K(1056768K)], 0.0056045 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

这里表示发生在老年代的GC(Major GC/Full GC) 它只是为伴随一次的新生代的GC(Minor GC),448K->0K(38400K)表示新生代内存变化,8K->378K(87552K) 表示老年代GC变化,456K->378K(125952K)表示GC前后堆内存的变化。

【本文是51CTO专栏机构“AiChinaTech”的原创文章,微信公众号( id: tech-AI)”】

戳这里,看该作者更多好文

【编辑推荐】

  1. 我是世界上的编程语言,100斤!
  2. Java:异常作为控制流?大佬:避免!避免!避免
  3. 什么技能产品经理不会提,但技术人必须懂?
  4. 跑得好好的Java进程,怎么突然就瘫痪了?
  5. 永别了,Java的“小苹果”!
【责任编辑:华轩 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢