什么是JVM

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

JDK,JRE和JVM关系(JDK包含JRE包含JVM)

  • JDK:Java Develop Kit,Java的开发工具包,JDK本体也是Java程序,因此运行依赖于JRE,由于需要保持JDK的独立性与完整性,JDK的安装目录下通常也附有JRE。目前Oracle提供的Windows下的JDK安装工具会同时安装一个正常的JRE和隶属于JDK目录下的JRE。

  • JRE:Java Runtime Environment,也就是JVM的运行平台,联系平时用的虚拟机,大概可以理解成JRE=虚拟机平台+虚拟机本体(JVM)。类似于你电脑上的VMWare+适用于VMWare的Ubuntu虚拟机。这样我们也就明白了JVM到底是个什么。所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的java程序,安装JRE即可

  • JVM:Java Virtual Machine,Java虚拟机是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。

JVM结构

  • JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。
  • java编译器主要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。

JVM = 类加载器 classloader + 执行引擎 execution engine + 运行时数据区域 runtime data area
classloader 把硬盘上的class 文件加载到JVM中的运行时数据区域, 但是它不负责这个类文件能否执行,而这个是 执行引擎 负责的。





classloader类加载器

作用:装载.class文件,classloader 有两种装载class的方式 (时机):

  • 隐式:运行过程中,碰到new方式生成对象时,隐式调用classLoader到JVM
  • 显式:通过class.forname()动态加载

双亲委派模型(Parent Delegation Model)

类的加载过程采用双亲委托机制,这种机制能更好的保证 Java 平台的安全。
该模型要求除了顶层的Bootstrap class loader启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。每个类加载器都有自己的命名空间(由该加载器及所有父类加载器所加载的类组成,在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类)

双亲委派模型的工作过程为:

1.当前 ClassLoader 首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。

每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,
等下次加载的时候就可以直接返回了。

2.当前 classLoader 的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到 bootstrap ClassLoader.

3.当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

使用这种模型来组织类加载器之间的关系的好处:

主要是为了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String,同时也避免了重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就是不同的两个类,如果相互转型的话会抛java.lang.ClassCaseException.

类加载器 classloader 是具有层次结构的,也就是父子关系。其中,Bootstrap 是所有类加载器的父亲。如下图所示:

Bootstrap class loader: 父类
当运行 java 虚拟机时,这个类加载器被创建,它负责加载虚拟机的核心类库,如 java.lang.* 等。例如 java.lang.Object 就是由根类加载器加载的。需要注意的是,这个类加载器不是用 java 语言写的,而是用 C/C++ 写的。
Extension class loader:
这个加载器加载出了基本 API 之外的一些拓展类。
AppClass Loader:
加载应用程序和程序员自定义的类。

除了以上虚拟机自带的加载器以外,用户还可以定制自己的类加载器(User-defined Class Loader)。Java 提供了抽象类 java.lang.ClassLoader,所有用户自定义的类加载器应该继承 ClassLoader 类。
这是JVM分工自治生态系统的一个很好的体现。

execution engine 执行引擎

作用: 执行字节码,或者执行本地方法

runtime data area 运行时数据区

JVM 运行时数据区 (JVM Runtime Area) 其实就是指 JVM 在运行期间,其对JVM内存空间的划分和分配。JVM在运行时将数据划分为了6个区域来存储。
程序员写的所有程序都被加载到运行时数据区域中,不同类别存放在heap, java stack, native method stack, PC register, method area.

下面对各个部分的功能和存储的内容进行描述:
1、PC程序计数器:一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器, NAMELY存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。Java 的多线程机制离不开程序计数器,每个线程都有一个自己的PC,以便完成不同线程上下文环境的切换。

2、java虚拟机栈:与 PC 一样,java 虚拟机栈也是线程私有的。每一个 JVM 线程都有自己的 java 虚拟机栈,这个栈与线程同时创建,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

3、本地方法栈:与虚拟机栈的作用相似,虚拟机栈为虚拟机执行执行java方法服务,而本地方法栈则为虚拟机使用到的本地方法服务。

4、Java堆:被所有线程共享的一块存储区域,在虚拟机启动时创建,它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配。

Java堆在JVM启动的时候就被创建,堆中储存了各种对象,这些对象被自动管理内存系统(Automatic Storage Management System,也即是常说的 “Garbage Collector(垃圾回收器)”)所管理。这些对象无需、也无法显示地被销毁。

JVM将Heap分为两块:新生代New Generation和旧生代Old Generation

Note:

  • 堆在JVM是所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也是new开销比较大的原因。

  • 鉴于上面的原因,Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间,这块空间又称为TLAB

  • TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效

