您的位置:首页 > 其它

jvm垃圾回收以及内存分配相关知识

2014-10-10 18:45 423 查看


JVM判断对象存活的算法 

引用计数算法(reference counting):垃圾收集的早期策略。一个对象被创建时,为该对象分配一个引用计数器。当有地方引用它时,计数加1。当一个对象的引用超过了生存期或者被设置一个新的值时,引用计数减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集的时候,它引用的任何对象计数值减1。在这种方法中,一个对象被垃圾收集后可能导致后续其他对象的垃圾收集行动。

此算法实现简单,判断效率高;但很难解决对象之间的相互循环引用,如,A对象有一个对B对象的引用,B对象又反过来引用A对象,除此之外这两个对象无任何引用,但引用计数都不会为0,无法回收它们。(因此JVM
未使用此算法)。另外,每次引用计数的增加或者减少也带来额外开销。

根搜索算法(GC Roots Tracting):通过一系列的名为“GCRoots”对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference
Chain),当一个对象到

GC Roots没有任何引用链相连时,则此对象是不可用的。

可作为GC Roots
的对象包括:

虚拟机栈(栈帧中的局地变量表)中的引用的对象;

方法区中的类静态属性引用的对象和常量引用的对象;

Native 方法栈中 JNI的引用的对象

关于引用:强-->软-->弱-->虚

强引用(Stong Reference):指程序代码中普遍存在的,类似“Object obj= new

Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收被引用的对象;

软引用(Soft Reference):用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回事还是没有足够的内存,才会抛出内存溢出异常;

弱引用(Weak Reference):用于描述非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收弱引用关联的对象;

虚引用(Phantom Refenrece):对对象的生存时间无影响,无法通过虚引用来取得对象

实例。它的唯一目的就是希望能在这个对象别回收时收到一个系统通知。

 

 

 

 

JVM垃圾收集算法 

1,标记-清除(Mark-Sweep):算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象(如果对象在进行根搜索后发现没有与GC
Roots 相连接的引用链,对象将会被标记)。它是最基础的收集算法,因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。如下图:

 

2,复制算法(Copying):为了解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半。如下图:

 现在的商业虚拟机都采用这种收集算法来回收新生代,IBM的专门研究表明,新生代中的对象
98%是朝生夕死的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分

为一块较大的 Eden 空间和两块较小的Survivor
空间,每次使用 Eden
和其中的一块

Survivor。当回收时,将 Eden和Survivor
中还存活着的对象一次性地拷贝到另外一块

Survivor 空间上,最后清理掉Eden和刚才用过的Survivor
的空间。HotSpot虚拟机默认Eden
和 Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生
代容量的 90%(80%+10%),只有10%的内存是会被“浪费”的。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,如果另外一块Survivor
空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保(HandlePromotion)机制进入老年代。

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

 
4,分代收集(Generational Collection):当前商业虚拟机的垃圾收集都采用这种算法,它并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

 

 

 

 

 

JVM垃圾收集器 

垃圾收集器就是收集算法的具体实现,不同的虚拟机会提供不同的垃圾收集器。并且提供参数供用户根据自己的应用特点和要求组合各个年代所使用的收集器。本文讨论的收集器基于Sun Hotspot虚拟机1.6
版。下图中展示了
jdk1.6中提供的6种作用于不同年代的收集器,两个收集器之间存在连线的话就说明它们可以搭配使用。没有最好的收集器,也没有万能的收集器,只有最合适的收集器。从Serial收集器到Parallel
收集器,再到

CMS 收集器, G1收集器,用户线程的停顿时间在不断缩短,但是仍然没有办法完全消除。

 

1. Serial收集器

单线程收集器,使用复制收集算法,收集时会暂停所有工作线程(我们将这件事情称之为

Stop The World),直到收集结束,虚拟机运行在Client模式时的默认新生代收集器。

优点是:简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,

Serial收集器没有现成交互的开销。在堆比较小的情况下,一般停顿时间很短,是可以使用这种收集器的。如下图:

 

2. ParNew收集器

ParNew收集器就是
Serial的多线程版本,除了使用多条收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一摸一样。ParNew收集器是许多运行在server模式下的虚拟机中首选的新生代收集器(一个原因是在除了serial收集器外,目前只有它能与
CMS收集器配合使用)。

ParNew收集器是使用-XX:+UseConcMarkSweepGC选项的默认新生代收集器;也可以用-XX:+UseParNewGC选项来强制指定它。ParNew收集器在单
CPU环境中不比

Serial效果好,甚至可能更差,两个
CPU也不一定跑的过,但随着CPU数量的增加,性能会逐步增加。默认开启的收集线程数与
CPU数量相同。在CPU数量很多的情况下,可以使用-XX:ParallelGCThreads参数来限制线程数。如下图

 

 

3. Parallel Scavenge收集器

同ParNew一样是使用复制算法的新生代并行多线程收集器。

Parallel Scavenge的特点是它的关注点与其他收集器不同,CMS等收集器的关注点尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel
Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。也被称为吞吐量优先收集器。所谓吞吐量就是CPU
用于运行用户代码时间与CPU总消耗时间的比值。吞吐量=运行用户代码时间/运行用户代码时间+垃圾收集时间。

高吞吐量和停顿时间短的策略相比,主要强调任务更快完成,适用于后台运算而不需要太多交互的任务;而后者强调用户交互体验。 

