JVM的理解(入门篇)

JVM(Java Virtual Machine)java虚拟机,它是java运行机制的核心和基础,它是运行在操作系统之上的一个虚拟处理器,就是这个原因,所以才使得java可以跨平台运行。

JVM的理解(入门篇)

一、数据区详解

1.程序计数器

程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令,分支,循环,跳转,异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java方法,买这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native,这个计数器值则为空。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2.Java虚拟机栈

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

经常有人把Java内存分为“堆”内存区和“栈”内存区,其实这样分是比较粗糙的,Java内存的划分远远比这复杂,这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。

局部变量表存放了编译期可知的各种基本数据类型(boolean、-byte、char..... shoft、int、float、Jong、double.以及对象引用(reference类型,它不每同于对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

电64位长度的long和double类型的数据会占用2个全局部变量空间(Slot)其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这全方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运街期间不会改变局部 变垂表的大小。

在Java虚拟机规抱中,对这个区域规定于两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可以动态扩展)只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemmoryError异常。

3.本地方法栈

本地方法栈和虚拟机栈发挥的作用是相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是虚拟机执行的Native(本地)方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以*实现它。甚至有的虚拟机(譬如 Sun HotSpot虚拟机〉直接就把本地方法找和虚拟机栈合二为一。与虚拟机栈一样·,本地方法栈区域也会抛出*Error和OutOfMemoryError异常。

4.Java堆

对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例和数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作“GC”堆,(Garbage Collected Heap) 从内存回收的角度来看,由于现在收集器基本都采用分带收集算法,所以Java堆中还可以细分为:新生代和老年代。再细致一点的右Eden空间、From Survivor空间、ToSurvivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。不过无论如何划分,斗鱼存放内容无关,无论哪个区域,存储的都仍然是对象示例,进一步换分的目的是为了更好的回收内存,或者更快的分配内存。

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时既可以实现固定大小的,也可以是可扩展的,不过当前主流的徐和你急都是按照可扩展来实现的。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

5.方法区

方法区和Java堆一样,是各个线程共享的内存区域,他用与存储已被虚拟机加载的类信息,常量,静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

对于习惯在HotSpot虚拟机上开发、部署程序的开发者来说,很多人都更愿意吧方法区成为“永久代”,本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择吧GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他虚拟机来说是不存在永久代的概念的。原则上,如何实现方法区属于虚拟机实现细节,不受虚拟机规范约束,但使用永久代来实现方法区,现在看来不是一个好主意,因为这样更容易遇到内存溢出问题。

6.运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

Java虚拟机对Class文件每一部分的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行、但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过一般来说,处理保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量相对于 Class 文件常量池的另外一个重要特征是具备动态性, Java 语言并 不要求常量一定只有编译期才能产生, 也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池, 运行期间也可能将新的常量放入池中, 这种特性被开发人员利用得比较多的便是 String 类的 intern()方法。

既然运行时常量池是方法区的一部分, 向然受到方法区内存的限制, 当常量?也无法再申请到内存时会抛出 OutOfMemoryError 异常。

7.直接内存

直接内存( Direct Memory)并不是虚拟机运行时数据区的一部分, 也不是 Java 虚拟机规范中定义的内存区域。 但是这部分内存也被频繁地使用, 而且也可能导致 OutOfMemoryError 异常出现, 所以我们放到这里一起讲解。
在 JDK 1.4 中新加入了 NIO (New Input/Output )类, 号|入了一种基于通道(Channel)与缓冲区C Buffer)的 1/0 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后通过一 个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了在 Java 堆和 Native 堆中来回复制数据。

显然, 本机直接内存的分配不会受到 Java 堆大小的限制, 但是, 既然是内存, 肯定还是 会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限 制。 服务糯管理员在配置虚拟机参数时, 会根据实际内存设置-Xmx等参数信息, 但经常忽略直接内存, 使得各个内存区域总和I大于物理内存限制(包括物理的和操作系统级的限制〉,从而导致动态扩展时出现 OutOfMemoryError 异常。

二、对象的创建(堆数据区内存分配方式)

Java是一门面向对象的编程语言,在Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个new关键字而已,而 在虚拟机中,对象(文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创 建又是怎样一个过程呢?

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一 个类的符号引用,并且.检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,本书第7章将探讨这部分内容的细节。

堆数据区内存分配的方式:指针碰撞法和空闲列表法

指针碰撞法:

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于 把一块确定大小的内存从Java堆中划分出来。假设Java堆中内存是绝对规擎的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所 分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。

空闲列表法:

如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰攘了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象 实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。

如何选择内存分配的方式

选择哪种分配方 式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃摄收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全 的,可能出现正在给对象A分配内存,指针还没来得及修改,对象8又同时使用了原来的指 针来分配内存的情况。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处

如何保证内存的分配是线程安全的

注意:在内存分配的时候需要考虑线程安全的问题?

除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全 的,可能出现正在给对象A分配内存,指针还没来得及修改,对象8又同时使用了原来的指 针来分配内存的情况。

解决这个问题有两种方案。

一种是对分配内存空间的动作进行同步处理

实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。

另一种是把内存分配的动作按照线程划分在不同的空间之中进行。

即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer TLAB)哪个线程要分配内存, 就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型,所对应的零值。

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头cObject Header)之中。根据虚指tJt当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

在上述工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建在刚刚开始<init>方法还没有执行,所有的字段都还为零。所以一般来说(由字节码中是否跟随invokespecial指令所决定)执行new指令之后会接着 执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

三、对象的内存布局

在HotSpot虚拟机中, 对象在内存中存储的布局可以分为3块区域: 

对象头(Header)、实例数据( Instance Data)和对齐填充(Padding)。

(1)对象头

HotSpot虚拟机的对象头包括两部分信息,

第一部分用于存储对象肉身的运行时数据,如哈希码( HashCode)、GC分代年龄、 锁状态标志、 线程持有的锁、 偏向钱程ID、 偏向时间戳等 , 这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit, 官方称它为 “Mark Word”对象需要存储的运行时数据很多, 其实已经超出了32位 、 64位Bitmap结构所能记录的限度, 但是对象头信息是与对象自身定义的数据无关的额外存 储成本, 考虑到虚拟机的空间效率, Mark Word 被设计成一个非固定的数据结构以便在极小 的空间内存储尽量多的信息, 它会根据对象的状态复用自己的存储空间。 例如, 在32位的 HotSpot虚拟机中, 如果对象处于未被锁定的状态下, 那么Mark Word的32bit空间中的25bit 用于存储对象哈希码,4bit用于存储对象分代年龄,2bit肘子存储锁标志位, 1bit固定为0,而在其他状态(轻量级锁定、 重量级锁定、 GC标记、 可偏向〉下对象的存储内容见表2-1 。

存储内容 标志位  状态
对象哈希码、对象分代年龄 01  未锁定
指向锁记录的指针 00  轻量级锁定
指向重量级锁的指针 11  膨胀(重量级锁定)
空,不需要记录信息 01  GC标记

偏向线程ID、偏向时间戳、对象分代年龄

   可偏向

对象头的另外一部分是类型数据指针, 即对象指向它的类元数据的指针, 虚拟机通过这个指 针来确定这个对象是哪个类的实例。 并不是所有的虚拟机实现都必须在对象数据上保留类型指针, 换句话说, 查找对象的元数据信息并不一定要经过对象本身 ,另外, 如果对象是一个Java数组, 那在对象头中还必须有一块用于记录数组长度的数据, 因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小, 但是从数组的元数据 中却无法确定数组的大小。

(2)实例数据

实例数据才是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops,从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那么子类之中较窄的变量也可能会插入到父类变量的空隙之中。

(3)对齐填充

这个部分不是必然存在的,也没有特别的含义,他仅仅起着占位符的作用。由于HotSpot VM 的自动内存管理系统要求对象起始地址必须是8字节的倍数,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数 (1倍或者2倍)因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

四、对象的访问定位

建立对象是为了使用对象,我们的Java程序需要通过栈上的reference(引用)数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。

目前主流的访问方式有使用句柄和直接指针两种。

(1)如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如图:

(2)如果采用直接指针的方式访问对象,则虚拟机栈中的引用存放的就是对象在堆中的首地址,包含实例数据和类型数据指针

由图可以看出类型数据存放在方法区,堆内存中只存放了类型数据的指针,用来指向方法区中该对象的类型数据。

两种访问对象的方式比较下,我们可以看出,直接指针的方式少了一个指针映射,在不断进行对象创建对象定位的堆内存中,可以节省很多的指针定位开销,所以相比而言直接指针法比较好。

五、内存回收

内存回收包括虚拟机栈和本地方法栈的内存回收,和堆区以及方法区的内存回收。

由于虚拟机栈和本地方法栈,内存的申请和内存的释放是以出栈和入栈来进行的,所以内存的回收随着出栈的操作就可以完成,不要额外的垃圾回收器,所以内存的回收主要是堆和方法区的垃圾回收。

(1)堆内存的垃圾回收

在垃圾回收之前我们需要判断哪些对象是需要被回收的,哪些不用回收。

因此在这里我们讲述下,判断对象是否死亡的两种方式:引用计数法和可达性分析法

对象计数法:就是给每个对象配置一个引用计数器,每当有一个指向该对象的引用生成,这个计数器的值就增加1,每当有一个指向该对象的引用失效,这个计数器的值就减少1。当这个计数器的值为零的时候,就说明指向该对象的引用全部失效,这个时候我们就可以判定该对象是死亡状态了。但是这个方法有一个问题,它无法解决对象循环引用的情况。

比如有两个对象ObjectA 和ObjectB 每个对象中都有字段instance 假如ObjectA.instance = ObjectB ,ObjectB.instance = ObjectA。这样的话两个对象都有被另一个对象所引用,这样的话如果采用这种方式来判断对象是否死亡,我想永远不可能对它进行垃圾回收。

可达性分析法:这个方式是通过一系列的叫做“GC Root”的对象作为起始点,从这个对象向下搜索,搜索所经过的路径被称为引用链,假如一个对象到“GC Root”没有任何引用链存在,那么就说明这个对象无用的。


以上两种方式只是作为判断对象是否死亡的基本条件,但是并不一定说被上面的方式判断为死亡了,真正判断一个对象死亡,要经过两次标记,判断无引用链只是第一步标记,第二次标记就是对象调用finalize()方法,如果对象调用finalize()方法后依然没有引用链,那么就可以判断为这个对象为死亡状态了。这个调用finalize()方法的方式,其实可以说是对象的一次救赎,如果在救赎的过程中没有成功,那么才会宣告对象死亡。

垃圾回收算法

标记-清除算法

标记好可以回收的对象,然后统一进行回收。

缺点:统一标记,会使垃圾回收的开销很大,并且这个方式的垃圾回收的效率不高。


标记-整理算法

第一步和标记-清除算法一样,不同的是第二步,第二步将可用对象向一端移动,然后清理掉端边缘部分。


复制算法

再分配内存的时候,一下子同时分配两个完全相等的内存,当一块内存不够用了,就将可用对象复制到另一个内存块中,然后清除掉不够用的那个内存块。

分代算法

将内存分为新生代和老年代

新生代大部分都是不可用的对象,只有极少数的可用对象,所以采用复制算法,复制可用对象的开销比较小,所以新生代采用复制算法,老年代采用标记-清楚,或者标记-整理算法。


六、垃圾回收器

(1)Serial垃圾回收器

这个垃圾回收器是一个单线程的垃圾回收器。不是说它只是使用一个cpu或者只是一个线程去垃圾收集。

更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束."Stop The World”这个名字也许听起来很醋,但这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的钱程全部停掉,这对很多应用来说都是难以接受的。

(2)ParNew收集器

Par New收集器真实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集 之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、 -XX: P retenureSize Threshold、-XX:HandlePromotionFai lure等)、收集算法、StopThe World、 对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了 相当多的代码。

(3)Parallel Scavenge

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器……看上去和ParNew都一样,那它有什么特别之处呢?

Parallel Scavenge收集器的特点是它的关谊点与其他收集器不同,CMS等收集器的 关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而ParallelScavenge收集器的目标则是达到一个可控制的吞吐量CThroughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+ 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是 99%。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高 吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集
停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回 收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使 得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾 收集发生得更频繁一些,原米10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、 每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
GCTimeRatio参数的值应当是一个大于0且小于JOO的整数,也就是垃极收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占 总时间的5%(即IIC 1+19)),默认值为99,就是允许最大1%(即II ( I +99))的垃圾收 集时间。

