java gc回收的是堆内存吗,Java垃圾收集器(Java GC机制)与内存分配回收策略

概述

垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。 提起java的内存回收机制,就要问三个问题

哪些内存需要回收?

什么时候回收?

怎么回收?

Java内存的动态分配和回收技术已经相当成熟。但是当我们需要排查各种内存溢出和泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就有必要学习一下垃圾回收机制。 在Java内存运行时的各个部分中,程序计数器、虚拟机栈、本地方法栈这三个区域随线程生随线程死。栈中的栈帧随着方法的进入和退出有条不紊的进行着出入栈操作。这几个区域是不需要过多的考虑内存回收的问题,因为方法或者线程结束,内存自然就跟着被回收。 然而,Java堆和方法区则不同——一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存可能也不一样。这部分内存的分配是动态的,我们只有在程序运行期间才能知道会创建哪些对象。这部分内存,就是我们关注的重点

回收什么——对象死了吗

在堆里面存放着Java中几乎所有的对象实例,垃圾回收器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些已经死去(不可能再被使用的对象),哪些还存活着。在回收之前我们必须搞清楚哪些才是“垃圾”需要我们进行回收。

引用计数算法

引用计数算法(Reachability Counting)是通过在对象头中分配一个空间作为引用计数器来保存该对象被引用的次数(Reference Count)。每当有一个地方引用它,计数器就加一;当引用失效时,计数器就减一。而计数器为0的对象就是没有任何引用的“垃圾”。 客观的说,引用计数算法的实现简单,判断效率高,在大部分情况下都是一个很不错的办法。但是,它最大的弊端就是很难解决对象之间相互循环引用的问题:

public class GC {

public Object obj;

public static void main() {

GC a = new GC();

GC b = new GC();

a.obj = b;

b.obj = a;

a = null;

b = null;

}

}

复制代码

实际上a和b这两个对象都已经不可能再被访问了,但是他们因为互相引用,导致计数器不为0,于是它们永远不会被引用计数器算法标记为垃圾。

可达性算法

基于无法解决循环引用的问题,主流的Java虚拟机里没有选用引用计数算法来管理内存。在Java的主流实现中,都是通过可达性算法(Reachability Analysis)来判定对象是否存活。 在Java中:

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

方法区中静态变量引用的对象;

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

本地方法栈中JNI引用的对象; 这几种对象可以作为GC Roots的对象。可达性算法以GC Roots对象作为起点,从这些节点开始搜索,其所走过的路径成为引用链,当一个对象到GC Roots没有认可引用链相连时,则说明这个对象不可用,可标注为垃圾。

93e68141fdd0ef0b49b0d01fbe61c1b4.png

tracing gc的基本思路是,以当前存活的对象集为root,遍历出他们(引用)关联的所有对象(Heap中的对象),没有遍历到的对象即为非存活对象,这部分对象可以gc掉。这里的初始存活对象集就是GC Roots。 为什么上述四种对象可以作为GC Roots对象可看Home3k的回答

引用(Java的四种引用)

无论是通过引用计数算法还是可达性算法判断对象是否存活,判定条件都与“引用”有关。最早的Java将引用定义为:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。后来Java对引用的概念进行扩充,将引用分为:

强引用(Strong Reference):指在程序中普遍存在,类似Object obj = new Object()这类的引用,只要引用还存在,垃圾收集永远不会回收掉被引用的对象,宁可产生OOM也不会进行回收。

软引用(Soft Reference): 用来描述一些还有用但是并非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出。(如果一个对象的引用全部为软引用,GC在内存不足时就会将该对象回收——内存不足就回收)

弱引用(Weak Reference):它的强度比软引用更弱一些,被弱引用关联的对象祝能生存到下一次垃圾收集发生之前。当垃圾收集器开始工作,无论当前内存是否够用,都会回收掉只被弱引用关联的对象(一旦被GC发现,就会被回收)。

虚引用(Phantom Reference):也叫做幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来去的一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

如何回收——垃圾收集算法

在确定了“垃圾”是什么——也就是哪些内存需要回收之后,垃圾回收器面临的下一个问题就是——如何进行回收。由于各个平台的虚拟机操作内存的方法各不相同而且涉及大量的程序实现,这里只介绍几种算法的思想。

标记-清除算法

标记清除算法(Mark-Sweep)——首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象(标记过程就是上面讲的对象判定是否死亡)。执行过程如下图:

cefcef80105d183e4c24c0ad640512e6.png

