垃圾收集与内存分配

3/8/2017来源:ASP.NET技巧人气:1567

一:垃圾回收

GC主要完成的三个任务:哪些内存需要回收,什么时候回收,如何回收

1.1.1:虽然目前内存的动态分配与内存回收技术已经相当的成熟,但我们还是要去了解GC和内存分配。当需要排查各种内存溢出、内存泄漏问题时,需要对这些实施必要的监控和调节。

java内存运行时区域的各个部分,其中程序计数器、Java栈、本地方法栈这三个区域随线程二生,随线程二灭,因此这三个区域的内存分配和回收都具备确定性,就不需要过多的考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。二Java堆和方法区(静态区)则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我只有在程序运期间才知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器多关注的都是这部分内存。

1:如何判断对象是“存活”还是“死去”

1.2.1:引用计数算法

有些地方是通过计数算法:即给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1,;当引用失效时,计数器值减1;任何计数器为0 的对象就是不可能在被使用(死去)。客观的说,引用计数算法的实现简单,判断效率高,也被方法的应用。但,现在主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中蛀牙的原因是它很难解决对象之间相互循环引用的问题。

1.2.2:可达性分析算法:

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路劲称为引用链,当一个对象到GC Roots没有任何引用链相连的时候,则证明此对象是不可用的。

 

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

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

b:方法区中静态属性引用的对象

c:方法区中常量引用的对象

d:本地方法栈中JNI(一般说的native方法)引用的对象

1.2.4:再谈引用

无论是通过引用计数方法还是可达性分析算法判断对象是否存活都是与“引用”有关,Java中的引用定义很传统,如果reference类型的数据与存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。其实,很多时候,我们希望当内存还足够的时候,则讲这些对象保存在内存中,如果内存空间在进行垃圾收集后还是非常的紧张,则可以抛出这些对象。在后来,又对引用分为强引用、软引用、弱引用、虚引用四种,并且这四种引用依次逐渐减弱。

1.2.4:生存还是死亡

上面说到两个算法来判断对象的存活和死亡,其实并非绝对。即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有雨GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是对此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已近被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判断为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后有一个由虚拟机自动建立的、低优先级的Finalizer线程去执行。

2:垃圾收集算法

垃圾收集算法主要有:标记-清楚算法、复制算法、标记整理算法、分代收集算法,这里我就不一一展开讨论了,读者可以自行查阅。

3:垃圾收集器

垃圾收集器主要有:Serial收集器、ParNew收集器、Parallel Scavenge收集器、Serial Old收集器、Parallel Old收集器、CMS收集器

 

 

二:内存配别与回收策略

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

对象的内存分配,往大的方向讲,就是在堆想分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况也可能会直接分配在老年代,分配的规则并不是百分之百固定,与虚拟机中与内存有关的参数的设置有关。

下面是几种常见的内存分配规则:

2.1:对象优先在Eden分配

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

2.2大对象直接进入老年代

所谓的大的对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获得足够的连续空间来“安置”它们。虚拟机提供了一个-XX:PRetenureSizeThreshold参数,令大于这个参数设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

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

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

2.4:动态对象年龄判定

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

推荐书籍:《深入理解Java虚拟机》 周志明 著