java虚拟机-jvm内存回收算法篇

目录

第一章 走进java

1 java虚拟机发展史

1.1 Sun Classic/Exact VM(jdk1.0~jdk1.2)

1.2 Sun HotSpot VM

2 模块化

3 64位虚拟机

第二章 Java内存区域与内存溢出异常

2.1 概述

2.2运行时数据区域

2.3 Hotspot虚拟机

2.3.1 对象的创建

2.3.2 对象的内存区域

2.3.3 对象的访问定位

第三章 垃圾收集器与内存分配策略

3.1 概述

3.2 判断对象是否已死

3.2.1 引用计数算法

3.2.2 可达性分析算法

3.2.3 引用

3.2.4 生存还是死亡

3.2.5 回收方法区

3.3 垃圾收集算法

3.3.1 标记-清除算法(老年代回收算法)

3.3.2 复制算法(新生代回收算法)

3.3.3 标记-整理算法(老年代回收算法)

3.4 HotSpot的算法实现

3.5 垃圾收集器

3.5.1 Serial收集器

3.5.2 ParNew收集器

3.5.3 Parallel Scavenge收集器

3.5.4 Serial Old收集器

3.5.5 Parallel Old收集器

3.5.6 CMS收集器

3.5.7 G1收集器

第四章 虚拟机性能监控与故障处理工具

4.1 概述

4.2 JDK的命令行工具

4.2.1 jps:虚拟机进程状况工具

4.2.2 jstat:虚拟机统计信息监视工具

4.2.3 jinfo:Java配置信息工具


第一章 走进java

java的优点:摆脱了硬件平台的束缚,实现了“一次编写,到处运行”;它提供了一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄漏和指针越界问题;它实现了热点代码检测和运行时编译及优化,这使得java应用能随着运行时间的增加而获得更高的性能。

1 java虚拟机发展史

1.1 Sun Classic/Exact VM(jdk1.0~jdk1.2)

世界上第一款商用java虚拟机,它只能使用纯解释器方式来执行Java代码,如果要使用JIT编译器,就必须进行外挂,但是外挂JIT后,JIT编译器就会完全接管虚拟机的执行系统,解释器就不工作了。由于Classic VM不能和JIT配合工作,这就意味着如果要使用编译器执行,编译器就不得不对每一个方法、每一行代码都进行编译,而无论他们执行的频率是否具有编译的价值。基于程序响应时间的压力,这些编译器不敢用编译耗时稍高的优化技术 ,即使用了JIT,其执行效率也和C++有很大差距。

1.2 Sun HotSpot VM

HotSpot VM继承了Sun之前两款商用虚拟机优点,HotSpot指是的它的热点代码探测技术。HotSpot VM的热点代码探测技术可以通过执行计数器找出最有编译价值的代码,然后通知JIT编译器以方法为单位进行编译,如果一个方法被频繁调用,或方法中的有效循环次数很多,将会分别触发标准编译和OSR编译动作。通过编译与解释器恰当的协同工作,可以在最优化的程序响应时间和最佳执行性能取得平衡,而且无需等待本地代码输出才能执行程序,即时编译的时间压力也相对减小,这样有助于更多的代码优化技术秘输出质量更高的本地代码。

2 模块化

它是解决应用系统与技术平台越来越复杂、越来越庞大 的一个重要途径。

3 64位虚拟机

几年之前,java程序运行在64位虚拟机上需要付出比较大的额外代价:首先是内存问题,由于指针和各种数据类型对齐补白的原因,运行于64位系统上的java应用程序需要耗费更多的内存,通常要比32位系统额外增加10%~30%的内存消耗;其次,64位虚拟机性能也全面落后于32位。

 

第二章 Java内存区域与内存溢出异常

2.1 概述

对于java程序员来说,在虚拟机自动内存管理机制下,不再需要为每一个new操作,去写配对的delete/free代码,不容易出现内存泄漏和内存溢出问题,由java虚拟机管理内存。但是也正是java程序员把内存控制权交给了java虚拟机,一旦出现内存泄漏和溢出方面的问题,不了解虚拟机是怎样实用内存的,那么排查错误会是一项艰难的工作。