标记清除算法是最基础的收集算法,后续的收集算法都是基于这种思路并对其进行改进而得的。它的不足主要有两个: 效率问题:标记和清除过程的效率不高; 空间问题(碎片化):标记清楚之后会产生大量的不连续的内存碎片,碎片太多可能导致以后在程序在程序运行过程中需要分配大对象时无法找到足够的连续内存。

复制算法

复制算法(Copying)的出现解决了标记清除算法的内存碎片问题。现在的商用虚拟机都是采用这种算法来回收新生代。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对这呢个半区进行回收,即提高了回收的效率,也解决了内存碎片化的问题。复制算法的执行过程如下:

10125fed3258359355c4f1a94234cfcd.png

然而,这种算法的代价是讲内存缩小为原来的一般,代价高到无法接受。幸运的是,研究表明,新生代中的对象98%都是朝生夕死的,所以并不炫耀按照一比一的比例来划分内存空间,而是将内存分为一块较大的Eden区的两个较小的Survivor区,每次使用Eden和其中的一块Survivor。当回收时,将Eden区和Survivor中还存活的对象全部复制到另一块Survivor区,最后清理掉Eden区和刚才用过的Survivor区。

06ccf9011f69231ae26eb8a7f877b0d6.png

如果Survivor区没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。 复制算法的缺点主要有:

对象存活率较高时要进行较多的复制操作,效率变低。

为了应对所有对象都存活的极端情况,需要额外的空间进行分配担保

标记-整理算法

复制算法的缺点使得它只适用于对象存活率较低的新生代。 标记整理算法(Mark-Compact)标记过程仍然与标记清除算法一样,但之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。

4c5b74bb776e6605f16dd115fd6f25fc.png

分代收集

当前商业虚拟机的垃圾收集算法都采用“分代收集”算法。这种算法并没有什么新的思想,只是根据对象存活周期不停将内存划分为几块,这样就可以根据各个年代的特点采用最适当的收集算法。 新生代(Young Generation):在新生代中,因为大量对象的声明周期都很短,每次回收垃圾时都有大批对象以及死去,只有少量存活,这里的GC采用复制算法,只需付出复制少量存活对象的成本就能完成GC。这个GC机制被称为Minor GC或叫Young GC。 老年代(Old Generation):老年代中存放的对象存活率高,使用复制算法不仅效率低下而且极度浪费内存空间。这里的GC一般使用标记清理或者标记整理算法。这里的GC叫做Full GC或者Major GC。   永久代(Permanent Generation):永久代中的对象生成后几乎是永生的,回收的东西有两种:常量池中的常量,无用的类信息。

内存分配与回收策略

对象的内存分配,往大了讲就是在堆上的分配。接下来我们学习几条普遍存在的内存分配规则

优先在Eden区分配:大多数情况下,最想主要在新生代Eden区中分配。当Eden区没有足够的控件进行分配时,虚拟机将发起一次Minor GC

大对象直接进入老年代:所谓大对象,需要大量连续内存空间的Java对象,最典型的是很长的字符串以及数组(比遇到一个大对象更惨的是遇到一群短命的大对象,这会导致内存抖动)。

长期存活的对象进入老年代:虚拟机给每个对象定义了一个年龄计数器。如果对象在Eden出生并再经理过一次Minor GC之后仍然存活并被Survivor容纳的话,它的年龄会加一。对象每经历过一次GC,年龄就加一,等增加到一定岁数(默认15),就将会被晋升到老年代。

动态年龄判定:为了能更好的试用不同程序的内存状况,虚拟机并不是永远的要求对象的年龄达到阈值才能晋升老年代,若果在Survivor空间中相同年龄所有对象大小的和总是大于Survivor控件的一般,年龄大于或者等于该年龄的对象就可以直接进入老年代。

空间分配担保:在发生Minor GC之前,虚拟机会检查老年代最大可用的连续控件是否大于新生代所有对象总空间,如果是,那么可以认为Minor GC是安全的。如果不成立,虚拟机会查看是否设置了允许失败担保。如果允许,就会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC。如果小于,或者设置了不允许冒险,则进行一次Full GC。这里的冒险中的风险,前面提到过新生代为了提高内存利用率,只使用其中一个Survivor作为轮换备份。因此当出现大量对象在Minor GC之后依然存活的情况下,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入到老年代。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的空间。然而有多少对象会活下来是在内存回收完成之前是无法预测的,所以只好取之前每一次晋升到老年代对象的平均大小作为参考值,与老年代的剩余空间进行比较,决定是否需要进行Full GC已变腾出更多的空间——而这显然是存在风险的。

结语

到这里GC的基本概念已经讲完,更详细的内容请持续关注我的博客

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值