5、方法区
方法区和堆区域一样,是各个线程共享的内存区域,它用于存储每一个类的结构信息,例如运行时常量池,成员变量和方法数据,构造函数和普通函数的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。当开发人员在程序中通过Class对象中的getName、isInstance等方法获取信息时,这些数据都来自方法区。

方法区也是全局共享的,在虚拟机启动时候创建。在一定条件下它也会被GC。这块区域对应Permanent Generation 持久代。 XX:PermSize指定大小。

6、运行时常量池
其空间从方法区中分配,存放的为类中固定的常量信息、方法和域的引用信息

对象“已死”的判定算法

由于程序计数器、Java虚拟机栈、本地方法栈都是线程独享,其占用的内存也是随线程生而生、随线程结束而回收。而Java堆和方法区则不同,线程共享,是GC的所关注的部分。

在堆中几乎存在着所有对象,GC之前需要考虑哪些对象还活着不能回收,哪些对象已经死去可以回收。

有两种算法可以判定对象是否存活:

1.)引用计数算法:给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。但是它很难解决两个对象之间相互循环引用的情况。

2.)可达性分析算法:通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明此对象已死、可回收。Java中可以作为GC Roots的对象包括:虚拟机栈中引用的对象、本地方法栈中Native方法引用的对象、方法区静态属性引用的对象、方法区常量引用的对象。
在主流的商用程序语言(如我们的Java)的主流实现中,都是通过可达性分析算法来判定对象是否存活的。

何为GC?

垃圾回收机制是由垃圾收集器Garbage Collection GC来实现的,GC是后台的守护进程。它的特别之处是它是一个低优先级进程,但是可以根据内存的使用情况动态的调整他的优先级。因此,它是在内存中低到一定限度时才会自动运行,从而实现对内存的回收。这就是垃圾回收的时间不确定的原因。

为何要这样设计:因为GC也是进程,也要消耗CPU等资源,如果GC执行过于频繁会对java的程序的执行产生较大的影响(java解释器本来就不快),因此JVM的设计者们选着了不定期的gc。

GC有关的是: runtime data area 中的 heap(对象实例会存储在这里) 和 gabage collector方法。
程序运行期间,所有对象实例存储在运行时数据区域的heap中,当一个对象不再被引用(使用),它就需要被收回。在GC过程中,这些不再被使用的对象从heap中收回,这样就会有空间被循环利用。
GC为内存中不再使用的对象进行回收,GC中调用回收的方法--收集器garbage collector. 由于GC要消耗一些资源和时间,Java 在对对象的生命周期特征(eden or survivor)进行分析之后,采用了分代的方式进行对象的收集,以缩短GC对应用造成的暂停。

在垃圾回收器回收内存之前,还需要一些清理工作。
因为垃圾回收gc只能回收通过new关键字申请的内存(在堆上),但是堆上的内存并不完全是通过new申请分配的。还有一些本地方法(一般是调用的C方法)。这部分“特殊的内存”如果不手动释放,就会导致内存泄露,gc是无法回收这部分内存的。
所以需要在finalize中用本地方法(native method)如free操作等,再使用gc方法。显示的GC方法是system.gc()

垃圾回收技术

方法一:引用计数法。简单但速度很慢。缺陷是:不能处理循环引用的情况。
方法二:停止-复制(stop and copy)。效率低,需要的空间大,优点,不会产生碎片。
方法三:标记 - 清除算法 (mark and sweep)。速度较快,占用空间少,标记清除后会产生大量的碎片。

  • JAVA虚拟机中是如何做的?
    java的做法很聪明,我们称之为"自适应"的垃圾回收器,或者是"自适应的、分代的、停止-复制、标记-清扫"式垃圾回收器。它会根据不同的环境和需要选择不同的处理方式。

  • 分代收集算法
    当前商业虚拟机的GC都是采用分代收集算法,这种算法并没有什么新的思想,而是根据对象存活周期的不同将堆分为:新生代和老年代,方法区称为永久代(在新的版本中已经将永久代废弃,引入了元空间的概念,永久代使用的是JVM内存而元空间直接使用物理内存)。

这样就可以根据各个年代的特点采用不同的收集算法。



新生代中的对象“朝生夕死”,每次GC时都会有大量对象死去,少量存活,使用复制算法。新生代又分为Eden区和Survivor区(Survivor from、Survivor to),大小比例默认为8:1:1。

老年代中的对象因为对象存活率高、没有额外空间进行分配担保,就使用标记-清除或标记-整理算法。

