JVM是每个JAVA开发都会听说了解过的东西,相关知识有一部分也是需要深入了解的,去年刚好有机会一次团队内部技术分享,就做了这方面的一些准备,简单了解了JVM的垃圾回收机制。
JVM虚拟机架构
上面是JVM架构,简单说说数据区每一部分的作用:
Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
方法区(Method Area),与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
程序计数器(Program Counter Register),程序计数器(Program CounterRegister)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual MachineStacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(StackFrame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
本篇主要所提及的垃圾回收机制,主要是针对JAVA堆的。
在讨论垃圾回收机制之前,需要先知道JVM会回收哪些对象?
简单来说,会被GC掉的对象是: 不是GC roots并且没有被GC roots引用的对象。
这里不讨论哪些对象、引用可以成为GC roots,如感兴趣可以自行深入了解一下。
GC 算法
标记-清除算法
算法分为“标记”和“清除”两个阶段:标记阶段:找到所有可访问的对象,做个标记 ,清除阶段:遍历堆,把未被标记的对象回收。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。
标记-清除它的主要缺点有两个:
一个是效率问题,标记和清除过程的效率都不高;
另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制(Copying)的收集算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
复制(Copying)的收集算法该算法使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。并且该算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
标记-整理算法
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
标记-整理“分代收集”(GenerationalCollection)算法
把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或“标记-整理”算法来进行回收。
GC收集器
Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
ParNew其实就是serial的多线程版本,ParNew在单线程的情况下甚至不如Serial,ParNew是除了serial之外唯一能和CMS配合的。
Parallel Scavenge收集器和ParNew收集器类似,是一个新生代收集器。使用复制算法的并行多线程收集器。
主要适应主要适合在后台运算而不需要太多交互的任务。比如需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务等。
CMS收集过程CMS算法主要分为四个步骤:
a. 暂停所有应用线程(stop-the-word)并且标记能直接从roots节点到达的节点,虽然会造成STW,由于只扫描全局变量、栈等根节点直接到达的对象,这一步会非常快。
b.恢复应用线程,并且开启gc线程从第一个步骤中扫描到的节点开始进行深度遍历并标记,这个过程GC Thread和应用线程是同时进行的,当应用进程改变了某个对象的状态时会把当前对象所在的page标记为dirty,new的object所在的page也标记为dirty,深度遍历完整个对象图。
c.暂停所有应用进程并扫描roots以及标记为dirty page所在的区域,这个步骤也会暂停所有应用,由于只扫描roots以及dirty page,因此暂停时间比较短。
d.恢复应用进程并对内存进行回收。
图中C虽然未被GC roots引用,但依旧被标记出来。是因为不希望在过程中再次造成dirty page。因此C对象会在下次GC时被回收
(此处page如何分页,暂时没有找到相关资料。)
总之CMS将stop-the-world的时间降到最低,能给电商网站用户带来最好的体验。
尽管CMS的GC线程对CPU的占用率会比较高,但在多核的服务器上还是展现了优越的特性,目前也被部署在国内的各大电商网站上。
下面列出七个基本的收集器:
Serial(串行GC)标记-复制
Serial Old(MSC)(串行GC)标记-整理
ParNew(并行GC)标记-复制
Parallel Scavenge(并行回收GC)标记-复制
Parallel Old(并行GC)标记-整理
CMS(并发GC)标记-清除
G1(JDK1.7+) (该收集器没有过多了解,性能据说是相对较好的,先留个坑之后填上。
GC收集器组合有关GC的调优
我没有工程上的具体调优经验,不过了解的是:GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
最后附上该篇文章大概的脑图
网友评论