2.2运行时数据区域

java虚拟机-jvm内存回收算法篇_第1张图片

  • 程序计数器(线程私有内存区域): 程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条执行字节码指令。每条线程都有一个独立的程序计数器,独立存储,互不影响,因此这类内存区域被称为线程私有的内存。 如果执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址。如果是native方法,计数器为空。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。 
  • Java虚拟机栈(线程私有内存区域):与程序计数器一样,它也是线程私有的,它的生命周期和线程相同。同样是线程私有,描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法对应一个栈帧。在java虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,则会抛出OutOfMemoryError异常。

  • 本地方法栈(Native Method Stack):和Java虚拟机栈很类似,虚拟机栈为虚拟机执行java方法,不同的是本地方法栈为Native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError异常和OutOfMemoryError异常。

  • Java堆:是Java虚拟机所管理的内存中最大的一块。由所有线程共享,在虚拟机启动时创建。堆区唯一目的就是存放对象实例。堆中可细分为新生代和老年代,再细分可分为Eden空间、From Survivor空间、To Survivor空间。 堆无法扩展时,抛出OutOfMemoryError异常。

  • 方法区(Non-Heap):所有线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,抛出OutOfMemoryError.

  • 运行时常量池:它是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池(Const Pool Table),用于存放编译期生成的各种字面量和符号引用。并非预置入Class文件中常量池的内容才进入方法运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。当方法区无法满足内存分配需求时,抛出OutOfMemoryError。

  • 直接内存:并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。JDK1.4加入了NIO,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。因为避免了在Java堆和Native堆中来回复制数据,提高了性能。 当各个内存区域总和大于物理内存限制,抛出OutOfMemoryError异常。

2.3 Hotspot虚拟机

2.3.1 对象的创建

new  对象-------》常量池定位类引用---------》检查类引用和引用的类是否被加载、解析和初始化---------》没有,就先执行类加载。----》创建对象分配内存

类加载通过后----》虚拟机从java堆中分配内存---》内存规整(已分配的内存和未分配的内存区域由一个指针划分开):分配内存时把该指针移动一个对象大小的距离,成为指针碰撞;内存不规整时:从一个大的空闲列表区域分配内存。

分配内存时存在并发问题----》两种解决方法:1 对分配内存空间的动作进行同步处理(实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性);2 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,成为本地线程分配缓冲。

new指令执行时,对象的所有字段仍为零,需要等init方法执行后,字段才会赋予新值。

 

2.3.2 对象的内存区域

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

2.3.3 对象的访问定位

对象的访问定位也取决于具体的虚拟机实现。当我们在堆上创建一个对象实例后,就要通过虚拟机栈中的reference类型数据来操作堆上的对象。现在主流的访问方式有两种(HotSpot虚拟机采用的是第二种):

  1. 使用句柄访问对象。即reference中存储的是对象句柄的地址,而句柄中包含了对象实例数据与类型数据的具体地址信息,相当于二级指针。
  2. 直接指针访问对象。即reference中存储的就是对象地址,相当于一级指针

两种方式有各自的优缺点。当垃圾回收移动对象时,对于方式一而言,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址;而对于方式二,则需要修改reference中存储的地址。从访问效率上看,方式二优于方式一,因为方式二只进行了一次指针定位,节省了时间开销,而这也是HotSpot采用的实现方式

2.4 OutOfMemoryError异常

2.4.1 java堆溢出

2.4.2 虚拟机栈溢出和本地方法栈溢出

2.4.3 方法区和运行时常量池区

2.4.4 本机直接内存溢出

 

 

 

第三章 垃圾收集器与内存分配策略

3.1 概述

程序计数器、虚拟机栈、本地方法栈等3个区域随线程而生,随线程而灭,因为方法结束或线程结束时,内存自然就跟着回收了。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序出于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

 

3.2 判断对象是否已死

3.2.1 引用计数算法