新产生的对象优先进去Eden区,当Eden区满了之后再使用Survivor from,当Survivor from 也满了之后就进行Minor GC(新生代GC),将Eden和Survivor from中存活的对象copy进入Survivor to,然后清空Eden和Survivor from,这个时候原来的Survivor from成了新的Survivor to,原来的Survivor to成了新的Survivor from。复制的时候,如果Survivor to 无法容纳全部存活的对象,则根据老年代的分配担保(类似于银行的贷款担保)将对象copy进去老年代,如果老年代也无法容纳,则进行Full GC(老年代GC)。

大对象直接进入老年代:JVM中有个参数配置-XX:PretenureSizeThreshold,令大于这个设置值的对象直接进入老年代,目的是为了避免在Eden和Survivor区之间发生大量的内存复制。

长期存活的对象进入老年代:JVM给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将被移入Survivor并且年龄设定为1。每熬过一次Minor GC,年龄就加1,当他的年龄到一定程度(默认为15岁,可以通过XX:MaxTenuringThreshold来设定),就会移入老年代。但是JVM并不是永远要求年龄必须达到最大年龄才会晋升老年代,如果Survivor 空间中相同年龄(如年龄为x)所有对象大小的总和大于Survivor的一半,年龄大于等于x的所有对象直接进入老年代,无需等到最大年龄要求。

heap组成

由于GC需要消耗一些资源和时间的,Java在对对象的生命周期特征进行分析后,采用了分代的方式来进行对象的收集,即按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停.
heap 的组成有三区域/世代:(可以理解随着时间,对象实例不断变换heap中的等级,有点像年级)

  1. 新生代 Young Generation

    1. Eden Space 任何新进入运行时数据区域的实例都会存放在此

    2. S0 Suvivor Space 存在时间较长,经过垃圾回收没有被清除的实例,就从Eden 搬到了S0

    3. S1 Survivor Space 同理,存在时间更长的实例,就从S0 搬到了S1

  2. 旧生代 Old Generation/tenured
同理,存在时间更长的实例,对象多次回收没被清除,就从S1 搬到了tenured
  1. Perm 存放运行时数据区的方法区

Java 不同的世代使用不同的 GC 算法。

  1. Minor collection:
    新生代 Young Generation 使用将 Eden 还有 Survivor 内的数据利用 semi-space 做复制收集(Copying collection), 并将原本 Survivor 内经过多次垃圾收集仍然存活的对象移动到 Tenured。

  2. Major collection 则会进行 Minor collection,Tenured 世代则进行标记压缩收集。
 这个搬运工作都是GC 完成的,这也是garbage collector 的名字来源,而不是叫garbage cleaner. GC负责在heap中搬运实例,以及收回存储空间。

GC工作原理

JVM 分别对新生代和旧生代采用不同的垃圾回收机制

何为垃圾?

Java中那些不可达的对象就会变成垃圾。那么什么叫做不可达?其实就是没有办法再引用到该对象了。主要有以下情况使对象变为垃圾:
1.对非线程的对象来说,所有的活动线程都不能访问该对象,那么该对象就会变为垃圾。
2.对线程对象来说,满足上面的条件,且线程未启动或者已停止。

例如: 
(1)改变对象的引用,如置为null或者指向其他对象。 
   Object x=new Object();//object1 
   Object y=new Object();//object2 
   x=y;//object1 变为垃圾 
   x=y=null;//object2 变为垃圾 

(2)超出作用域 
   if(i==0){ 
      Object x=new Object();//object1 
   }//括号结束后object1将无法被引用,变为垃圾 
(3)类嵌套导致未完全释放 
   class A{ 
      A a; 
   } 
   A x= new A();//分配一个空间 
   x.a= new A();//又分配了一个空间 
   x=null;//将会产生两个垃圾 
(4)线程中的垃圾 
   class A implements Runnable{   
     void run(){ 
       //.... 
     } 
   } 
   //main 
   A x=new A();//object1 
   x.start(); 
   x=null;//等线程执行完后object1才被认定为垃圾 
   这样看,确实在代码执行过程中会产生很多垃圾,不过不用担心,java可以有效地处理他们。

JVM中将对象的引用分为了四种类型,不同的对象引用类型会造成GC采用不同的方法进行回收:
(1)强引用:默认情况下,对象采用的均为强引用
(GC不会回收)

