畅谈GC垃圾回收

  • 什么是垃圾回收
    1. 垃圾回收(Garbage Collection,GC),所谓的垃圾回收就是释放占用内存的垃圾,防止内存泄露溢出,对JVM内存堆中已经死亡的对象或者没有使用的对象进行回收,释放内存
    2. 在java 语言出现之前,大多数人员都使用的是C或者C++,大家都知道C和C++的对象要不断的去开辟内存空间,不用的时候还需要自己用代码释放内存,代码量十分庞大,这时候java语言就出现了,自带内存垃圾回收
  • 如何定义垃圾
    首先我们要搞清楚什么是垃圾,才能有效的做到垃圾回收,那些垃圾需要被回收
    • 引用计数算法
      引用计数算法,是通过在对象头中分配一个空间来记录某些对象被引用的次数(Reference count),如果该对象被其他对象引用,则他的引用计数加1,如果删除了该对象的引用则该对象的引用计数减1,当该对象的引用计数为0的时候,则改对象就会被回收
      String m = new String(“jack”);
      先创建一个字符串,这时候 “jack”就有了一个引用,就是m
      在这里插入图片描述
      然后我们将m设置为null,这时候“jack”的引用次数就等于0了,在引用计数算法中,意味着这块内容需要被回收了
      m =null;
      在这里插入图片描述
      引用计数器算法是将垃圾回收分摊到整个程序应用当中了,而不是在进行垃圾回收的时候挂起整个应用的运行,直到对堆内的所有对象处理完毕都结束后在释放应用。因此采用引用计数算法垃圾收集器不属于严格上的"Stop-The-Word"的垃圾收集机制。

我们都知道现在JVM垃圾回收就是"Stop-The-Word",那是什么原因导致JVM使用"Stop-The-Word"而放弃引用计数算法呢?

public class ReferenceCountingGC {

public Object instance;

public ReferenceCountingGC(String name){}
}

public static void testGC(){

ReferenceCountingGC a = new ReferenceCountingGC("objA");
ReferenceCountingGC b = new ReferenceCountingGC("objB");
// a 引用b ,b 引用a 相互引用
a.instance = b;
b.instance = a;

a = null;
b = null;
}

1.相互引用
2.置空各自的声明引用

计数在这里插入图片描述
我们可以看到,及时二个对象都设置成了null,但是在内存当中他们的互相引用(计数器在堆内存当中计算,及时设置null,计数器还是会运算)还是存在的,计数器永远不可能变成0,这样就会引起GC垃圾收集器永远也无法回收他们,所以在后来的JVM当中就放弃了引用计数算法收集器。

  • 可达性分析算法
    可达性分析算法的基本思路是,通过一些被称之为引用链(GC Roots)的对象作为起点,从这些对象开始向下搜索,搜索走过的路径被称之为(Reference Chain) ,当一个对象到GC Roots没有任何引用链的时候(即从Gc Roots节点到该节点不可达),则证明该对象不可达
    在这里插入图片描述
    通过可达性分析可以解决引用计数算法相互引用的问题,只要你于GC ROots无法建立引用连接,则垃圾收集器可以判定你为可回收的对象,那就延伸另一个问题,那些对象可以是GC Roots 对象?

Java 内存区域

在java 语言中,以下可作为GC Roots的对象:

  • 虚拟机栈(栈帧中的本地变量表栈帧用于支持虚拟机进行方法调用和方法执行的数据结构 虚拟机栈中引用的对象则可以理解为 某个方法里所有对象的引用 GC Roots 分布最多的地方)中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈JNI(一般是Native 的方法 (外部方法 可以是其他语言的方法) )引用的对象
  • PS:实例变量等信息是直接在堆中分配的不作为GC Root对象

在这里插入图片描述
1.虚拟机栈(栈帧中的本地变量表 又叫 局部变量区)中引用的对象
此时s,即为GC Root ,当S置空的时候,localocaParamter对象也断了GC Root的引用链(Reference Chain) ,即将会被回收

public class StackLocalParameter {
public StackLocalParameter(String name){}
}

public static void testGC(){
StackLocalParameter s = new StackLocalParameter("localParameter");
//localParameter 字符串也将会被回收
s = null;
}

2.方法区中静态引用的对象
此时s 为Gc Root,如果s置空,经过GC后,s所指向的properties对象将无法与GC Root连接,即不可达对象,即将被GC 回收

而m对象作为静态属性,是属于类的,也是GC root 对象,parameter仍然通过m 与GCRoot 对象关联,所以不会被垃圾回收

public class MethodAreaStaicProperties {
public static MethodAreaStaicProperties m;
public MethodAreaStaicProperties(String name){}
}

public static void testGC(){
MethodAreaStaicProperties s = new MethodAreaStaicProperties("properties");
s.m = new MethodAreaStaicProperties("parameter");
s = null;
}

3.方法区中的常量变量
m为方法区中常量引用,也是GC Root,当s置空的时候,final字符串对象仍然于GC Root 建立联系,所以不会被回收

public class FinalProperties {
public static final FinalProperties m = FinalProperties ("final");
public FinalProperties (String name){}
}

public static void testGC(){
FinalProperties s = new FinalProperties("staticProperties");
s = null;
}

4.本地方法栈中的引用对象
任何native 接口都会使用某种本地方法栈(JDK8 将本地方法栈和虚拟机栈合并了),实现的方法接口使用C语言连接的话,那么本地方法栈就是C栈,当线程调用java方法的时候,虚拟机会创建一个栈帧并且压入虚拟机栈,然而当他调用本地方法的时候,虚拟机会保持栈不变,不在创建新的栈帧压入栈内,虚拟机只是简单的动态链接并直接调用本地方法(PS:疑问 HotSpot 虚拟机 本地方法栈和虚拟机栈 合并后 线程调用 外部方法的时候 会不会创建栈帧 入到虚拟机栈里面 )
在这里插入图片描述

  • 怎么回收垃圾
    在确定哪些垃圾可以被回收的时候,垃圾收集器要做到的事情就是对这些垃圾进行回收,所以就延伸出来一个新的问题: 如何高速高效的对这些垃圾进行回收。由于java虚拟机规范并没有具体声明如何具体实现垃圾收集器的实现的明确规则,因此各个厂商的虚拟机可以采用不同的垃圾收集器,这里我们探讨几个常见的垃圾收集器的核心思想。

    • 标记 — 清除算法
      在这里插入图片描述
      标记清除算法(Mark-Sweep)是最基础的一种垃圾回收算法,它分为2部分,先把内存区域的对象进行标记,标记出那些对象属于可回收对象,然后将他标记起来,像上图一样,被标记的对象将会被回收,回收后的内存区域就变成了未使用状态,等待再次被使用。

这种算法很简单,而且很好操作,但是会引起一个问题 : 内存碎片

上图中等方块假设是2M,小的是1M,大一些的是4M,等我们回收完了后,内存可能就会被分割成很多块(内存的数据是连续的),我们在使用内存的时候,需要连续的内存区域,假设我们这时候需要存放一个,2M的内存数据,这时候2的1M 就无法使用了,这样就会导致其实我们内存还有很多空间,但是我们无法使用

  • 复制算法
    在这里插入图片描述
    复制算法(Copying)是标记算法上演变过来的,解决了标记算法产生的内存碎片的问题,它是可以根据内存划分成二块(from 区域和To 区域),每次使用只使用其中的一块(假设是from区),当其中的某一块内存不足的时候就开始执行GC操作,这时候会将from区存活的对象放在To 区,然后把From区的空间一次性清理掉,保证了内存连续可用,这样内存分配的时候就不需要考虑内存碎片的情况了,逻辑清晰,运行高效。

    PS:上图完美的暴露了一个问题 内存分配代价过大,好比 我有200平方的房子,我只能用100平的道理

  • 标记整理算法
    标记整理算法(Mark-Compact) 标记过程仍然于标记清除算法一样,但是后续操作不是直接将标记的对象进行删除,而是将存活的对象向另一端移动,将要删除的对象向反方向移动,在清理掉删除的对象。

    标记整理算法一方面在标记清除算法上做了改善,解决了内存碎片的问题,也避免了复制算法只能使用一半空间的弊端,看起来十分完美,但是从上图可看出来,标记整理算法操作内存过于频繁,需要整理所有存活对象的内存地址,在效率上比复制算法还要差很多。

  • 分代收集算法(Generational Collection)
    分代收集算法严格上来讲并不是一种思想或者理论,而是融合了以上3中的基础算法思想,而产生的针对不同情况采用不同的算法的一套收集算法。根据对象存活周期的不同将内存分为几块模块,一般Java 堆分为新生代和老年代,这样可以根据各个年代不同的特点采用不同的收集算法,在新生代当中,大部分对象都是朝生夕死的,每次垃圾收集的时候都会发现大量的对象死亡,只有少量的对象存活,这时候就适合使用复制收集算法,只需要付出少量的存活对象的复制成本就可以完成收集,而老年代中的对象存活率比较高,没有额外的空间对它进行分配担保,就必须使用标记清除算法或者标记整理算法进行回收,所以Java吧堆内的内存区域分为以下几块,每一快又使用了不同的收集算法

内存模型与回收策略
在这里插入图片描述
Java 堆(Java Heap) 是JVM所管理的内存最大的一块,堆也是垃圾收集器最重要的区域,这里我们首先来分析一下Java 的堆结构。

Java 堆主要分为2个区域-年轻代和年老代,其中年轻代又分为Eden区和Survivor区,Survivor区又分为From区和To区。

Eden区
IBM公司专业研究表明,有将近百分之98的对象都是朝生夕死的,所以针对这中状况,大多数情况下,对象都会在Eden区快速分配,当Eden区空间不足的时候,没有足够的空间分配对象的时候,就会触发Minor GC ,Minor GC 比Major GC(年老代) 执行更加频繁,回收速度也非常快。
通过Minor GC后,Eden区中的大多数对象都会被回收,而那些存活的对象就会被移入到Survivor区中的From区(如果From区空间不够,则直接将对象移入到Old 区)

Survivor
Survivor区相当于年轻代和年老代中的一个缓冲,Survivor区又分为From区和To区,每次执行MinorGC的时候,都会将Eden区清空,将存活的对象移入From区,再一次的时候将Eden区和From区中存活的对象移入To区,如果To区空间不足则将对象移入到Old区,以此相互移动。

1.为什么需要Eden和Survivor二个区?
不就是新生代和老年代嘛,直接将Eden区存活的对象移入Old区不就行了嘛?为什么还要这么麻烦分这么多区域呢,可以想象如果说Eden区没有Survivor区,Eden区每一次进行MinorGC的时候,存在的对象直接移入Old区,年老代的空间可能很快就被填满了,而且很多对象可能能经过几次MinorGc后就被移除了,如果这时候将对象放入Old区,可能回收的代价就很大了,Old区的垃圾回收代价要比年轻代的垃圾回收代价要大很多。

2.为什么Survivor还要分二个区域?

设置Survivor区的From区和To区主要就是为了解决内存碎片的问题

假设,Survivor只有一个区域,那么当经过MinorGC的时候Eden区迅速被清空,讲存活的对象放入Survivor区,可是Survivor区中的对象可能也需要清除,那我们怎么清除呢?只能通过标记清除,那样就会造成大量的内存碎片,在年轻代区域如果产生大量严重的内存碎片,可能大量的数据就直接被移入Old区了,Old区很快就被填满了,所以Survivor区设定二个区域From区和To区,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。

这种机制最大的好处就是,整个过程中永远都有一个Survivor Space区域是空的,另一个Survivor Space 是无碎片的。那么Survivor 为什么不多分几个区?比如说三个,四个,五个?年轻代原本占用内存就少 如果Survivor区继续细分下去,Survivor 区域很快就被填满了,Survivor 区域太容易被填满了,这样对象又快速的存入了Old区,所以二个Survivor区是最好的方案。

Old区

老年代占据2/3的堆内存空间,只有Major GC的时候才会进行清理,每次处罚Major GC的时候,JVM将挂起整个项目"Stop-The-Word"。内存越大 Stop-The-Word 的时间就越长,所以内存也不仅仅是越大越好,由于复制算法在对象存活率比较高的年老代当中会进行很多次复制操作,所以年老代采用的算法是 标记整理算法

Old 区存放那些对象?

  • 大对象
    大对象指的是大量的连续内存空间的对象,这些对象不是朝生夕死的,都会直接写入Old区,这种做法主要是为了避免Eden区于二个Survivor区发生大量的内存复制PS:当你的系统有非常多的 朝生夕死的大对象的时候,就要注意了。

  • 长期存活对象
    虚拟机给每个对象定义了年龄的计数器,正常情况下对象会在Eden区和Survivor 的From区和To区不断的移动,对象在Survivor区中每经历一次MinorGC,年龄就会增加1岁,当年龄增加到一定年龄的时候(16岁),这时候该对象就会被移入年老代,JVM可以对年龄进行参数设置

  • 动态对象年龄
    虚拟机并不重视所有对象年龄必须到达16岁,才放入年老代,如果Survivor区中相同年龄的所有对象加起来大小综合大于Survivor空间的一半,年龄大于该年龄的对象可以直接移入到年老代,无需等到“成年”

这其实有点类似负载均衡,轮询是负载均衡的一种,保证每台机器都分得同样的请求,看起来很均衡,但每台机器硬件不同,健康状况也不同,我们还可以根据每台机器接受的请求数或者每台机器的响应时间等等来调整我们的负载均衡算法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值