算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1,当失效时,计数器就减1,任何时刻计数器为0的对象就是不可能再被用的。缺点:难以解决对象之间循环相互引用的问题。

3.2.2 可达性分析算法

在Java中,是通过可达性分析(Reachability Analysis)来判定对象是否存活的。该算法的基本思路就是通过一些被称为(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。

 

java虚拟机-jvm内存回收算法篇_第2张图片

在Java中,可作为GC Root的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

 

3.2.3 引用

在JDK1.2后,java对引用进行了扩充,将引用分为强引用、软引用、弱引用和虚引用。这四种引用强度依次减弱。

  • 强引用:是指创建一个对象并把这个对象赋给一个引用变量,比如:Object object =new Object(),强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
  • 软引用:用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在jdk1.2后,提供了SoftReference类来实现软引用。
  • 弱引用:它是用来描述非必须对象的,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2后,提供了WeakReference类来实现弱引用。
  • 虚引用(也称幽灵引用或者幻影引用):它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是在这个对象被垃圾回收器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

3.2.4 生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是非死不可的,这时候它们暂时处于缓刑阶段,要真正宣告一个对象的死亡,至少要经过两次标记阶段:

  • 如果对象在进行可达性分析后发现没有与GC Roots相连的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
  • 当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。

如果这个对想法被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫作F-Queue的队列中,并且稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里的‘执行’是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将可能导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----------只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出‘即将回收’的集合;如果对象这时候还没有逃脱,那么它就真的被回收了。例如:

public class FinalizeEscape {
	public static FinalizeEscape SAVE_HOOK = null;
	
	public void isAlive(){
		System.out.println(" i am alive");
	}

	@Override
	protected void finalize() throws Throwable {
		// TODO Auto-generated method stub
		super.finalize();
		System.out.println("finallize method exec");
		FinalizeEscape.SAVE_HOOK = this;
	}
	public static void main(String[] args) throws InterruptedException {
		SAVE_HOOK = new FinalizeEscape();
		
		SAVE_HOOK = null;
		System.gc();
		
		Thread.sleep(5000);
		
		if(SAVE_HOOK != null){
			SAVE_HOOK.isAlive();
		}else{
			System.out.println(" i am dead");
		}
		
		SAVE_HOOK = null;
		System.gc();
		Thread.sleep(5000);
		
		if(SAVE_HOOK != null){
			SAVE_HOOK.isAlive();
		}else{
			System.out.println(" i am dead");
		}
	}
	
	
}

输出:

java虚拟机-jvm内存回收算法篇_第3张图片

3.2.5 回收方法区

方法区是所有线程共享的一片内存区域。它存储的是已被JVM加载的类信息,常量,静态变量,编译器编译后的代码等数据。在JDK1.8以前的HotSpot虚拟机中,方法区也被称为永久代,1.8后被元空间取代。方法区称为永久代并不意味这进入方法区就永久存在,方法区也会发生内存回收,此区域的内存回收主要是针对常量池的回收以及对类型的卸载。我们已经知道,GC在进行垃圾回收之前,先要进行回收对象的判断(回收对象判断算法:引用计数法,可达性分析算法),然后再进行垃圾回收。

很多人认为方法区(或者Hotspot中的永久代)是没有垃圾收集的,jvm规范中确实说过可以不要求在方法区实现垃圾收集,而且在方法区进行垃圾收集的性价比比较低:在堆中,新生代的常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量和回收java堆中的对象非常相似。以常量池中字面量的回收为例,假如一个字符串“abc"已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这是发生内存回收,而且必要的话,这个“abc"常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

判断一个常量是否是废弃常量比较简单,而要判定一个类是否是无用的类的条件相对苛刻很多。类需要同时满足下面三个条件才能算是无用的类:

  • 该类的所有的实例都已经被回收,也就是java堆中不存在该类的任何实例;
  • 加载该类的ClassLoader被回收;
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

3.3 垃圾收集算法

JVM中的堆,一般分为三大部分:新生代、老年代、永久代。

当前商业虚拟机的垃圾收集都采用分代收集算法,这种算法是根据各个年代的特点采用最适当的收集算法。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大量对象死去,只有少量存活,那就选用复制算法,只需要付出少量的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用标记-清除或者标记-整理算法进行回收。

新生代主要是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代又分为 Eden区、ServivorFrom、ServivorTo三个区。当JVM无法为新建对象分配内存空间的时候(Eden满了),Minor GC被触发。因此新生代空间占用率越高,Minor GC越频繁。

MinorGC的过程:采用复制算法。

  1. 首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,一般是15,则赋值到老年代区)
  2. 同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区)
  3. 然后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。

java虚拟机-jvm内存回收算法篇_第4张图片

老年代的对象比较稳定,所以MajorGC不会频繁执行。

在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。

MajorGC采用标记—清除算法:

  1. 首先扫描一次所有老年代,标记出存活的对象
  2. 然后回收没有标记的对象。

MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。

当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。

永久代指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。

Class在被加载的时候被放入永久区域。它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。

在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中. 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。

  • Major GC和Full GC区别
  • Full GC:收集young gen、old gen、perm gen
  • Major GC:有时又叫old gc,只收集old gen

 

3.3.1 标记-清除算法(老年代回收算法)

标记-清除算法(最基础的算法)分为标记和清除两个阶段:首先标记处所有需要回收的对象,在标记完成之后统一回收所有被标记的对象,它的标记过程其实在前一节讲述对象标记判定时已经介绍过了。

缺点:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量的不连续的内存碎片,空间碎片较多时会导致以后在程序运行过程中分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

java虚拟机-jvm内存回收算法篇_第5张图片

3.3.2 复制算法(新生代回收算法)

为了解决效率问题,一种称为复制的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。

优点:实现简单,运行高效。

缺点:要牺牲内存为代价。

java虚拟机-jvm内存回收算法篇_第6张图片

 

3.3.3 标记-整理算法(老年代回收算法)

由于复制算法在对象存活率较高时存在大量的复制操作,效率降低,而且浪费空间,所以老年代一般不能选择这种算法。

根据老年代的特点,提出了标记-整理算法,标记过程与标记-清除算法一致,整理过程是先将存活的对象移到一端,然后将存活的对象边界外的内存清理。

java虚拟机-jvm内存回收算法篇_第7张图片

 

3.4 HotSpot的算法实现

3.5 垃圾收集器

3.5.1 Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器,曾经(jdk1.3.1之前)是虚拟机新生代收集的唯一选择。它是一个单线程的收集器,它使用一个CPU或一条收集线程完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

直到现在为止,它依然是虚拟机运行在CLient模式下的默认新生代垃圾收集器。

优点:简单而高效,由于运行在单个CPU的环境中,没有线程交互的开销,可以专心做垃圾收集从而获得最高的单线程收集效率。

缺点:有停顿时间,需要停止所有当前工作线程。

3.5.2 ParNew收集器

ParNew收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法和回收策略等和Serial收集器完全一样。

ParNew收集器是许多运行在Server模式下的虚拟机首选的新生代收集器,其中一个与性能无关的原因是,它能和CMS收集器配合工作。在JDK1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器-----------CMS收集器,这款收集器是HotSpot中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

ParNew收集器在Cpu环境中不会比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百的保证可以超越Serial收集器。当然随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境下可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

3.5.3 Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

Parallel Scavenge收集器的特点是它的关注点与其它收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,垃圾收集1分钟,那吞吐量就是99%。

Parallel Scavenge收集器拥有自适应的调节策略(GC Ergonomics),它会根据当前系统的运行状况动态调整停顿时间和吞吐量,这也是和ParNew收集器不同之处。

3.5.4 Serial Old收集器

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

3.5.5 Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。这个收集器是在JDK1.6才开始提供的,在此之前,新生代的Parallel Scavenge收集器处于比较尴尬的状态,因为新生代选择了Parallel Scavenge收集器,老年代必须选择Serial Old(因为Parallel Scavenge收集器无法与CMS收集器搭配使用)。Serial Old由于是单线程,在服务端性能是拖累,因此即使使用了Parallel Scavenge收集器也未必能获得吞吐量最大化的效果,而且单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS组合的能力。