(2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用

(只有在内存不够用的情况下才会被GC) 

(3)弱引用:在GC时一定会被GC回收
(4)虚引用:在GC时一定会被GC回收

GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停

(1)对新生代的对象的收集称为minor GC;

(2)对旧生代的对象的收集称为Full GC;

(3)程序中主动调用System.gc()强制执行的GC为Full GC。

不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:

(1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)

(2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)

(3)弱引用:在GC时一定会被GC回收

(4)虚引用:由于虚引用只是用来得知对象是否被GC


8:垃圾收集算法

1、标记-清除算法

最基础的算法,分标记和清除两个阶段:首先标记处所需要回收的对象,在标记完成后统一回收所有被标记的对象。

它有两点不足:一个效率问题,标记和清除过程都效率不高;一个是空间问题,标记清除之后会产生大量不连续的内存碎片(类似于我们电脑的磁盘碎片),空间碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

2、复制算法

为了解决效率问题,出现了“复制”算法,他将可用内存按容量划分为大小相等的两块,每次只需要使用其中一块。当一块内存用完了,将还存活的对象复制到另一块上面,然后再把刚刚用完的内存空间一次清理掉。这样就解决了内存碎片问题,但是代价就是可以用内容就缩小为原来的一半。

3、标记-整理算法

复制算法在对象存活率较高时就会进行频繁的复制操作,效率将降低。因此又有了标记-整理算法,标记过程同标记-清除算法,但是在后续步骤不是直接对对象进行清理,而是让所有存活的对象都向一侧移动,然后直接清理掉端边界以外的内存。

4、分代收集算法

当前商业虚拟机的GC都是采用分代收集算法,这种算法并没有什么新的思想,而是根据对象存活周期的不同将堆分为:新生代和老年代,方法区称为永久代(在新的版本中已经将永久代废弃,引入了元空间的概念,永久代使用的是JVM内存而元空间直接使用物理内存)。

这样就可以根据各个年代的特点采用不同的收集算法。


新生代中的对象“朝生夕死”,每次GC时都会有大量对象死去,少量存活,使用复制算法。新生代又分为Eden区和Survivor区(Survivor from、Survivor to),大小比例默认为8:1:1。

老年代中的对象因为对象存活率高、没有额外空间进行分配担保,就使用标记-清除或标记-整理算法。

新产生的对象优先进去Eden区,当Eden区满了之后再使用Survivor from,当Survivor from 也满了之后就进行Minor GC(新生代GC),将Eden和Survivor from中存活的对象copy进入Survivor to,然后清空Eden和Survivor from,这个时候原来的Survivor from成了新的Survivor to,原来的Survivor to成了新的Survivor from。复制的时候,如果Survivor to 无法容纳全部存活的对象,则根据老年代的分配担保(类似于银行的贷款担保)将对象copy进去老年代,如果老年代也无法容纳,则进行Full GC(老年代GC)。


大对象直接进入老年代:JVM中有个参数配置-XX:PretenureSizeThreshold,令大于这个设置值的对象直接进入老年代,目的是为了避免在Eden和Survivor区之间发生大量的内存复制。

长期存活的对象进入老年代:JVM给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将被移入Survivor并且年龄设定为1。没熬过一次Minor GC,年龄就加1,当他的年龄到一定程度(默认为15岁,可以通过XX:MaxTenuringThreshold来设定),就会移入老年代。但是JVM并不是永远要求年龄必须达到最大年龄才会晋升老年代,如果Survivor 空间中相同年龄(如年龄为x)所有对象大小的总和大于Survivor的一半,年龄大于等于x的所有对象直接进入老年代,无需等到最大年龄要求。


9:垃圾收集器

垃圾收集算法是方法论,垃圾收集器是具体实现。JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。

JDK7/8后,HotSpot虚拟机所有收集器及组合(连线)如下:


1.Serial收集器

Serial收集器是最基本、历史最久的收集器,曾是新生代手机的唯一选择。他是单线程的,只会使用一个CPU或一条收集线程去完成垃圾收集工作,并且它在收集的时候,必须暂停其他所有的工作线程,直到它结束,即“Stop the World”。停掉所有的用户线程,对很多应用来说难以接受。比如你在做一件事情,被别人强制停掉,你心里奔腾而过的“羊驼”还数的过来吗?

尽管如此,它仍然是虚拟机运行在client模式下的默认新生代收集器:简单而高效(与其他收集器的单个线程相比,因为没有线程切换的开销等)。

工作示意图:

2.ParNew收集器

ParNew收集器是Serial收集器的多线程版本,除了使用了多线程之外,其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同Serial收集器一样。

是许多运行在Server模式下的JVM中首选的新生代收集器,其中一个很重还要的原因就是除了Serial之外,只有他能和老年代的CMS收集器配合工作。

工作示意图:

3.Parallel Scavenge收集器

新生代收集器,并行的多线程收集器。它的目标是达到一个可控的吞吐量(就是CPU运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=行用户代码的时间/[行用户代码的时间+垃圾收集时间]),这样可以高效率的利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。

4.Serial Old收集器

Serial 收集器的老年代版本,单线程,“标记整理”算法,主要是给Client模式下的虚拟机使用。

另外还可以在Server模式下:

JDK 1.5之前的版本中雨Parallel Scavenge 收集器搭配使用

可以作为CMS的后背方案,在CMS发生Concurrent Mode Failure是使用

工作示意图:



5.Parallel Old收集器

Parallel Scavenge的老年代版本,多线程,“标记整理”算法,JDK 1.6才出现。在此之前Parallel Scavenge只能同Serial Old搭配使用,由于Serial Old的性能较差导致Parallel Scavenge的优势发挥不出来,尴了个尬~~

Parallel Old收集器的出现,使“吞吐量优先”收集器终于有了名副其实的组合。在吞吐量和CPU敏感的场合,都可以使用Parallel Scavenge/Parallel Old组合。组合的工作示意图如下:


6.CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。

基于“标记清除”算法,并发收集、低停顿,运作过程复杂,分4步:

1)初始标记:仅仅标记GC Roots能直接关联到的对象,速度快,但是需要“Stop The World”

