垃圾回收器及内存分配策略

内存系列相关文章

java的内存区域

垃圾回收器及内存分配策略

垃圾回收器

内存分配与回收策略

  • 概述
  • 如何判断对象的生死
  • 垃圾回收算法
  1. 概述

人们大多数会把垃圾回收器看作是java语言的半生产物,实际上GC的历史远比java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。GC需要完成的三件事:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 怎么回收?

其中程序计数器,虚拟机栈,本地方法栈三个区域是随xian线程而生随线程而亡的;栈中的栈帧是随着方法的进入和退出而执行入栈和出栈的。每个栈帧中分配的内存在类结构确定下来时就是已知的,因此这几个区域的内存分配和回收都是确定的,方法结束和线程结束时,内存自然就回收了。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序运行期间才能知道要创建哪些对象,这部分内存分配和回收是动态的,也就是说这部分内存的回收是要干预的。

2. 如何判断对象的生死

  • 引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数值就加1;当引用失效时计数器值就减1;任何时刻当一个对象的计数器值为0时就是不再被使用的,即就是要被回收的。

这种算法,实现简单,判定效率也很高,在多数情况下它是一个不错的算法,但是在java语言中没有选取这种方法来管理内存,因为它很能解决对象之间互相循环引用的问题:比如对象Ahe'd和对象B都有字段instance,令A.instance = B及B.instance = A,除此之外这两个对象再无其他任何引用。实际上这两个对象是要被回收的对象,但是他们之间存在着互相引用,导致计数器的值不为0,引用计数算法就不能回收他们(回收条件计数器值为0)。

  • 根搜索算法

java使用的是根搜素算法判断对象的存活,通过“GC Roots”的对象作为起始点,从这个起始点向下搜索,搜索所走过的路径成为引用链,当一个对象没有与任何引用链相连(即从GC Roots不可达),此时说明这个对象是不可用的。

在java语言中,可作为GC Roots的对象包含以下几种

  • 虚拟机栈(栈中的本地变量表)中的引用对象
  • 方法区中类的静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈中JNI的引用的对象

