Java之JVM垃圾回收 内存结构以及垃圾回收算法
作为Java语言的核心之一,JVM垃圾回收帮我们解决了让我们很头疼的垃圾回收问题。我们不需要像VC++一样,作为内存管理的统治者需要我们对我们分配的每一块内存进行回收,否则就会造成内存泄露问题。
是不是只要有JVM存在我们就不会出现内存泄露问题,出现内存泄露问题我们又该怎么办,如果我们想提高我们程序的稳定性和其他性能我们能从什么地方下手!!!相信这些问题是我们程序过程中不可逾越的。了解JVM的内存分配及其相应的垃圾回收机制,不仅仅是可以了解底层的JVM运行机制,而且对于程序性能的优化和提升还是很有必要的。
一.JVM堆内存介绍
工欲善其事,必先利其器。所以了解堆内存的内部结构是很必要的。
在JVM中堆空间划分为三个代:年轻代(Young Generation)、年老代(Old Generation)和永久代(Permanent Generation)。年轻带主要是动态的存储,年轻带主要储存新产生的对象,年老代储存年龄大些的对象,永久带主要是存储的是java的类信息,包括解析得到的方法、属性、字段等。永久带基本不参与垃圾回收。所以说我们说的垃圾回收主要是针对年轻代和年老代。
年轻代又分成3个部分,一个eden区和两个相同的survior区。刚开始创建的对象都是放置在eden区的。分成这样3个部分,主要是为了生命周期短的对象尽量留在年轻带。当eden区申请不到空间的时候,进行minorGC,把存活的对象拷贝到survior。年老代主要存放生命周期比较长的对象,比如缓存对象。(经过IBM的一个研究机构研究数据表明,基本上80%-98%的对象都会在年轻代的Eden区死掉从而本回收掉,所以说真正进入到老年代的对象很少,这也是为什么MinorGC比MajorGC更加频繁的原因)
具体JVM内存垃圾回收过程描述如下 :
1、对象在Eden区完成内存分配
2、当Eden区满了,再创建对象,会因为申请不到空间,触发minorGC,进行young(eden+1survivor)区的垃圾回收
3、minorGC时,Eden不能被回收的对象被放入到空的survivor(Eden肯定会被清空),另一个survivor里不能被GC回收的对象也会被放入这个survivor,始终保证一个survivor是空的
4、当做第3步的时候,如果发现survivor满了,则这些对象被copy到old区,或者survivor并没有满,但是有些对象已经足够Old,也被放入Old区 XX:MaxTenuringThreshold
5、当Old区被放满的之后,进行fullGC
补充:
MinorGC:年轻代所进行的垃圾回收,非常频繁,一般回收速度也比较快。
MajorGC:老年代进行的垃圾回收,发生一次MajorGC至少伴随一次MinorGC,一般比MinorGC速度慢十倍以上。
FullGC:整个堆内存进行的垃圾回收,很多时候是MajorGC
以后就是堆内存结构已经大致的垃圾回收过程。
二、对象分配原则
1.对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
2.大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
3.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
4.动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
5.空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
三、垃圾收集器
作为JVM中的核心之一垃圾收集器,主要完成的功能包括:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。所以说我们在实现垃圾收集器的同时就要实现两个算法一个是发现无用的对象第二就是回收该对象的内存。
收集器主要分为引用计数器和跟踪收集器两种,Sun JDK中采用跟踪收集器作为GC实现策略。发现无用对象只要的实现算法包括引用计数法和根搜索算法,引用计数法主要是JVM的早期实现方法,因为引用计数无法解决循环引用的问题,所以现在JVM实现的主要是根搜索算法,
引用计数法:堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就不可用从而可以被回收。 根搜索算法:通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
目前的收集器主要有三种:
串行收集器:使用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高
并行收集器:对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用
并发收集器:可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用
四、垃圾收集器的回收算法
Copying算法:
算法:复制采用的方式为从根集合扫描出存活的对象,并将找到的存活对象复制到一块新的完全未使用的空间中。
过程: 此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不过出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
Mark-Sweep算法:
算法:标记-清除采用的方式为从根集合开始扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未标记的对象,并进行回收。
过程: 第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。它停止所有工作,收集器从根开始访问每一个活跃的节点,标记它所访问的每一个节点。走过所有引用后,收集就完成了,然后就对堆进行清除(即对堆中的每一个对象进行检查),所有没有标记的对象都作为垃圾回收并返回空闲列表。
Mark-Compact算法:
算法:标记阶段与“Mark-Sweep”算法相同,但在清除阶段有所不同。在回收不存活对象所占用的内存空间后,会将其他所有存活对象都往左端空闲的空间进行移动,并更新引用其对象指针。
过程:此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
Sun JDK GC策略:
新生代算法实现:Copying,Copying,Copying
旧生代算发实现:Mark-Sweep-Compact,Mark –Compact,Mark –Sweep!!