转载

java垃圾回收与内存分配

一般判断对象是否已经“死去”有两种方法,一个是 引用计数法 ,还有一个是 可达性分析法

引用计数算法

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

而java虚拟机中并不是通过引用计数算法来判断对象是否存活的。

可达性分析算法

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

在java语言中,可作为GC Roots的对象包括下面几种:

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

再谈引用

有时我们可能有这样的需求:当内存空间还足够时,则能保留在内存之中;当内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。

jdk 1.2之后,java引入了4中引用即:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。下面简单介绍这四种引用.

  • 强引用一般是通过new来创建的引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用用来描述一些还有用但并非必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象进行回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用也是用来描述非必须对象的,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用的唯一作用是能在这个对象被收集器回收时收到一个系统通知。

垃圾收集算法

标记-清除算法

最基础的收集算法是“标记-清除”(Mark-Sweep)算法,算法分为“标记”和“清除”两个阶段:标记出所有需要回收的对象,在标记完成后统一回收。这里的标记通过可达性分析算法来标记。

这个方法存在两个不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后分配大对象时,无法找到足够的连续内存而不得不提前触发另一个垃圾收集动作。

复制算法

为了解决效率问题,它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完之后,就将还活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

现代的商业虚拟机都采用这种收集算法来回收新生代,将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

标记-整理算法

根据老年代的特点,一般采用“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前商业虚拟机的垃圾收集都采用“分带收集”(Generational Collection)算法,根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。即在新生代中,每次垃圾收集时都会有大批对象死去,只有少量存活,选用复制算法。对于老年代中对象存活率很高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记整理”算法进行回收。

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。这里主要介绍CMS收集器和G1收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它的运作过程分为4个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始化标记阶段稍长一些,但远比并发标记的时间短。

G1收集器

G1(Garbage-First)收集器是一款面向服务端应用的垃圾收集器。G1的特点如下:

  • 并行和并发
  • 分代收集
  • 空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。
  • 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型。

在G1之前的其它收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。

G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking)
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)
  • 筛选回收(Live Data Counting and Evacuation)

内存分配与回收策略

java技术体系中所提倡的自动内存管理最终可以归结为自动化的解决两个问题:给对象分配内存以及回收分配给对象的内存。

对象优先在Eden分配

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

大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

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

虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应该在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象的年龄设为1。对象每经过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。

动态对象年龄判定

为了能更好的适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到Max TenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

原文  https://juejin.im/post/5d2fdfa96fb9a07f070e62aa
正文到此结束
Loading...