2)并发标记:就是进行追踪引用链的过程,可以和用户线程并发执行。

3)重新标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要“Stop The World”

4)并发清除:清除标记为可以回收对象,可以和用户线程并发执行

由于整个过程耗时最长的并发标记和并发清除都可以和用户线程一起工作,所以总体上来看,CMS收集器的内存回收过程和用户线程是并发执行的。

工作示意图:

CSM收集器有3个缺点:

1)对CPU资源非常敏感

并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。

CMS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。

2)无法处理浮动垃圾(在并发清除时,用户线程新产生的垃圾叫浮动垃圾),可能出现"Concurrent Mode Failure"失败。

并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;

3)产生大量内存碎片:CMS基于"标记-清除"算法,清除后不进行压缩操作产生大量不连续的内存碎片,这样会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。


7.G1收集器

G1(Garbage-First)是JDK7-u4才正式推出商用的收集器。G1是面向服务端应用的垃圾收集器。它的使命是未来可以替换掉CMS收集器。

G1收集器特性:

并行与并发:能充分利用多CPU、多核环境的硬件优势,缩短停顿时间;能和用户线程并发执行。

分代收集:G1可以不需要其他GC收集器的配合就能独立管理整个堆,采用不同的方式处理新生对象和已经存活一段时间的对象。

空间整合:整体上看采用标记整理算法,局部看采用复制算法(两个Region之间),不会有内存碎片,不会因为大对象找不到足够的连续空间而提前触发GC,这点优于CMS收集器。

可预测的停顿:除了追求低停顿还能建立可以预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超N毫秒,这点优于CMS收集器。

为什么能做到可预测的停顿?

是因为可以有计划的避免在整个Java堆中进行全区域的垃圾收集。

G1收集器将内存分大小相等的独立区域(Region),新生代和老年代概念保留,但是已经不再物理隔离。

G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;

每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);

这就保证了在有限的时间内可以获取尽可能高的收集效率。

对象被其他Region的对象引用了怎么办?

判断对象存活时,是否需要扫描整个Java堆才能保证准确?在其他的分代收集器,也存在这样的问题(而G1更突出):新生代回收的时候不得不扫描老年代?
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
每个Region都有一个对应的Remembered Set;
每次Reference类型数据写操作时,都会产生一个Write Barrier 暂时中断操作;
然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的 Region(其他收集器:检查老年代对象是否引用了新生代对象);
如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
进行垃圾收集时,在GC根节点的枚举范围加入 Remembered Set ,就可以保证不进行全局扫描,也不会有遗漏。

不计算维护Remembered Set的操作,回收过程可以分为4个步骤(与CMS较为相似):

1)初始标记:仅仅标记GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时能在正确可用的Region中创建新对象,需要“Stop The World”

2)并发标记:从GC Roots开始进行可达性分析,找出存活对象,耗时长,可与用户线程并发执行

3)最终标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录。并发标记时虚拟机将对象变化记录在线程Remember Set Logs里面,最终标记阶段将Remember Set Logs整合到Remember Set中,比初始标记时间长但远比并发标记时间短,需要“Stop The World”

4)筛选回收:首先对各个Region的回收价值和成本进行排序,然后根据用户期望的GC停顿时间来定制回收计划,最后按计划回收一些价值高的Region中垃圾对象。回收时采用复制算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;可以并发进行,降低停顿时间,并增加吞吐量。

工作示意图: