引用文章有:
JVM内存管理、JVM垃圾回收机制、新生代、老年代以及永久代
GC(gabage collection)垃圾收集器。
- 为什么java要有GC。
我们编程人员在编程中用到的内存空间主要有两部分:堆栈和堆。一般变量的空间都占用的是堆栈空间,不过我们编程人员可以主动去申请堆的空间,例如C语言的malloc函数和calloc函数和java的new关键字等,但是C语言给出了free函数来释放申请的空间,但是在java中却没有看到类似的操作。为了进行内存的管理GC就应用而生。GC的出现使得java程序员不用操心内存管理,因为GC会自动进行垃圾回收。
- jvm内存模型
方法区:存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用永久代(PermanetGeration)来存放方法区,(在JDK的HotSpot虚拟机中,可以认为方法区就是永久代,但是在其它类型的虚拟机中,没有永久代的概念,有关信息可以看周志明的书)可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值。
GC主要是针对堆区域的,在Java中,堆被划分了两个不同的区域:新生代(Young)和老年代(Old)。新生代又被划分为三个区域:Endn、From Survivor、To Survivor。这样划分的目的是为了使JVM能更好的管理内存中的对象,包括内存的分配以及回收。
新生代(Young)与老年代(Old)的比例1:2(该值可以通过参数-XX:NewRatio来指定),即:新生代(Young)=1/3的堆空间大小。老年代(Old)=2/3的堆空间大小。其中,新生代(Young)被细分为Eden和Survivor区域,这两个Survivor区域分别被命名为from和to,以示区分。默认的Eden:ftom:to=8:1:1(可以通过参数-XX:SurvivorRatio来设定),即:Ende=8/10的新生代空间大小,from=to=1/10的新生代空间大小。
判断对象已死的算法:
- 引用计数法:在每个对象中增加一个引用计数,这个计数代表当前程序有多少个引用引用了此对象,如果此对象的引用计数变为0,那么此对象就可以作为GC收集的对象。(但存在循环引用的BUG,因此不被使用)
- 可达性分析算法(根搜索算法):
GC Root:
a.java虚拟机栈中(栈帧中的本地变量表)中的引用。
b.方法区中的类静态属性引用。
c.方法区中的常量引用。
d.本地方法栈中JNI本地方法的引用。
e.从永久代到年轻代的引用可以被当成GC Roots,从年轻代到永久代的引用直接忽略。
通过向一系列名为GC Roots的引用作为起点,从这些节点向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则该对象不可达,可被GC回收。
- GC的算法
1、标记-清除算法
包含标记和清除两个阶段,是最基础的算法。标记就是标记出那些已经可以被回收的对象,在标记完成后进行清除,此算法效率不高而且会产生内存碎片,当程序在以后的运行中需要分配一大块的连续内存时可能会因为内存碎片而提前触发垃圾回收。
2、复杂算法(新生代采用的算法之一)
为了解决标记-清除算法的效率问题的,将内存划分为多块,在标记完一块区域的对象状态后,将存活的对象拷贝到另一块上,然后将之前那块一次清理掉。
在新生代上对象的存活期很短,并且98%的对象是朝生夕死,所以在内存的划块上不需要1:1进行划分,hotstop虚拟机默认按照Eden-80%,两块Survivor各10%进行划分(8:1),其中Eden和一块Survivor空间存放新生代的对象,还有一块用来负责垃圾回收时Eden和另一块Survivor的存活对象,然后一次清除调用Eden和那块Survivor内存,下次垃圾回收时这两块Survivor的功能互调换。当一块Survivor不够装下存活的对象时,则会向老年代的分配担保机制(借内存)进入老年代。
3、标记-整理算法
相比标记-整理算法,此算法解决了标记清理算法的内存碎片问题,清理完可回收的对象会对该内存块进行整理。
4、分代收集算法
当前商业虚拟机都采用此算法,其实说这是算法还不如说是将前面3个算法在新生代和老年代上应用不同的一个策略,新生代采用复制算法,老年代采用标记-清除或者标记-整理算法。
- 新生代的GC(Minor GC)
1、Serial收集器(复制算法)
此收集器是最基本的,也是历史最悠久的收集器,是JDK1.3.1之前新生代的唯一选择,该收集器的缺点是垃圾收集器会暂停所有用户线程来进行垃圾收集,后面的其它收集器基本就是在此收集器的基础上进行优化修改而来,在Client模式下运行还是可以接受的。
2、ParNew收集器(复制算法)
Serial的多线程版本,是许多运行在Server模式下的虚拟机中新生代收集器的首选。在多核心的服务器上运行游戏明显,默认开启的线程=CPU核心数,通过使用-XX:ParallelGCThreads参数可以来限制垃圾收集的线程数。
3、Parallel Scavenges收集器(复制算法)
更parNew收集器差不多,但与上面两个收集器的关注端不同,前面的两个更多的关注用户的停顿时间,而此收集器更关注CPU的吞吐量(吞吐量=运行用户代码的时间/(运行用户代码时间+;垃圾收集时间)),其中有两个参数可以控制这个吞吐量,1.-XX:MaxGCPauseMillis xxxx,控制最大垃圾收集停顿时间(x>0的毫秒数)。2.-XX:GCTimeRatio XXX,设置吞吐量的大小(设置一个0<x<100的整数,表示垃圾收集时间占总时间的比率,相当于吞吐量的倒数)。
另外,此收集器还有一个参数-XX:+UseAdaptiveSizePolicya,如果打开这个这个开关,则新生代的大小(-Xmn)、Eden、Survivor(-XX:SurvivorRatio)、Survivor(-XX:SurvivorRatio)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等参数就不需要手工指定了,虚拟机会根据系统的运行情况来动态调整。
- 老年代GC(Major GC/Full GC)
1、 Serial Old收集器(标记-整理算法)
新生代serial的老年代版本,主要有如下两个用途:
①在JDK1.5以及之前的版本搭配新生代的Parallel Scavenge收集器使用。
②作为Concurrent Mode Failure的一个后备方案。
2、Parallel Old收集器(标记-整理算法)
新生代Parallel Scavenge收集器的老年代版本,在JDK1.6的版本才提供,只能和新生代的Parallel Scavenge收集器搭配使用。
3、CMS收集器(标记-清除算法)
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的收集器,具有并发低顿挫的特点,但也有几个缺点:
①对CPU资源很敏感(需要更用户线程并发执行)。
②无法处理浮动垃圾(Float Garbage,在标记过程后产生的垃圾),CMS需要预留足够的内存空间给用户线程使用,所以在CMS收集器在老年代使用了68%的空间后被激活(可以通过-XX:CMSInitiatingOccupancyFranction参数来设置触发百分比),如果在运行过程中预留的空间无法满足程序的需要,则会出现一次Concurrent Mode Failure失败并启用后备预案(Serial Old收集器)进行Full GC。
浮动垃圾:在并发清除过程中,程序可能还在运行,可能产生新的垃圾,但是本次GC不可能清除掉这些新产生的垃圾了,所以这些新产生的垃圾就叫浮动垃圾,也就是说在一次CMS的GC后,用户获取不到一个完全干净的空间,还算是或多或少存在浮动垃圾的。
③由于采用标记-清除算法,会产生内存碎片,不过可以配置-XX:+UseCMSCompactAtFullCollection开关让Full GC后进行一次碎片整理,也可以使用-XX:CMSFullGCsBeforeCompaction来设置执行多少次Full GC自动进行一次碎片整理。
共分为4个过程:
(一)、初始标记(需暂停用户线程,但过程很快)
(二)、并发标记(多线程方式,通过根可达性算法并发执行)
(三)、重新标记(需暂停用户线程,对前面的标记进行修正)
(四)、并发清除
4、G1收集器
Garbage First收集器是当前收集器技术发展的前沿成果,在JDK1.6_update14提供试用,是一款面向服务器应用的垃圾收集器。
主要有以下特点:
①并发运行
②基于标记-整理算法,不会产生空间碎片
③非常精确的控制停顿,能让使用者指定在一个长度为M毫秒的时间片段内,消耗在收集的时间不超过N毫秒,这几乎是实时JAVA(RTSJ)垃圾收集器的特征了。
④将整个堆(新生代、老年代)划分为多个大小固定的独立区域,并与这些区域为单位进行垃圾回收,避免对整个堆进行全量回收操作,并对这些区域进行优先级维护和回收。
G1收集器把Heap分为多个大小相等的Region,G1可以有计划的避免进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先收集价值大的Region,保证G1收集在有限时间内获取最大的收集效率。在进行内存回收时,是将某个或多个区域的存活对象拷贝至其他空区域,同时释放被拷贝的内存区域,这种方式在整体上看是标记-整理,在局部看(两个Region之间)是复制算法,所以不会产生内存碎片。
为什么要分不同的GC类型,主要是1.对象有不同的生命周期,经研究,98%的对象都朝生夕死;
2.根据各代的特点应用不同的GC算法,提高GC效率。
- 方法区(永久代)回收的相关说明:
永久代的垃圾回收只要有两部分内容:废弃的常量和无用的类。
废弃的常量:无对象引用此常量。
无用的类:1、该类所有实例已经被回收(堆中不存在任何该类的实例)
2、加载该类的ClassLoader已经被回收
3、给类对应的Class对象已经没有任何地方被引用,无法在任何地方通过反射访问该类的方法
- 进入老年代的几种途径
1、在新生代GC过程中survivor空间不够,通过老年代的分配担保机制提前转入老年代。
2、超过参数PretenSizThreshold设置的大对象直接进入老年代,所谓大对象就是大量需要连续内存的java对象(例如很长的字符串和数组)。
3、长期存活的新对象进入老年代,如果对象在一次Minor GC(新生代GC)后仍存活并且能被survivor容纳后被移动到survivor中后将该对象的年龄+1,当它的年龄达到一定的程度(默认15)后就会晋升到老年代,这个阀值可以通过-XX:MaxTenuringThreshold来设置。
4、虚拟机并不总要求对象达到XX:MaxTenuringThreshold的值后才晋升老年代,如果在survivor空间中相同年龄所有对象大小总和大于survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代。
- GC是怎样运行的
(一)、标记
目的:找出所有引用不为0(live)的实例。
方法:找到所有的GC的根结点(GC Root),将他们放在队列里,然后依次递归遍历所有的根结点,将所有被遍历到的结点标记成live。弱引用不会被考虑在内。
(二)、计划和清理
1、计划
目标:判断是否需要压缩
方法:遍历当前所有的generation上所有标记(live),根据特定算法做出决策。
2、清理
目标:回收所有free空间
方法:遍历当前所有的generation上所有的标记(live or Dead),把所有处在live实例中间的内存块加入到可用内存链表中去
(三)、引用更新和压缩
1、引用更新
目标:将所有引用的地址进行更新
方法:计算出压缩后每个实例对应的新地址,找到所有GC的根结点(GC Root),将他们放到队列里,然后依次递归遍历所有的根节点以及引用的所有子结点和子子结点,将所有被遍历到的结点中引用的地址进行更新,包括弱引用。
2、压缩
目标:减少内存碎片
方法:根据计算出来的新地址,把实例移动到相应的位置。
- Minor GC、Major GC、Young GC与Full GC分别在什么时候发生?
MinorGC:指发生在新生代的垃圾收集动作,所有的Minor GC都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个过程非常短暂。
通常采用复制算法,用空闲指针的方式来控制GC触发,指针保持在最后一个分配的对象在新生代区间的位置,但有新对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。
Major GC/Full GC:老年代的GC,指发生在老年代的GC。(也有的说发是将两者分开,Full GC是对整个堆内存进行回收)众所周知,所有通过new创建的对象的内存都在堆中分配,堆被划分为新生代和老年代,新生代又被进一步划分为Eden和survivor区,而Survivor由From和to组成。
新生代:新创建的对象都是用新生代的内存,Eden空间不足时,触发Minor GC,这时会把存活的对象转移进Survivor区。
老年代:老年代用于存放经过多次Minor GC之后仍然存活的对象。