3.5.6 CMS收集器

 CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。主要应用在服务端,以提供较短的响应时间。它是基于标记-清除算法实现。整个过程分为4个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

其中初始标记和重新标记依然会有停顿时间(Stop the world)。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC Roots Tracing的过程,而重新标记则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

CMS优点:并发收集,低停顿。

缺点:

  • CMS收集器对CPU资源非常敏感。在CMS中并发阶段,它依赖CPU资源,若CPU资源较少时,CMS会消耗掉比较多的一部分CPU资源,使得程序的执行速度变慢。
  • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户程序还在运行,就会产生新的垃圾,这一部分垃圾出现在标记过程后,只好留到下一次GC时清理,这一部分垃圾成为
    "浮动垃圾"。
  • CMS收集器使用的时标记-清除算法,所以会产生内存碎片,导致大对象申请内存时,无法找到足够大的连续空间来分配对象,不得不提前触发Full GC。

3.5.7 G1收集器

为解决CMS算法产生空间碎片和其它一系列的问题缺陷,HotSpot提供了另外一种垃圾回收策略,G1(Garbage First)算法,通过参数-XX:+UseG1GC来启用,该算法在JDK 7u4版本被正式推出,官网对此描述如下:

G1垃圾收集算法主要应用在多CPU大内存的服务中,在满足高吞吐量的同时,竟可能的满足垃圾回收时的暂停时间,该设计主要针对如下应用场景:

  • 垃圾收集线程和应用线程并发执行,和CMS一样
  • 空闲内存压缩时避免冗长的暂停时间
  • 应用需要更多可预测的GC暂停时间
  • 不希望牺牲太多的吞吐性能
  • 不需要很大的Java堆 (翻译的有点虚,多大才算大?)

第四章 虚拟机性能监控与故障处理工具

4.1 概述

了解了虚拟机内存分配与回收技术的介绍,再根据从实践的角度去了解虚拟机内存管理的世界。

给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用处理数据的手段。这里说的数据包括:运行日志、异常堆栈、GC日志、线程快照(threaddump/javacore文件)、堆转储快照(heapdump/hprof文件)等。经常使用适当的虚拟机监控和分析的工具可以加快我们分析数据、定位解决问题的速度,但在学习工具前,也应当意识到工具永远都是知识技能的一层包装,没有什么工具是秘密武器。

4.2 JDK的命令行工具

 Sun jdk监控和故障处理工具

名称 主要作用
jps JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程
jstat JVM Statistics Monitoring Tool,用于手机HotSpot虚拟机各方面的运行数据
jinfo Configuration Info for Java,显示虚拟机配置信息
jhat JVM Heap Dump Browser,用于分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果
jmap Memory Map for Java,生成虚拟机的内存转储快照(heapdump文件)
jstack Stack Trace for Java,显示虚拟机的线程快照

4.2.1 jps:虚拟机进程状况工具

jps [option] [hostid]

-q    只输出LVMID,省略主类的名称

-m   输出虚拟机进程启动时传递给主类main()函数的参数

-l     输出主类的全名,如果进程执行的是Jar包,输出Jar路径

-v    输出虚拟机进程启动JVM参数

4.2.2 jstat:虚拟机统计信息监视工具

jstat [option vmid [interval[s|ms] [count] ]

-class            监视类装载、卸载数量、总空间以及类装载所耗费的时间

-gc                 监视java堆状况,包括Eden区、两个suivivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息

-gccapacity    监视内容与gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间

-gcutil            监视内容与gc基本相同,但输出主要关注已使用空间占总空间的百分比

-gccause        与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因

-gcnew           监视新生代GC状况

 

4.2.3 jinfo:Java配置信息工具

jinfo [option] pid

 

4.2.4 jmap:Java内存映像工具

jmap [option] vmid

 

你可能感兴趣的:(java)