本文是阅读深入理解java虚拟机之后对jvm垃圾收集机制进行总结的读书笔记。
以上就是1.7及之前的java内存的布局,在java1.8之后使用Meta Space取代了方法区。接下来详细介绍一下每一部分。
程序计数器线程私有,在程序运行时,每个线程实际上都是一系列的方法调用的过程,而方法调用的过程最后转化为字节码一条条执行的过程,在这个过程中,程序计数器pc记载了当前线程执行的字节码的位置。
jvm使用字节码执行引擎支持程序的执行,而java虚拟机是基于栈的字节码执行引擎。这就意味着每次方法调用或者返回,在jvm中体现为虚拟机栈上栈帧的入栈和出栈。在栈帧中主要保存了方法调用的形式参数,方法内声明的局部变量,还有操作数表,方法出口动态链接等信息。虚拟机栈自然也是线程独立的,每个线程根据自己执行的方法不同,有不同的虚拟机栈,不同的虚拟机栈之间的数据互不干扰。虚拟机栈的生命周期和拥有它的线程相同。
本地方法栈和虚拟机栈类似,只不过本地方法栈是native方法执行时产生的。
在HotSpot虚拟机的实现中方法区又被称为永久代,是所有线程共享的。方法区存放了一些相对更稳定的数据,如已经被类加载器加载的类信息(Class实例),静态变量,常量(如Integer的对象池机制)。
堆内存是java虚拟机内存回收的主要对象。也是java内存管理的最大的一块空间。几乎所有的对象的实例和数组都在堆上分配。堆又可以分为年轻代和老年代,年轻代又可以分为Eden区和两个survivor区,默认的Eden:survivor的值为8,即Eden区占年轻代的80%,两个Survivor各占10%。
java 8对java内存模型进行了一些调整。详细可以看这里metaspace介绍。使用Meta Space取代了方法区,将符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap,其中native heap指的是jvm本身运行时使用的内存,一般指的是C-堆。需要注意的是,Meta Space也是在native heap上分配,即Meta Space的大小只受本地可用内存大小的影响,使用MaxMetaspaceSize这个参数可以控制Meta Space区的大小,并在MetaSpace区的占用达到这个值时进行类的卸载GC。
java 使用可达性分析算法来判断一个对象是否还存活。所谓可达性分析算法就是找出一些可以确定当前仍然存活的对象(GCROOT),从这些对象出发,沿着引用关系遍历,能遍历到的对象就是存活的对象,不能遍历到的对象就是已经死亡的对象。哪些对象可以认为是存活的呢,首先是当前仍然在使用的对象,即虚拟机栈和本地方法栈局部变量表中引用的对象。另外方法区中的常量和类静态变量引用的对象也可认为是存活的。
Object类中有一个finalize()方法,这个方法与gc相关,覆盖了finalize方法的类的实例,在将要被清除时判断其finalize方法是否被调用过,如果没有调用过则该对象会首先进入finalize队列,而不是马上被清除。这个finalize队列中的对象会由一个低优先级的Finalizer线程调用其finalize方法,在进入队列一段时间后,gc线程会再次检查finalize队列中的对象,如果此时对象成功执行了finalize方法并和某些存活的对象发生了联系,那么这个对象就可以不被回收,反之这个对象这次将被回收
Java有4种引用类型,按照强弱分别是强引用,软引用,弱引用,虚引用,使用这四种引用引用的对象在gc时有不同的表现。
强引用:String s = new String("test");
这里的s就是一个强引用,强引用的对象只有在不再存活,即和GCROOT没有联系的时候才会被回收。
软引用:SoftReference
软引用的对象在即将发生内存溢出异常时会被回收,即当将可回收的对象全部回收之后,剩余的内存仍然不够分配下次的需要,那么会回收所有的软引用,如果还是不够,那就抛出异常。
弱引用:WeakReference
弱引用引用的对象只要遇到垃圾收集,不管是否存活都会立即被回收。
虚引用:
ReferenceQueue
PhantomReference
虚引用不能被用来操作对象,一个对象被虚引用引用了和没有被引用一样。虚引用引用的对象在回收时会首先加入引用队列,可以通过判断对象的虚引用是否已经进入了上面的那个队列来判断这个对象是否将要被gc.
所谓标记清除算法,即先将要回收的对象进行标记,标记完成之后统一回收这些对象。实际上标记和清除这两个步骤的效率很低,而且标记清除算法会产生内存碎片,不利于之后内存的分配。
所谓复制算法就是将内存分为两个区域,使用的时候先在一边分配,等到空间不够用的时候就将一边的存活的对象复制到另外一边,然后将原先用来分配的那一边里面所有的对象都清除。使用复制算法能提供非常规整的内存,使得内存分配只用移动堆顶指针,按照顺序分配即可。由于实际上90%以上的对象都是朝生夕灭的,因此也不用为存活的对象预留太多空间。在HotSpot虚拟机中使用两个10%大小的survivor区和一个80%的Eden区,每次在一个survivor区和另一个Eden区里分配,垃圾收集的时候将存活的对象放入一个10%的survivor区,这样使得内存的利用率达到了90%,基本上客服了复制算法需要预留一部分空间的弊端。但是这样需要内存分配担保机制来保证在存活的对象太多时的内存分配。即假如有多于10%的对象,那么应该将放不下的对象放入老年代。
在对象存活较多的情况下,复制算法需要保留较多的空余空间来存放收集时尚存活的对象,对于某些生命周期较长的对象,采用复制算法效率不高。比如老年代的对象每次GC的存活率较高,不适合使用复制算法,可以使用标记整理算法。
标记整理算法包括标记和整理两个过程,标记过程和标记清楚算法的标记过程相同,整理过程使存活的对象向一端移动,借此来保证规整的剩余空间,避免了空间碎片。
jvm针对不同的类型对象的生存周期不同,使用分代收集算法来对不同类型的对象采用不同的收集算法。对象一开始分配时,都在年轻代分配,当一个对象经历多次gc仍然存活时则认为这个对象是一个生命周期较长的对象,将这个对象加入老年代。年轻代的对象gc时存活率低,gc频繁,使用复制算法进行gc,并使老年代为年轻代提供分配担保。老年代gc时存活率高,也没有额外的空间进行分配担保,因此老年代的垃圾收集器使用标记整理和标记清除算法进行垃圾收集。
java1.8 使用的默认垃圾收集器是Paraller Scavenge+Parallel Old,另外两个比较重要的垃圾收集器是CMS和G1
Parallel Scavenge垃圾收集器是一个年轻代的垃圾收集器,它的控制目标是实现一个可控制的吞吐量。所谓吞吐量就是执行用户程序的时间和CPU消耗的时间的比。用户可以通过两个参数来对吞吐量进行控制:
-XX:MaxGCPauseMillis
控制最大单次垃圾收集停顿时间
-XX:MaxGCTimeRatio
直接设置吞吐量大小的参数,这个参数的默认值是99,表示允许最大1%的垃圾收集时间,实际上这个值是用户代码运行时间/垃圾收集时间的值,设为19时吞吐量是 19/(19+1) =0.95
这个收集器还有一个参数值得注意:
-XX:UseAdaptiveSizePolicy
这个参数打开了之后就不用手动设置新生代大小,Eden survivor比例,晋升老年代年龄等参数,虚拟机会根据需要自己调节。
CMS全称是Concurrent Mark Sweep,特点是可以和用户程序并发运行。CMS可以在实现在垃圾收集时只有很短的停顿时间。CMS的垃圾收集分为4个步骤。
第一个步骤初始标记,初始标记阶段不是和用户程序并发执行的,需要stop the world,初始标记阶段仅标记和GC Roots直接关联的对象。
第二个步骤是并发标记阶段,并发标记阶段是和用户程序并发执行的,但是并发标记是一个比较耗时的操作,通过并发执行可以使用户感觉不到停顿。并发标记将对老年代的所有对象进行标记,找出已经不再存活的对象。
第三个步骤是重新标记,重新标记阶段对第三步因为用户程序执行导致存活状态发生变化的对象进行重新标记,这一阶段不是和用户程序并发执行的,但是这一部分对象的数量很少,时间也非常短。
第四阶段,第四阶段是并发清理,并发清理阶段是和用户程序并发执行的,负责将标记为不再存活的对象清理掉。
CMS将耗时的操作和用户程序并发执行的特点使其有较好的响应性,但是其有以下四个缺点:
1 CMS是CPU敏感型垃圾收集器,对于CPU资源较紧张的环境,使用CMS可能会严重影响到用户程序的执行。
2 CMS不能很好地处理浮动垃圾,CMS的并发清理阶段产生的垃圾成为浮动垃圾,另外因为并发清理时用户程序还在运行,所以不能等到老年代快占满了才启动垃圾收集,需要给浮动垃圾和用户程序预留执行的空间。
3 CMS基于标记清除算法,标记清除算法会产生空间碎片。
G1垃圾收集器称为Gabrage Frist,是jdk 9的默认垃圾收集器。与其它的垃圾收集器只单独负责新生代或老年代不同,G1垃圾收集器兼顾了新生代和老年代的垃圾收集。G1垃圾收集器的关键在于其建立了一个可预测的停顿时间模型。
使用-XX:MaxGCPauseMillis=200参数可以设置允许的最大停顿毫秒数。
G1 垃圾收集器将java堆划分为若干个(可以通过-XX:G1HeapRegionSize参数调整Region的大小)分区,收集时在整体上对region采用标记清除算法收集,对每个region采用复制算法收集。之前的Eden区,survivor区,老年代就不再是物理分开的,而是由一些region 组成。
G1 中每个Region都有一个Remembered Set,这个Remembered Set记录这个Region里面的对象被另外的Region引用的情况。使用Remembered Set可以在对新生代进行收集的时候避免对老年代进行扫描。
G1 垃圾收集过程:
G1 垃圾收集分为两个主要的步骤:
1 全局并发标记(global concurrent marking)
2 拷贝存活对象 (evaluation)
全局并发标记阶段又分为下面这4个步骤:
1 初始标记,初始标记阶段会标记GC Roots,这一阶段会Stop the world
2 并发标记,这个阶段对整个堆进行并发标记,这个阶段和用户程序并发运行
3 最终标记, 这个阶段对第三个阶段导致引用情况发生变化的对象进行重新标记,STW
4 清理,这个阶段会清点并重置标记状态,更新rememberSet,STW
evalutation阶段将全局并非标记阶段筛选出的可以回收的对象按照回收的价值进行排序,根据用户配置的最大停顿时间来进行最后的回收,将存活的对象复制到另外的Region中。