JVM梳理

注:你在网上所看到的大部分内容(包括本文)均来自于《深入理解Java虚拟机》周志明著,注意我这里用了著字,所有内容均来自于自己,而编著一般是指来自于引用别人的,大家以后买书可以以此来辨别下作者的优秀程度。

JVM内存区域图

在这里插入图片描述上图中的方法区(也就是永久代)在1.8之后的HotSpot虚拟机上移除了,原先我们常说String.intern()把字符串会放在运行期常量池中,现在也不放入了,而是又像普通的一样,放到的堆里面。方法区的数据被移动到了叫做元数据(Metaspace\color{#FF0000}{Metaspace})的一块区域里面。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

再来看看垃圾回收(回收堆的内存)

另外方法区真的不需要回收吗?一般不用,但是在频繁使用反射,动态代理,CGLIB等字节码操作框架,以及动态生成JSP以及OSGI这类频繁自定义ClassLoader的长江下,需要虚拟机回收类的功能。

如何判断该不该回收一个对象呢?

  • 引用计数算法
    众所周知,虽然使用简单,但是这个方法存在循环引用的问题。
  • 可达性分析算法
    计算从GCRoots往下遍历,看这个对象是否被引用。一般GCRoots包含以下几种:
    • 虚拟机栈(栈中的本地变量表)中引用的对象
    • 方法区中静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中引用的对象

引出来了一个问题,对象只有对引用和没被引用两种关系吗?万一有些对象想表达下中立态度呢?于是编出来了强软弱虚引用。

我们基于上面的可达性分析算法进而延展出来了垃圾收集算法

  • 标记-清除算法

算法流程分为标记和清除两个阶段,简单易懂,但是会出现很多内存碎片,但是运行很快。

  • 标记-复制算法

算法流程分为标记和复制两个阶段,没被标记的需要被复制到另外的一部分区域,然后整体清除原先的区域,不会造成内存碎片,但是总使需要浪费一部分内存。运行稍慢,不会出现内存碎片,但是比标记整理快。

  • 标记-整理算法

先标记,然后存货对象都向一段移动,然后清理掉端边界以外的内存。栈指针需要挨个计算更换指向的内存区域。所以说没有绝对好的算法,总是有优劣。

算法已经全了,那么我们要基于这些算法去实现一个垃圾回收器了。在这之前,我们再看下堆内存更加详细的分布。

堆内存的更加详细划分

在这里插入图片描述
新生代这样划分是为了更好的管理堆内存中的对象,方便GC算法–>“复制算法”来进行垃圾回收。

JVM每次只会使用Eden和其中一块survivor来为对象服务,所以无论什么时候,都会有一块survivor空间,因此新生代实际可用空间为90%。

新生代GC(minor gc):指发生在新生代的垃圾回收动作,因为Java对象大多数都是“朝生夕死”的特性,所以minor GC非常频繁,使用复制算法快速的回收。

新生代几乎是所有Java对象出生的地方,Java对象申请的内存和存放都是在这个地方。当对象在Eden(包括一个survivor,假如是from),当此对象经过一次minor GC后仍然存活,并且能够被另一块survivor所容纳(这里的survivor则是to),则使用复制算法将这些仍然存活的对象复制到to survivor区域中,然后清理掉Eden和from survivor区域,并将这些存活的对象年龄+1,以后对象在survivor中每熬过一次则+1,当达到某个值(默认为15),这些对象会成为老年代!

事情不是绝对,有些较大的对象(需要分配连续的内存空间),则直接进入老年代。

再来看看各种垃圾回收器

Serial收集器

新生代采用标记复制,老年代采用标记整理算法,单线程处理。会导致收集的时候停止响应。但是在桌面client端依旧好用,因为简单有效。

ParNew收集器

Serial收集器的多线程版本(新生代),老年代还是还是单线程。因此在server端一般只用于手机新生代,老年代用其他收集器收集。最大好处是只有它能够配合CMS收集器,他一个负责新生代,CMS一个负责老年代

Parallel Scavenge收集器

新生代收集器,依旧使用标记复制算法,和ParNew一样。它存在的原因是因为它并不是为了垃圾回收的时间,而是为了达到一个可控的吞吐量。设计他的参数会影响到新生代老年代的内存大小分配。一般高吞吐量的服务用到这个垃圾回收器。无法与CMS老年代收集器配合使用,能和Serial Old和Parallel Old收集器配合。

Parallel Old收集器

是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法,这个收集器是JDK1.6之后出现的,就是为了用在服务端配合Parallel Scavenge收集器使用的,利用服务端的多核优势。

Serial Old收集器

是一个老年代收集器,主要适用于老年代收集,供client端使用。如果用在server端,一般是为了配合Parallel Scavenge老年代收集器(1.5之前),或者是用于CMS收集器的后备方案。

CMS(Concurrent Mark Sweep)收集器

这是一个专用的老年代收集器,是一种以获取最短回收停顿为目标的收集器。是基于标记清除的算法实现的(就是为了快)。但是真正的实现很复杂,有初始标记,并发标记(耗时长),重新标记(耗时长),并发清除四个阶段。运行的时候不会导致用户线程停顿,但是会占用一部分CPU让用户线程变慢,总吞吐量降低。因为使用标记清除算法,所以比较容易出现FullGC。

G1收集器

新生代我们一般使用:Serial,ParNew,Parallel Scavenge
老年代一般使用:CMS,Serial Old,Parallel Old
G1收集器都使用,然后存在于实验阶段。

所以说大部分我们都是用ParNew+CMS收集器的选择。
如果追求吞吐量,可以选择Parallel Scavenge+Parallel Old收集器

什么是FullGC

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生代即方法区的回收(JDK8中无永生带了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。Major GC的速度一般会比Minor GC慢10倍以上。

Using the -XX flags for our collectors for jdk6,

UseSerialGC is “Serial” + “Serial Old”
UseParNewGC is “ParNew” + “Serial Old”
UseConcMarkSweepGC is “ParNew” + “CMS” + “Serial Old”. “CMS” is used most of the time to collect the tenured generation. “Serial Old” is used when a concurrent mode failure occurs.
UseParallelGC is “Parallel Scavenge” + “Serial Old”
UseParallelOldGC is “Parallel Scavenge” + “Parallel Old”

上面介绍的很简单,实际上在HotSpot VM内因为历史原因情况稍微复杂一些。

HotSpot VM里多个GC有部分共享的代码。有一个分代式GC框架,Serial/Serial Old/ParNew/CMS都在这个框架内;在该框架内的young collector和old collector可以任意搭配使用,所谓的“mix-and-match”。
而ParallelScavenge与G1则不在这个框架内,而是各自采用了自己特别的框架。这是因为新的GC实现时发现原本的分代式GC框架用起来不顺手。请参考官方文档的Collector Styles一段。

ParallelScavenge(PS)的young collector就如其名字所示,是并行的拷贝式收集器。本来这个young collector就是“Parallel Scavenge”所指,但因为它不兼容原本的分代式GC框架,为了凸显出它是不同的,所以它的young collector带上了PS前缀,全名变成PS Scavenge。对应的,它的old collector的名字也带上了PS前缀,叫做PS MarkSweep。
这个PS MarkSweep默认的实现实际上是一层皮,它底下真正做mark-sweep-compact工作的代码是跟分代式GC框架里的serial old(这个collector名字叫做MarkSweepCompact)是共用同一份代码的。也就是说实际上PS MarkSweep与MarkSweepCompact在HotSpot VM里是同一个collector实现,包了两张不同的皮;这个collector是串行的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章