在实际中我们希望能有这样的对象:当内存空间足够时保存在内存中,当内存jin'紧张时,则可以抛弃这些对象。在JDK1.2之后,java将引用的概念进行了扩充,将引用分为:强引用,软引用,弱引用,虚引用四种,这四种的引用强度依次逐渐减弱。

  • 强引用在代码中普遍存在,如Object obj = new Object() 这样的引用就是强引用,只要这个对象的引用还存在,垃圾回收器就永远不会回收它。(在通常对静态属性赋值时一定要各位注意,它的生命周期会贯穿整个app的生命周期
  • 软引用用来描述一些还有用,但是并非必须的对象,在系统即将发生内存溢出之前,会将这些对象进行回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用是用来描述非必须对象的,他比软引用更更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收收集之前。当GC时无论当前内存是否足够,都会回收掉只被弱引用关联的对象(注意是只被弱引用关联的对象,如果一个对象即被强引用引用也被弱引用引用,GC时是不会回收的,只有强引用取后下一次GC时才会回收掉
  • 虚引用也称为幽灵引用或幻影引用,它是最弱的一种引用关系。一个对象是否有有虚引用,完全不会影响它的生存周期周期,也无法通过一个需引用获得一个对象。为一个对象设置为虚引用关联的唯一目的就是希望能在这个对象被回收时收到一个系统通知。

对象的回收

在根搜索算法中不可达的对象,也并非是非死不可的,这个时候它是处于暂时的缓刑阶段,要一个对象死亡,至少要经历两次标记过程。如果对象在进行跟搜索后发现没有与GC Roots相连接的引用,那它就会被标记一次并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果这个对象被判定为需要执行finalize()方法,则这个对象将被放置到一个名为F-Queue的队列中,并在稍后由一条虚拟机自动建立的,低优先级的Finalizer线程去执行(调用finalize()方法),但并不承诺会等待它运行结束(原因是:如果一个对象的finalize()方法执行缓慢或者发生死循环等极端情况,将会导致F-Queue队列永久处于等待zhua状态,甚至导致整个GC系统的崩溃)。finalize()方法是对象逃离死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()中成功拯救了自己(重新与引用链建立关联),那在第二次标记时它将被移除出即将回收的集合;如果这时它还没有逃脱,那么它就离死不远了。这里的需要注意的是:finalize()方法在一个对象中这能被虚拟机自动执行一次,并且并不保证一定能执行完。(在java中不建议使用finalize()方法)

回收方法区

在方法区进行垃圾回收的性价比比较低。在堆中,尤其是在新生代中,常规的应用进行一次垃圾回收一般能回收70%~95%的空间,而永久代代的垃圾回收效率远低于此。

永久代的垃圾回收主要回收两部分的内容:废弃常量与无用类。回收废弃常量与与回收java堆中的对象非常类似。如:有一个字符串字符串“abc”已经进入到了常量池中,但是当前系统没有任何一个String的对象是叫做“abc”的,换句话说就是没有任何String对象引用常量池中的“abc”常量,如果这个时候发生GC,如果有必要的话这个对象会被清理掉。常量池中的其他类(接口),方法,字段的符号引用也类似。

判断一个常量是否废弃是比较简单的,而要判断一个类是否是无用类的条件比较苛刻,需满足以下3个条件:

  • 该对象的所有实例都已经被回收
  • 加载该类的ClassLoader也已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,也无法在任何地方通过反射来访问该类的方法

满足上面3个条件的类可以进行回收,也仅仅是可以,而不是和对象一样,不使用了就必然回收。

3. 垃圾回收算法

  • 标记清除算法

标记清除算法是最基础的收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉被标记的对象。它主要有两个缺点:一个是效率问题,标记和清理过程效率都不高;另一个问题是空间问题,在清除后会产生大量不连续的内存碎片,当空间碎片太多时会导致,当程序以后运行需要分配较大对象时无法找到足够的连续内存而不得不提前触发下一次GC动作。

  • 复制算法

为了解决效率问题,一种称为“复制”的收集算法出现了,它将内存按容量划分为大小相等的两块,每次使用其中的一块。当这一块用完时就将还存活的对象复制到令一块上,然后将一使用过的这一块内存清理掉。这样分配时就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行效率高。但是缺点也是显而易见的:内存利用率只有一半。

现在的商业虚拟机都采用这种算法来收集新生代,IBM的专门研究表明,新生代的对象98%都是朝生夕死的,所以并不需要按1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和lian两块较小的Survivor空间,每次使用Eden和其中的一块SuSurvivor。回收时,将Eden和刚才刚才用过的Survivor的空间中还活着的对象一次拷贝到令外一块Survivor空间上,最后清理掉Eden和刚才刚才使用过的Survivor的空间。当然,98%的dui'对象可回收只是一般场景下的数据,我们没办法保证每次回收的都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他的内存(这里指老年代)进行分配担保。

  • 标记--整理算法

复制算法在对象存活存活较高的时候需要执行较多的复制操作,效率会变低。跟关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。根据老年代的特点,有人提出了“标记--整理”算法,标记过程仍与“标记--清除”算法一样,但是后续步骤不是直接对可回收的对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  • 分代收集算法

当前商业虚拟机的垃圾回收都是采用的“分代收集”算法,根据对象的存货周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的垃圾回收算法。在新生代中,每次垃圾每次垃圾回收都发现大批对象死去,只有少量存活,那就使用复制算法。而老年代中因为对象存活率较高,没有额外的空间对它进行分配担保,就必须使用“标记--清理‘’或者‘标记--整理’‘算法来进行回收。

未完待续。。。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM内存结构: JVM内存分为如下五个部分: 1. 程序计数器 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都有一个程序计数器,是线程私有的,生命周期与线程相同。 2. Java虚拟机栈 Java虚拟机栈也是线程私有的,生命周期与线程相同。每个方法执行的时候,JVM都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法调用结束后,相应的栈帧也会被销毁。 3. 本地方法栈 本地方法栈也是线程私有的,它与Java虚拟机栈的作用非常相似,只不过它是为虚拟机使用到的Native方法服务。 4. Java堆 Java堆是JVM所管理的内存中最大的一块,也是所有线程共享的。Java堆是用于存储对象实例的内存区域,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的重点区域,也被称为GC堆。 5. 方法区 方法区也是线程共享的,用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在JDK8之前,永久代(PermGen)是方法区的一部分。在JDK8时,永久代被彻底移除,使用了元空间(Metaspace)来代替。 内存分配策略: JVM的内存分配策略主要有以下几种: 1. 对象优先在Eden区分配 当JVM需要为新的对象分配内存时,会优先在Eden区进行分配。如果Eden区没有足够的空间,JVM会通过Minor GC回收部分内存空间。 2. 大对象直接进入老年代 如果要分配的对象大小超过了Eden区的一半,JVM会直接将该对象分配到老年代。这样做的目的是为了避免在Eden区内产生大量的垃圾对象,从而降低了Minor GC的频率。 3. 长期存活的对象进入老年代 JVM会为每个对象定义一个年龄计数器,当一个对象在Eden区经历了一次Minor GC后仍然存活,会被移动到Survivor区。在Survivor区中,对象会被继续观察,如果其存活时间达到了一定的阈值,就会被晋升到老年代中。这样做的目的是为了保证长期存活的对象能够在老年代中有足够的空间进行分配。 4. 空间分配担保 每次进行Minor GC时,JVM都会检查老年代的可用空间是否足够,如果足够,就可以安全地将所有存活的对象晋升到老年代中。如果不足,JVM会检查这次Minor GC之前的晋升到老年代的对象的平均大小与老年代的剩余空间的比值,如果比值大于某个阈值(通常为50%),那么这次Minor GC就会中止,JVM会进行Full GC来释放一些空间。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值