由于与吞吐量关系密切,ParallelScavenge收集部也经常称为“吞吐量优先”收集器。 除上述两个参敏之外,ParallelScavenge收集器还有一个参数-XX :+UseAdapti veSizePolicy 值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例C-XX:SurvivorRatio)晋升老年代对象年龄等细节参数了。

(4)Serial Old

Serial Old 是 Serial 收集器的老年代版本, 它同样是一个单线程收集器, 使用 “ 标记一 整理 ” 算法。 这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。 如果在 Server 樵式下, 那么它主要还有两大用途: 一种用途是在 JDK M以及之前的版本中与 Parallel Scavenge 收集器搭配使用气另一种用途就是作为 CMS 收集器的后备预案, 在并发收集发生 Concurrent Mode Failure 时使用。

(5)ParNew Old

Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 使用多钱程和 “标记-整理” 算 法。 这个收集器是在 JDK 1.6 中才开始提供的, 在此之前, 新生代的 Parallel Scavenge 收集 11i-直处于比较尴尬的状态。 原因是, 如果新生代选择了 Parallel Scavenge 收集器, 老年代除了Serial Old ( PS MarkSweep)收集器外别无选择(还记得上面说过Parallel Scavenge收集 却无法与CMS收集器配合工作吗?)。由于老年代Serial Old收集器在服务端应用性能上的 “拖累 ”, 使用了Parallel Scavenge收集器也米必能在整体应用上获得吞吐量最大化的效果, 由于单线程的老年代收集巾无法充分利用服务器多CPU A<J处理能力,在老年代很大丽且硬件比较高级的环挠中 , 这种组合的吞吐量甚至还不一定有ParNew加CMS的组合 “ 给力飞
直到Parallel Old收集器出现后, “ 吞吐量优先” 收集器终于有了比较名副其实的应用组 合,在在重平干-吐量以及CPU资漉敏感的场合,都可以优先王军’虑Parallel Scavenge加Parallel Old收集器。

(6)CMS收集器

CMS" ( Concurrent Mark- Sweep)收集器是一种以获取最矫回收停顿时间为目标的收集器。 陆前很大一部分胁Java