垃圾回收与内存分配

垃圾回收机制

java的垃圾回收机制用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。回收的是无任何引用的对象占据的内存空间而不是对象本身。GC只知道释放由new分配的内存。java允许在类中定义finalize的方法,它的工作原理是一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用该方法,并且下一次垃圾回收动作发生时才会真正回收对象占用的内存,C++中对象一定会被销毁,但是Java里的对象并非总是被回收。

一、如何判断对象已经死去(不可能再被任何途径使用的对象)?

1、引用计数算法
给对象添加一个引用计数器,当有一个地方引用它,计数器值加1;引用失效,计数值减1。计数器为0的对象不能被使用。

优点:实现简单,判定效率高,可以很快执行,交织在程序运行中,对程序不被长时间打断的实时环境有利。

缺点:主流Java虚拟机没有选用引用计数算法来管理内存,最主要原因是它无法检测出循环引用。

//循环引用的例子
public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();

        object1.object = object2;
        object2.object = object1;

        object1 = null;
        object2 = null;
    }
}

class MyObject{
    public Object object = null;
}

2、可达性分析算法

基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,此对象不可用。

特点:比较慢,但是没有交互自引用的对象组的问题。

GC Roots的对象包括:

1)虚拟机栈栈帧中局部变量表中引用的对象。
2)本地方法栈中JNI(即一般说的Native方法)引用的对象。
3)方法区中类静态属性引用的对象。
4)方法区中常量引用的对象。

二、垃圾收集算法

1、标记—清除mark-sweep

首先标记出所有需要回收的对象,在标记完成后通知回收所有被标记的对象。
这里写图片描述
它的主要不足有两个:

1)效率问题,标记和清除两个过程的效率都不高;

2)空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2、复制copying

为了解决效率问题,copying算法出现。它实现简单,运行高效且不容易产生内存碎片,但是对内存空间使用代价较高,因为能够使用的内存是原来的一半。copying的算法效率和存活对象的数目有关,存活对象很多,算法效率就会大大降低。现在的商业虚拟机都采用这种收集算法来回收新生代。

HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被浪费。当超过10%的对象存活时,Survivor空间不够用,需要依赖其他内存(老年代)进行分配担保。
这里写图片描述

3、标记—整理mark-compact

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要额外的空间进行担保,以应对使用的内存中所有对象100%存活的极端情况,所以老年代一般采用标记-整理算法。完成标记后,不是直接清理,而是将存活对象向一端移动,然后清理端边界以外的内存。
这里写图片描述

4、分代回收Generational Collection

目前大部分JVM的GC采用的算法。核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域,一般将堆分为老年代和新生代,老年代每次只有少量对象被回收,新生代有大量对象被回收,可以根据不同代的特点选择最适合的收集算法。通常新生代用copying的算法,因为复制的操作次数较少,新生代分为一个较大的Eden空间和两个较小的Survivor区间,每次复制到一个Survivor空间。当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。老年代通常使用Mark-compact算法。

内存分配策略

自动内存管理包括两个问题:给对象分配内存以及回收分配给对象的内存。

对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是固定,取决于使用哪种垃圾收集器,还有虚拟机中与内存相关的参数的设置。

Minor GC和Major GC
  1. Minor GC(新生代GC):指发生在新生代的垃圾收集动作。因为Java对象大多具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  2. Major GC/Full GC:发生在老年代的GC,出现了Major GC,经常伴随至少一次的Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。
一、对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

    //GC时打印内存回收日志,并且在进程退出的时候输出内存区域分配情况
    -XX:+PrintGCDetails
二、 大对象直接进入老年代。

大对象指的是需要大量连续内存空间的Java对象,典型的是那种很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来安置它们。

    //大于设置值的对象直接在老年代分配,该参数只对部分收集器有效(Serial、ParNew)
    -XX:PretenureSizeThreshold

这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

三、长期存活的对象将进入老年代

每个对象定义一个对象年龄计数器,对象在survivor区中熬过一次Minor GC,年龄增加1。当它的年龄增加到一定程度(默认15岁),就会被晋升到老年代中。

    //年龄设置
    -XX:MaxTenuringThreshold=15
四、动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

五、空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的,如果小于,或者HandlePromotionFailure设置不允许冒险,这时就要改为Full GC。

取平均值进行比较其实仍然是一种动态概率的手段。如果某次Minor GC存活后的对象突增,远远高于平均值的话,会导致担保失败。如果出现了担保失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值