最近开始读周志明老师《深入理解Java虚拟机》,已经将第二部分的自动内存管理机制详细的读了一遍,对java虚拟机的内存模型,垃圾回收有了初步的了解。
一、JVM内存区域
Java虚拟机在运行时,会把内存空间分为若干个区域,根据《Java虚拟机规范(Java SE 7 版)》的规定,Java虚拟机所管理的内存区域分为如下部分:方法区、堆内存、虚拟机栈、本地方法栈、程序计数器。
1.程序计数器
程序计数器(Program CounterRegister)是一块较小的内存,它可以看做是当前线程所执行字节码的行号指示器。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个单独的程序计数器,各线程之间计数器互不影响,独立存储,这类内存区域可以称为“线程私有”的内存。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2.Java虚拟机栈
与程序计数器一样,Java虚拟机栈(Java Virtual MachineStacks)也是线程私有的,它的生命周期与线程相同。虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应Java代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个Java方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。
3.本地方法栈
本地方法栈(Native MethodStacks)与虚拟机栈所发挥的作用是非常相似的。他们的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机执行Native方法服务。
4.java堆
对大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放实例对象,几乎所有的对象实例都在这里分配内存。从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以Java堆可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
5.方法区
方法区(Method Area)与Java堆一样是各线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机把方法区描述为堆的一个逻辑部分,但它却有一个别名叫做Non-heap(非堆),目的应该是与Java堆区分开来。从jdk1.7已经开始准备“去永久代”的规划,jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中,(常量池除字符串常量池还有class常量池等),这里只是把字符串常量池移到堆内存中;在jdk1.8中,方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。
二、JVM垃圾回收
垃圾回收,就是通过垃圾收集器把内存中没用的对象清理掉。垃圾回收涉及到的内容有:1、判断对象是否存活;2、垃圾收集算法;3.垃圾回收器。
1 判断对象是否存活
(1)引用计数法
引用计数法是指给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时候计数器值为0就表示不会再被任何对象使用。引用计数法(Reference Counting)的实现简单,判断效率也很高,在大部分情况下都是一个不错的算法。但在主流的Java虚拟机里面没有使用引用计数法来管理内存,主要原因是它很难解决对象之间相互循环引用的问题。
如图:当引用1和引用2断开后,对象1和对象2之间彼此引用,这时候两个对象的引用计数器不为0,无法判断他们是死对象,垃圾回收器也就无法回收。
(2)可达性分析法
基本思路是通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链(就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
①虚拟机栈(栈帧中的本地变量表)中引用的对象。
②方法区中类静态属性引用的对象。
③方法区中常量引用的对象。
④本地方法栈中JNI(即Native方法)引用的对象。
进行可达性分析后对象和GC Roots之间没有引用链相连时,对象将会被进行一次标记,接着会判断如果对象没有覆盖Object的finalize()方法或者finalize()方法已经被虚拟机调用过,那么它们就会被行刑(清除);如果对象覆盖了finalize()方法且还没有被调用,则会执行finalize()方法中的内容,所以在finalize()方法中如果重新与GC Roots引用链上的对象关联就可以拯救自己。但是周志明老师也建议尽量不用使用这个方法,因为它运行代价高昂,不确定性大,无法保证各个对象的调用顺序,并不适合做“关闭外部资源”之类的工作。
(3)对象引用
在JDK1.2之后,Java对引用的概念做了扩充,将引用分为强引用(StrongReference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(PhantomReference)4种,这4种引用的强度依次减弱。
2 垃圾收集算法
2.1 标记-清除算法
“标记-清除”(Mark-Sweep)算法是最基础的收集算法,这个算法分为“标记”和“清除”两个阶段:首先标记出需要回收的所有对象,在标记完成后统一回收标记的对象,标记过程就是之前讲的通过引用计数法和可达性分析法进行判定。之所以说它是最基础的算法,是因为后面的算法都是在它基础上进行的改进,它的主要不足有两个:一个是效率问题,标记和清除的效率都不高,还有一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致在需要分配较大对象时,无法找到连续的内存空间而不得不提前触发另一次垃圾收集动作。如图:
2.2 复制算法
把内存分为大小相等的两块,每次存储只用其中一块,当这一块用完了,就把存活的对象全部复制到另一块上,同时把使用过的这块内存空间全部清理掉,往复循环。现在商用虚拟机都采用这种算法来回收新生代,因为新生代中98%的对象都是朝生夕死的,所以并不需要1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和一块Survivor空间。当回收时,将Eden和Survivor中开存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小是8:1,也就是每次新生代可用空间是整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%可回收只是一般的情况的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
2.3 标记-整理算法
标记过程任然与“标记-清除”算法的标记过程相同,但后续步骤不是对可回收对象进行直接清理,而是让所有活的对象都移动到另一端,然后直接清理掉端边界以外的内存,这种算法适合于老年代。
2.4 分代收集算法
把堆内存分为新生代和老年代,新生代又分为Eden区、From Survivor和To Survivor。一般新生代中的对象基本上都是朝生夕灭的,每次只有少量对象存活,因此采用复制算法,只需要复制那些少量存活的对象就可以完成垃圾收集;老年代中的对象存活率较高,就采用标记-清除和标记-整理算法来进行回收。
3 垃圾收集器
现在常见的垃圾收集器有如下几种
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:Serial Old、CMS、Parallel Old
堆内存垃圾收集器:G1
3.1 Serial收集器
Serial收集器是最基本、发展历史最悠久的收集器。这个收集器是单线程收集器,采用复制算法进行垃圾收集,它在完成垃圾收集工作时,必须暂停其他所有的工作线程,直到它收集结束。就好比“在打扫房间时你必须老老实实的在椅子上或者房间外待着,如果一边打扫,你一边乱扔纸屑,那么房间就打扫不完了。“,这确实是一个合乎情理的矛盾,虽然垃圾收集这项工作听起来和打扫房间属于一个性质,但实际上肯定是要比打扫房间复杂的多。
3.2 ParNew收集器
ParNew收集器实际上就是Serial收集器的多线程版本,但它却是许多运行在Server模式下虚拟机的首选新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器之外,目前只有ParNew收集器能与CMS收集器配合工作。
3.3 ParalleScavenge收集器
Paralle Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。他的特点是它的关注点与其他收集器不同,CMS收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目的是达到一个可控制的吞吐量(Throughout)。所谓吞吐量就是CPU运行用户代码的时间与CPU总消耗时间的比,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉了1分钟,那吞吐量就是99%。
3.4 SerialOld收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是给Client模式的虚拟机使用。如果在Server模式下,那么它有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用。
3.5 ParallelOld收集器
Parallel Old是Parallel Scavenge收集器的老年代版,使用多线程和“标记-整理”算法。
3.6 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其注重服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
整个垃圾收集过程分为4个步骤:
① 初始标记:标记一下GC Roots能直接关联到的对象,速度较快
② 并发标记:进行GC Roots Tracing,标记出全部的垃圾对象,耗时较长
③ 重新标记:修正并发标记阶段引用户程序继续运行而导致变化的对象的标记记录,耗时较短
④ 并发清除:用标记-清除算法清除垃圾对象,耗时较长
3.7 G1收集器
G1是一款面向服务器的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器