欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

老生常谈Java虚拟机垃圾回收机制(必看篇)

程序员文章站 2024-02-20 17:34:10
在java虚拟机中,对象和数组的内存都是在堆中分配的,垃圾收集器主要回收的内存就是再堆内存中。如果在java程序运行过程中,动态创建的对象或者数组没有及时得到回收,持续积累...

在java虚拟机中,对象和数组的内存都是在堆中分配的,垃圾收集器主要回收的内存就是再堆内存中。如果在java程序运行过程中,动态创建的对象或者数组没有及时得到回收,持续积累,最终堆内存就会被占满,导致oom。

jvm提供了一种垃圾回收机制,简称gc机制。通过gc机制,能够在运行过程中将堆中的垃圾对象不断回收,从而保证程序的正常运行。

垃圾对象的判定

我们都知道,所谓“垃圾”对象,就是指我们在程序的运行过程中不再有用的对象,即不再存活的对象。那么怎么来判断堆中的对象是“垃圾”、不再存活的对象呢?

引用计数法

每个对象都有一个引用计数的属性,用来保存该对象被引用的次数。当引用次数为0时,就意味着该对象没有被引用了,也就不会在使用这个对象了,可以判定为垃圾对象。但是,这种方式有一个很大的bug,就是无法解决对象间相互引用或者循环引用的问题:当两个对象相互引用,他们两个和其他任何对象也没有引用关系,它俩的引用次数都不为0,因此不会被回收,但实际上这两个对象已经不再有用了。

可达性分析(根搜索法)

为了避免使用引用计数法带来的问题,java采用了可达性分析法来判断垃圾对象。

这种方式可以将所有对象的引用关系想象成一棵树,从树的根节点gc root遍历所有引用的对象,树的节点就为可达对象,其他没有处于节点的对象则为不可达对象。

老生常谈Java虚拟机垃圾回收机制(必看篇)

那么什么样的对象可以作为gc的根节点呢?

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

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

方法区中常量引用的对象

本地方法栈中jni引用的对象

引用状态

垃圾回收机制,不管采用是引用计数法,还是可达性分析法,都与对象的引用有关,java中存在四种引用状态:

强引用 - 我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,就表示它处于可达状态,垃圾回收器绝不会回收它,即便系统内存非常紧张,java虚拟机宁愿抛出 outofmemoryerror 错误,使程序异常终止,也不会回收被强引用所引用的对象。因此,强引用是造成java内存泄露的主要原因之一。

软引用 - 一个对象只具有软引用,如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

弱引用 - 一个对象只具有弱引用,那就类似于是可有可无的。弱引用和软引用很像,但弱引用的引用级别更低。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

虚引用 - 一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动,我们平常一般不会使用。

垃圾回收算法

通过可达性分析算法能够判定哪些对象是需要回收的了,那么回收具体需要怎样去执行呢?

标记-清除算法

首先需要标记可以回收的对象内存,然后在对回收的内存进行清除。

老生常谈Java虚拟机垃圾回收机制(必看篇)

标记-清除算法(回收前)

老生常谈Java虚拟机垃圾回收机制(必看篇)

标记-清除算法(回收后)

但是这样的话,随着程序的运行,会不断分配释放内存,在堆中会产生很多的不连续的空闲内存区,即内存碎片。这样即使有足够多的空闲内存,也不一定能分配出足够大的内存,并且可能会造成频繁的gc,影响效率,甚至oom。

标记-整理算法

和标记-清除算法不同的是,标记-整理算法在标记后不直接清理可回收内存,而是将存活对象都移动到一端,然后清除掉可回收内存。

老生常谈Java虚拟机垃圾回收机制(必看篇)

标记-整理算法(回收前)

老生常谈Java虚拟机垃圾回收机制(必看篇)

标记-整理算法(回收后)

这样做的好处就是不会产生内存碎片。

复制算法

复制算法需要先将内存分为两块,先在其中一块内存上分配内存,当这块内存被分配完后,则执行垃圾回收,然后把存活对象全部复制到另一块内存上,第一块内存则全部清空。

老生常谈Java虚拟机垃圾回收机制(必看篇)

复制算法(回收前)

老生常谈Java虚拟机垃圾回收机制(必看篇)

复制算法(回收后)

这种算法不会产生内存碎片,但是相当于只能使用一半的内存空间。同时,复制算法和存活对象的数量有关,如果存活对象的数量多,那么复制算法的效率会大大降低。

分代收集算法

在java虚拟机中,对象的生命周期有长有短,大部分对象的生命周期很短,只有少部分的对象才会在内存中存留较长时间,因此可以依据对象生命周期的长短将它们放在不同的区域。在采用分代收集算法的java虚拟机堆中,一般分为三个区域,用来分别储存这三类对象:

新生代 - 刚创建的对象,在代码运行时一般都会持续不断地创建新的对象,这些新创建的对象有很多是局部变量,很快就会变成垃圾对象。这些对象被放在一块称为新生代的内存区域。新生代的特点是垃圾对象多,存活对象少。

老年代 - 一些对象很早被创建了,经历了多次gc也没有被回收,而是一直存活下来。这些对象被放在一块称为老年代的区域。老年代的特点是存活对象多,垃圾对象少。

永久代 - 一些伴随虚拟机生命周期永久存在的对象,比如一些静态对象,常量等。这些对象被放在一块称为永久代的区域。永久代的特点是这些对象一般不需要垃圾回收,会在虚拟机运行过程中一直存活。(在java1.7之前,方法区中存储的是永久代对象,java1.7方法区的永久代对象移到了堆中,而在java1.8永久代已经从堆中移除了,这块内存给了元空间。)

分代收集算法也就根据新生代和老年代来进行垃圾回收的。

对于新生代区域,每次gc都会有很多垃圾对象被回收,只有少量存活。因此采用复制回收算法,gc时把剩余很少的存活对象复制过去即可。

在新生代区域中,并不是按照1:1的比例来进行复制回收,而是按照8:1:1的比例分为了eden、survivora、survivorb三个区域。其中eden意为伊甸园,形容有很多新生对象在里面创建;survivor区则为幸存者,即经历gc后仍然存活下来的对象。

eden区对外提供堆内存。当eden区快要满了,则进行minor gc(新生代gc),把存活对象放入survivora区,清空eden区;

eden区被清空后,继续对外提供堆内存;

当eden区再次被填满,此时对eden区和survivora区同时进行minor gc(新生代gc),把存活对象放入survivorb区,此时同时清空eden区和survivora区;

eden区继续对外提供堆内存,并重复上述过程,即在 eden 区填满后,把eden区和某个survivor区的存活对象放到另一个survivor区;

当某个survivor区被填满,且仍有对象未被复制完毕时,或者某些对象在反复survive 15次左右时,则把这部分剩余对象放到老年代区域;当老年区也被填满时,进行major gc(老年代gc),对老年代区域进行垃圾回收。

老年代区域对象一般存活周期较长,每次gc时,存活的对象比较多,因此采用标记-整理算法,gc时移动少量存活对象,不会产生内存碎片。

触发gc的类型

java虚拟机会把每次触发gc的信息打印出来,可以根据日志来分析触发gc的原因。

gc_for_malloc:表示是在堆上分配对象时内存不足触发的gc。

gc_concurrent:当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发gc操作来释放内存。

gc_exppcit:表示是应用程序调用system.gc、vmruntime.gc接口或者收到sigusr1信号时触发的gc。

gc_before_oom:表示是在准备抛oom异常之前进行的最后努力而触发的gc。

以上这篇老生常谈java虚拟机垃圾回收机制(必看篇)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。