Parallel Scavenge提供两个参数精确控制吞吐量,-XX:MaxGCPauseMillis控制最大垃圾收集停顿时间和-XX:GCTimeRatio设置吞吐量大小 

MaxGCPauseMillis允许的值是一个大于零的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。GC停顿时间缩小是以牺牲吞吐量和新生代空间来换取的,也就是要使停顿时间更短,需要使新生代的空间减小,这样垃圾回收的频率会增加,吞吐量也降下来了。 

GCTimeRatio的值是一个大于0小于100
的整数,也就是垃圾收集时间占总时间的比率。默认为99,则允许最大GC时间就占总时间的
1%(1/(1+99)).还有一个参数,-

XX:+UseAdaptiveSizePolicy,是个开关参数,打开后会自动调整Eden/Survivor
比例,老年代对象年龄,新生代大小等。这个参数也是Parallel Scavenge和
ParNew的重要区别。

4. Serial Old收集器 

是Serial
的老年代版本,同样是单线程收集器,使用标记-整理算法。主要是client模式下的虚拟机使用。参考上面图Serial/Serial
old.

两大用途:在JDK1.5
及之前的版本中与Parallel Scavenge搭配使用;作为
CMS收集器的后备预案。在并发收集发生Concurrent Mode Failure时使用。

5. Parallel Old收集器

是Parallel Scavenge
收集器的老年代版本,使用多线程和标记-整理算法。在JDK1.6

中才开始使用。由于之前的版本中,Parallel Scavenge只有使用
Serial Old作为老年代收集器,其吞吐量优先的设计思路不能被很好的贯彻,在Parallel Old
收集器出现后,这两者的配合主要用于贯彻这种思路。如下图:

 

6. CMS收集器

Concurrent MarkSweep以获取最短回收停顿时间为目标的收集器,比较理想的应用场景是B/S架构的服务器。如下图:

 基于标记-清除算法实现,运行过程分成4
个步骤:

 a)初始标记(需要stopthe
world),标记一下GC Roots能直接关联到的对象,速度很快

 b)并发标记,进行GCRoots
Tracing 的过程。

 c)重新标记(需要stop
the world),为了修正并发标记时用户继续运行而产生的标记变化,停顿时间比初始标记长,远比并发标记短。

 d)并发清除

缺点:

CMS收集器对
CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量就会降低。CMS默认启动的回收线程数为(CPU数量+3)/4。为了解决这一情况,有一个变种i-CMS,但目前并不推荐使用。

CMS收集器无法处理浮动垃圾(floating garbage)。同样由于CMSGC
阶段用户线程还需要运行,即还需要预留足够的内存空间供用户线程使用,因此CMS收集器需要预留一部分空间提供并发收集时的程序运作使用。默认设置下,CMS收集器在老年代使用了

68%的空间后就会被激活。这个值可以用-XX:CMSInitiatingOccupancyFraction来设置。要是CMS运行期间预留的内存无法满足程序需要,就会出现concurrent
mode failure,这时候就会启用Serial Old收集器作为备用进行老年代的垃圾收集。

空间碎片过多(标记-清除算法的弊端),提供-

XX:+UseCMSCompactAtFullCollection参数,应用于在
FULLGC后再进行一个碎片整理过程。-XX:CMSFullGCsBeforeCompaction,多少次不压缩的full
gc 后来一次带压缩的。

7. G1收集器

G1收集器与前面的CMS收集器相比有两个显著的改进:一是G1收集器是基于“标记-整理”算法实现的收集器,也就是说它不会产生空间碎片,这对于长时间运行的应用系统来说非常重要。二是它可以非常精确地控制停顿,既能让使用者明确指定在一个长度为M毫秒

的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java

(RTSJ)的垃圾收集器的特征了。 

G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力地避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个
Java堆(包括新生代、老年代)划分为多个大小固定的独立区域

(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage
First 名称的来由)。区域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。

 

 

 

 

 

JVM内存分配策略 

Java堆,分配对象实例所在空间,是GC的主要对象。分为新生代(Young

Generation/New),老年代(Tenured Generation/Old)

新生代又划分成 Eden Space, From Survivor, ToSurvivor

对象的内存分配,就是在堆上分配(但也可能经过JIT
编译后被拆散为标量类型并间接地在栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在
TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor
GC 非常频繁,一般回收速度也比较快。

老年代
GC(Major GC / Full GC):指发生在老年代的GC,出现了Major
GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge
收集器的收集策略里就有直接进行 Major GC的策略选择过程)。MajorGC的速度一般会比Minor
GC慢 10倍以上。

下面是最普遍的内存分配规则,并通过代码去验证这些规则。下面的代码在测试时使用

Client模式虚拟机运行,没有手工指定收集器组合,验证的是使用
Serial / Serial Old收集器下(ParNew / Serial Old收集器组合的规则也基本一致)的内存分配和回收的策略。

虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前内存各区域的分配情况。在实际应用中,内存回收日志一般是打印到文件后通过日志工具进行分析,不过本实验的日志并不多,直接阅读就能看得很清楚。

1.对象优先在
Eden分配

2.大对象直接进入老年代
所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组。大对象对虚拟机的内存分配来说就是一个坏消息(遇到一群“朝生夕灭”的“短命大对象”,写程序应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。 

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

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

4.动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到

MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于
Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: