深入学习Java虚拟机笔记

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


前言

本文内容摘抄自 周志明 著 的深入理解Java虚拟机(第二版) 一书
深入理解Java虚拟机(第二版)看完一遍之后 收获颇丰,
但也感到自己的不足,很多内容也一知半解,总不能一看了之,一知半解
想着不如以这笔记的形式 将以归纳总结
当然其中也受到了其他大博主 文章的触动和启发
但别人整理的 和自己理解的也有很大出入 或者 重点不一样 所以想着自己整理一遍
加强理解,督促自己学习(实话 真的要耐住性子 慢慢啃)

随着2019年第三版的发版,又开始从头读一遍,毕竟经过十年的发展,java也迎来了很多新的特性


深入理解Java虚拟机(第二版)基于的是 Jdk1.7 有些案例也涉及更早的版本
第三版补充的内容 以及修改的部分内容还未来及更新,后续慢慢更新

一、走进Java

java的发展史 这里不赘述 有兴趣可以读读,涨涨见识

下面简单摘抄一点关于重要虚拟机的知识,大家了解了解
大家只要记住 JAVA虚拟机 有很多公司在开发,大家在自己的运维时,一定要关注自己所部署环境中
是哪种虚拟机,不同的虚拟机 提供的功能和工具都有所区别,本人就遇到过

1.4.2 HotSpot VM

HotSpot 虚拟机 是目前运用最广泛的虚拟机 属于原来的Sun公司 (也是收购公司获得的)

1.4.4 BEA JRockit/IBM J9 VM

如果说HotSpot 是老大的话,JRockit与 J9 就是天下第二 分别是BEA 公司和IBM公司开发的

很多大公司都有自己的虚拟机,基本上大多数都是从Sun/Oracle 公司购买版权的方式获得或者基于OpenJDK项目改进而来(如阿里巴巴)

还有很多虚拟机这里就不一一介绍了

1.6.1

OpenJDK 和 Sun/OracleJDK 之间的关系:
2006年11月13日的JavaOne 大会上,Sun公司宣布最终将Java开源
并建立OpenJDK组织对开源的源码进行独立管理。除了极少量的产权代码
(这部分大多是Sun本身也无权限进行开源处理的)外,OpenJDK几乎包括了SunJDK的全部代码。
在JDK1.7中,SunJDK和OpenJDK除了代码文件头的版权注释之外,代码基本完全一样,所以OpenJDK7和SunJDK1.7
本质上就是同一套代码库开发的产品


二、Java内存区域和内存溢出异常

2.2 运行时数据区域

Java虚拟机在执行程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
Java虚拟机运行时的数据区图 如下:
在这里插入图片描述

2.2.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

在虚拟机的概念里(仅时该概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖
这个计数器来完成。

为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,堵路存储,我们称这类内存区域为“线程私有”的内存


2.2.2. Java 虚拟机栈

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

有人把 Java 内存区分为 堆内存(Heap) 和 栈内存 (Stack),这种是比较粗糙的分法。
这里的“栈” 就是这里的虚拟机栈,或者说是虚拟机栈中局部变量表部分
局部变量表部分 存放了 编译期可知的各种基本数据类型(boolean 、byte、char、short、int、float、long、double)、
对象引用和 reaturnAddress类型(指向了一条字节码指令的地址)。

在Java 虚拟机规范中,对这个区域规定了两种异常情况:
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果虚拟机栈可以动态扩展),当扩展时无法申请到足够的内存,就会抛出OutOfMemoryError

HotSpot 虚拟机的栈容量是不可以动态扩展的

2.2.3 本地方法栈

本地方法栈(Native Method
Stack)与虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
本地栈方法 具体的虚拟机可以自由实现它。(Sun HotSpot 虚拟机)直接就把本地方法和虚拟机合二为一。

本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常


2.2.4 Java 堆

Java堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。 Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆时垃圾收集器管理的主要区域,因此很多时候被称为“GC堆”
从内存回收的角度,由于现在收集器基本都采用粉黛收集器,所以Java堆中还可以细化:新生代和老年代。

Java虚拟机规范规定,Java
堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,既可以实现固定大小的,也可以是可扩展的,主流的虚拟机都是按照可扩展来实现的(通过
-Xmx 和-Xms 控制)。 如果Java堆中没有内存完成实例分配,并且堆无法再扩展时 就抛出了此OOM 异常


2.2.5 方法区

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

在HotSpot 虚拟机上开发,部署程序的开发者 习惯将方法区 称为 “永久代” ,本质上两者并不等价,仅仅是因为HotSpot 虚拟机 的设计团队 选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。这样HotSpot 的垃圾收集器可以像管理Java 堆一样去管理这部分内存。
对于其他虚拟机(如BEA JRockit、IBM J9等)来说不存在永久代的概念的
永久代有 -XX:MacPermSize 的上限,J9 和 JRockit 只要没有触碰到进程可用内存的上限,例如32位系统中的4GB,就不会出现问题
对于HotSpot 虚拟机,现在也有放弃永久代并逐步采用Native Memory 来实现方法去的规划了,在JDK 1.7 的HotSpot 中,已经把原本放在永久代的字符串常量池移出。
这区域的内存回收目标主要针对常量池的回收和对类型的卸载(条件很严苛),当方法区无法满足内存分配需求时将抛出OutOfMemoryError 异常
(这段话 板正了我对JVM GC的理解,一直不知道永久代是这样的来历)
这又让我想起刚工作时的一次 系统Bug解决,在某其他项目组上线时,公司的技术群里的大佬 特意询问了他们上线时的具体环境 其中提到了虚拟机的版本 说设置 -XX:MacPermSize 的上限 没作用,因为使用的是JRockit


2.2.6 运行时常量池

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


2.2.7 直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OOM异常出现。

理解就是 缓冲区使用的 堆外内存,避免Java 堆和 Native堆 来回复制数据,不受Java 堆内存的大小限制但是受到本机总内存的限制


2.3 HotSpot 虚拟机对象探秘

2.3.1 对象的创建

原文 对 对象创建过程的叙述 很多,有兴趣自己阅读一下,这里根据我自己的理解 画了个概括图
在这里插入图片描述


2.3.2 对象的内存布局

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

内容碎,了解即可


2.3.2 对象的访问定位

对象访问方式 主流的有 使用句柄直接指针 两种

句柄访问,Java 堆划出一块内存作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的地址信息
在这里插入图片描述
直接指针访问,Java 堆对象的布局就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象地址
在这里插入图片描述

句柄访问优点: 对象被移动,只会改变句柄中的实例数据指针,而reference 本身不需要修改。
直接指针访问优点:速度更块,节省指针定位的时间开销


2.4 实战: OOM 异常

这一节 就很有实际操作意义 在工作中可以直接运用

在Eclipse 的 Debug/Run 页签中设置虚拟机参数

运行时 使用 Run/Debug Configurations…

在这里插入图片描述
这里 整理 JVM 修改的各类参数的含义

-verbose:gc :开启gc日志 在控制台输出GC情况 
-XX:+PrintGCDetails : 在控制台输出详细的GC情况
-XX:+PrintGCTimeStamps -- 打印出GC的时间信息  
-XX:SurvivorRatio=8 --设置的是Eden区与每一个Survivor区的比值, 表示 在新生代中 eden区和survivor区的大小比值为8比1
-Xloggc:d:/gc.log -- gc日志的存放位置
-Xms20M :JVM初始分配的堆内存堆 最小值 为20M
-Xmx20M :JVM最大允许分配的堆内存堆最大值 为20M
-XX:PermSize=256M JVM初始分配的非堆内存
-XX:MaxPermSize=1024M JVM最大允许分配的非堆内存
-XX:+HeapDumpOnOutOfMemoryError :虚拟机出现内存溢出异常时Dump出当前内存堆转储快照以便事后分析
-XX:+UseSerialGC :使用串行回收器进行回收
-Xss128k : 设定栈容量128k
-XX:PretenureSizeThreshold=3145728:令大于这个设置值的对象直接在老年代分配(只对Serial 和 ParNew两款收集器有效)


2.4.1 Java 堆溢出

Java 堆 用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。 (GC Roots 到对象之间可达路径 在 垃圾收集机制那章 有详细解释,这里简单解释就是 对象还活着,没有被回收)

package jvm1_7;

import java.util.ArrayList;
import java.util.List;

/**
 * 模拟OOM 的情况 定死了堆的大小并且不让它自动扩展,当出现内存溢出异常时Dump出当前的内存堆转储快照
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * 堆最小值-Xms ;
 * 堆最大值-Xmx ;
 * 虚拟机出现内存溢出异常时Dump出当前内存堆转储快照以便事后分析-XX:+HeapDumpOnOutOfMemoryError

 * 生成的dump 在项目根目录下 直接导入eclipse  已经安装MAT插件

 */
public class HeapOOM {
    
    

	static class OOMObject {
    
    
		
	}
	
	public static void main(String[] args) {
    
    

		List<OOMObject> list = new ArrayList<OOMObject>();
		while(true) {
    
    
			list.add(new OOMObject());//一直生成 撑爆了堆内存
		}
	}

}

这里就引入了内存映像分析工具 (如 Eclipse Memory Analyzer 简称 MAT)
就是对 Dump 出来的堆转储快照 进行分析(Eclipse 先在 它的Help --> Eclipse Marketplace 里搜索 MAT 直接安装即可 ),再将 生成的dump(在项目根目录下 ) 直接导入eclipse (MAT 的各类界面和分析 在这就不列举了)

在这里插入图片描述


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

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
如果虚拟机再扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError异常
两者也会重叠

package jvm1_7;

/**
 * 模拟StackOverflowError
 * VM Args: -Xss128k
 * 设定栈容量-Xss
 * 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
 * 如果虚拟机再扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError异常
 */
public class JavaVmStackSOF {
    
    

	private  int stackLength = 1;
	
	public  void stackLeak() {
    
    
		stackLength ++;
		stackLeak();
	}
	
	public static void main(String[] args) {
    
    
		JavaVmStackSOF oom = new JavaVmStackSOF();
		try {
    
    
			oom.stackLeak();
		}catch (Throwable e) {
    
    
			System.out.println("stack Length:"+ oom.stackLength);
			throw e;
		}
		
	}

}

  1. 使用 -Xss 参数减少栈内存容量。结果:抛出StackOverflowError异常时输出的堆栈深度相应缩小
  2. 定义大量的本地变量,增大此方法帧中本地变量表的长度。结果:抛出StackOverflowError异常时输出的堆栈深度相应缩小

这里实验表明 单线程下, 栈帧太大 还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常

特别点: 多线程的应用时,StackOverflowError异常 有错误栈可以阅读 但是如果是 建立过多线程导致内存溢出,但在不能减少线程或更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
(这里真的的 是只有理解了这里的JVM 的运行原理 才能懂,我也是读了两三遍才搞明白 减少内存 来 解决内存溢出 这个看起来有点怪的 话)


在我们日常的工作当中 其实 StackOverflowError异常 和OutOfMemoryError
真的很常见,像以前看到这两个异常 只知道 堆栈满了 至于异常为什么会抛出来的底层原因 一无所知。

这让我想起 一次生产问题:有一天 生产服务器 突然挂了,查看了日志发现了OOM 异常
但是 排查了一下,代码并没有导致内存溢出的地方,且服务已经运行1年多,最近也没有进行版本更新,重启服务器后让服务先运行起来
然后开始了查错的历程
1个月后 再次发生该问题,这次就发现有很多线程 挂起,并且经过这一个月的监控,GC的日志 标明 每隔1周堆内存 就猛增 且Full GC 的频率再增高,让我找到突破口
原来 系统环境并不是我们自己准备的,Java虚拟机各类启动参数都还是初始值,包括堆内存的大小,最大都只设置了512M
随着业务量的增大,堆内存扩展到 512M 后 终于承受不足了

排查的经历 https://blog.csdn.net/sinat_34979884/article/details/104962895


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

jdk 1.6 以及之前版本 会出现 运行时常量池溢出,在OOM后紧跟的提示信息 “PermGen space”,说明运行时常量池属于方法区(HotSpot 虚拟机中的永久代)的一部分
jdk 1.7 开始逐步“去永久代”,并在JDK8 中完全使用元空间来代替永久代

在JDK 8以后,永久代便完全退出了历史舞台,元空间作为其替代者登场。在默认设置下,前面
列举的那些正常的动态创建新类型的测试用例已经很难再迫使虚拟机产生方法区的溢出异常了。不过
为了让使用者有预防实际应用里出现一些破坏性的操作,HotSpot还是提供了一些参数作为元空间的防御措施,主要包括:

·-XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。

·-XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集
进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放
了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该
值。

·-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可
减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最
大的元空间剩余容量的百分比。


2.4.4 本机直接内存溢出

直接内存(DirectMemory)容量可通过 -XX:MaxDirectMemorySize 指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样
导致DirectMemory 的内存溢出,一个明显的特征是在Heap Dump 文件中不会看见明显的异常,如果发现OOM之后Dump文件很小,而程序又直接或间接使用NIO,就可以考虑这方面的原因


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

3.1 概述

这里我自己说几点 对于垃圾收集器
在实际运用中其实我们很少去关注的,Java正是有这种自动的垃圾收集器,才让我们省去了很多精力,但是在日常生产中,我们又需要对其有所了解,才能方便我们查找问题。
当然 这里面大部分的理论知识,也会出现(也仅仅)在面试题上,从实战的角度来看,懂得GC 日志就能解决很大一部分得问题了,但我觉得知其所以然
也是对自我的一种提升


3.2.1 引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用技术算法 实现简单,判定效率也高,但是很难解决对象之间相互循环引用的问题

package jvm1_7;

/**
 * 引用计数法算法
 * testGC() 方法执行后,objA和objB会不会被GC呢?
 * VM Args:
 -Xms20M -Xmx20M -Xmn10M
-XX:+PrintGCTimeStamps -- 打印出GC的时间信息  
-XX:+PrintGCDetails  --打印出GC的详细信息  
-verbose:gc --开启gc日志 
-XX:SurvivorRatio=8 --设置的是Eden区与每一个Survivor区的比值, 表示 在新生代中 eden区和survivor区的大小比值为8比1
-Xloggc:d:/gc.log -- gc日志的存放位置
 *
 */
public class ReferenceCountingGC {
    
    

	public Object instance = null;
	
	private static final int _1MB = 1024 * 1024;
	
	//用来占点内存,以便能在GC日志中看清楚是否被回收过
	private byte[] bigSize = new byte[3 * _1MB];
	
	public static void main(String[] args) {
    
    

		ReferenceCountingGC objA = new ReferenceCountingGC();
		ReferenceCountingGC objB = new ReferenceCountingGC();
		
		objA.instance = objB;
		objB.instance = objA;
		
		objA = null;
		objB = null;
		
		//假设在这行发生GC,objA和objB会不会被GC呢?
		System.gc();
		
		
		//GC 日志
		//0.116: [GC (System.gc()) [PSYoungGen: 7291K->864K(9216K)] 7291K->872K(19456K), 0.0008256 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
		//0.117: [Full GC (System.gc()) [PSYoungGen: 864K->0K(9216K)] [ParOldGen: 8K->630K(10240K)] 872K->630K(19456K), [Metaspace: 2707K->2707K(1056768K)], 0.0052436 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
		
		//发现PSYoungGen: 864K->0K(9216K) 被回收了,说明java 用的不是引用计数算法来做GC 解决了相互引用的问题
		//如果是用引用计数算法来做GC 两个对象相互引用 计数永远为1 应该不会被回收才对
	}

}

3.2.2 可达性分析算法

在主流的商用程序语言(Java/C#等)的主流实现中,都是称通过可达性来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象道GC Roots没有任何引用链相连时,则证明此对象是不可用的。

在这里插入图片描述

如图所示,对象 object5、object6、object7 虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象

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

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

3.2.3 再谈引用

无论是通过引用计数器算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK1.2以,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是过于狭隘,一个对象在这种定义下只用被引用或者没有被引用两种状态。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中:如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。

JDK1.2之后,Java 对引用的概念进行了扩充,将引用分为
强引用、软引用、弱引用、虚引用 4种,这4种引用强度依次逐渐减弱

这四类,具体定义有兴趣去看下


3.2.4 生存还是死亡

有兴趣可以了解下


3.2.5 回收方法区

很多人认为方法区(或者HotSpot 虚拟机中的元空间或者永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且方法区中进行垃圾收集的“性价比” 一般比较低。 在堆中,新生代中 可以回收70% 到 95% 而方法区垃圾收集效率远低于此。

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
回收废弃常量与回收Java堆中的对象非常类似。

举个常量池中字面量回收的例子:

假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接
口)、方法、字段的符号引用也与此类似。

判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就
比较苛刻了。需要同时满足下面三个条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP的重加载等,否则通常是很难达成的。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方 法。

Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是
和对象一样,没有引用了就必然会回收。

关于是否要对类型进行回收,HotSpot虚拟机提供了-
Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:
+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在
Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版 [1] 的虚拟机支持。

在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载
器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压
力。


3.3 垃圾收集算法

这里对类似的名称做一次统一定义

部分收集(Partial GC):指目标不是完整收集整个Java 堆的垃圾收集,其中又分为:

  • 新生代收集(Minor GC/ Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/ Old GC): 指目标只是老年代的垃圾收集。
    Major GC 有点混淆 在不同资料上有不同的含义 最好看上下文
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。

整堆收集(Full GC):收集整个Java 堆和方法区 的垃圾收集。

新生代、老年代是HotSpot 虚拟机,也是业内主流命名方式。在IBM J9虚拟机中对应称为婴儿区和长存区,名字不同但含义是一样的的。

3.3.1 标记-清除算法

标记-清除 算法 和名字一样容易理解,直接看图
1.标记 出 所有需要回收的对象(问我哪些需要回收 请看上一小节 3.2.1.3.2.2)
2.清除 统一回收

在这里插入图片描述
主要不足

  1. 效率问题,标记和清除的过程效率都不高
  2. 空间问题,你看图也能很容易看出,会出现很多大量不连续的内存碎片

3.3.2 复制算法

为解决效率问题,一种称为“复制”的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
实现简单,运行高效,还不产生内存碎片,但是这种算法的代价是将内存缩小一半,未免太高了。

看图 一目了然

在这里插入图片描述

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM 公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden 空间和两块较小的Survivor 空间,每次使用Eden 和其中一块Survivor,当回收时,将Eden 和 Survivor还活着的对象一次性复制到另一块Survivor 空间上,最后清理掉Eden 和刚才用过的Survivor 空间。HotSpot虚拟机默认Eden 和 Survivor的大小比例是8:1,也就是每次新生代中可用空间为整个新生代容量的90%(80% + 10%),只用10%的内存会被“浪费”。
当然 我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor 空间不够用时,需要依赖其他内存(老年代)进行分配担保。


3.3.3 标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这个算法。
根据老年代的特色,有人提出了另外一种“标记–整理”算法,标记过程仍然与“标记-清除”算法一样,但后续的步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理端边界以外的内存

看图说话

在这里插入图片描述


3.3.4 分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”
算法,这种算法并没有什么新的思想,知识根据对象存活周期的不同将内存划分为几块。一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记清理”或“标记整理” 算法来进行回收。


3.4 HotSpot 的算法实现

这里都是概念的内容,了解下,有这些概念即可

3.4.1 枚举根节点

从可达性分析中从GC Roots 节点找引用链这个操作为例,可作为GCRoots的节点主要再全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查者里面的引用,那么必然会消耗很多时间。
另外,可达性分析堆执行时间的敏感性还体现再GC停顿上,因为这项分析工作必须再一个确保一致性的快照中进行—这里的一致性的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。这个导致GC进行时必须停顿所有Java执行线程(Sun将这件事情称为“Stop The World”)的其中一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

GC进行时必须停顿所有Java执行线程(Sun将这件事情称为“Stop The World”)哇塞 不明觉厉

在HotSpot 的实现中,是使用一组称为OopMap 的数据结构来直接得知哪些地方存在对象引用,这样当执行系统停顿下来后,并不需要一个不漏地检查所有执行上下文和全局地引用位置


3.4.2 安全点

在OopMap 协助下,HotSpot 可以快速且准确地完成GC Roots枚举,但可能导致引用关系变化 或者生成地OopMap过多,需要大量地额外空间。 实际上,HotSpot也的确没有为每条指令都生成OopMap ,只是在“特定地位置”记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。
如何在GC发生时让所有线程都“跑”到最近地安全点上再停顿。有两种方案:抢断式中断 和 主动式中断


3.4.3 安全区域

安全区域是指再一段代码片段之中,引用关系不会发生变化。在这个区域中地任意地方开始GC都是安全的。我们也可以把安全区域(Safe Region) 看做是被扩展了的安全点


3.5 垃圾收集器

在这里插入图片描述

图展示了 7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它可以搭配使用。虚拟机所处的区域,则表示它属于新生代收集器还是老年代收集器。
在介绍这些收集器各自特性之前,我们先来明确一个观点:虽然我们是在对各个收集器进行比较,但并非为了挑选出一个最好的收集器。所以我们选择的只是具体应用最适合的收集器。


3.5.1 Serial 收集器

Serial 收集器是最基本、发展历史最悠久的收集器,曾经(JDK1.3.1之前)是虚拟机新生代收集的唯一选择。 单线程,在垃圾收集时只会使用一条收集线程,并必须暂停其他所有的工作线程 (Stop The World)
Clinet 模式下的虚拟机 使用效果很好

看图就很容易理解了
在这里插入图片描述

从JDK1.3 开始,一直到现在,HotSpot 虚拟机开发团队为消除或者减少工作线程因内存回收而导致停顿的努力一直在进行着,从Serial 收集器 到 Parallel 收集器,再到 CMS (Concurrent Mark Sweep)乃至GC 收集器的最前沿成果Garbage First(G1)收集器,用户线程的停顿时间在不断缩短,但是仍然没有办法完全消除(这里暂不包括RTSJ 中的收集器)


3.5.2 ParNew 收集器

ParNew 收集器 其实就是Serial 收集器的多线程版本

在这里插入图片描述

3.5.3 Parallel SCavenge 收集器

Parallel SCavenge 收集器 是新生代收集器,它也是使用复制算法的收集器。看上去和 ParNew 都一样,但 Parallel SCavenge收集器关注的点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户的停顿时间,而它的目的时达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量= 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行100 分钟,其中垃圾收集花费1 分钟,那吞吐量就是 99%。
Parallel SCavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis 参数以及直接设置 吞吐量大小的 -XX:GCTimeRadio 参数。
MaxGCPauseMillis 参数允许的值是一个大于0的毫秒数,这个值越小,越要牺牲吞吐量和新生代空间 所以要综合考虑
GCTimeRadio参数的值应当是一个大于0且小于100的整数,默认值是99 就是允许最大1% (即1/(1+99))的垃圾收集时间


3.5.3 Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用“标记 整理”算法。这个收集器德主要意义也是在于给Client 模式下的虚拟机使用。在Server 模式下,主要两大用途,一是在JDK1.5以及以前版本中与Parallel SCavenge 收集器搭配使用,另一种是作为CMS 收集器的后备预案。

在这里插入图片描述

3.5.5 Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程 和 “标记–整理”算法。
Parallel Old 出现后,注重吞吐量以及CPU资源敏感的场合,都可以考虑Parallel Scavenge 加Parallel Old 组合了

在这里插入图片描述


3.5.6 CMS 收集器

CMS(Concurrent Mark Sweep) 收集器 是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java 应用集中在互联网站 或者B/S系统的服务端上,这类应用尤其重视服务器的相应速度。 CMS 收集器是基于 “ 标记–清除”算法实现的

整个过程 分为4个步骤

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

在这里插入图片描述
CMS 是一款优秀的收集器,主要优点

  • 并发收集
  • 低停顿

三个明显的缺点:

  • 对CPU资源非常敏感 ;并发阶段 占用一部分的线程(或者说CPU 资源)而导致应用程序变慢,总吞吐量会降低。默认启动的回收线程数是(CPU数量 + 3 )/4
  • 无法处理浮动垃圾;并发清理阶段,用户线程还在运行,就会产生新的垃圾,这部分的垃圾出现在标记过程之后,CMS无法处理,只好留待下一次GC。可能出现 “Concurrent Mode Failure” 失败,导致Full GC,性能反而下降了。
  • CMS 是基于“标记–清除”算法 会带来大量碎片

3.5.7 G1收集器

G1(Garbage First) 收集器是一款面向服务端应用的垃圾收集器。HotSpot 开发团队赋予它的使命是未来可以替代JDK1.5 中发布的CMS 收集器。

与其他收集器相比有以下显著特点

  • 并行与并发;
  • 分代收集;
  • 空间整合;
  • 可预测的停顿;

在这里插入图片描述


3.5.8 理解 GC 日志

又是有实战意义的一章,开发、运维时的利器 当然受到不同JDK 版本和 垃圾收集器的影响
所输出的GC 日志 有一定的格式差别,但大部分的内容都是有着一样的意思。

下面的这段GC 日志 是我自己生成的,我的jdk版本是 JDK1.8 和书中的有所差异,结合自己的理解,整理下日志

	0.116: [GC (System.gc()) [PSYoungGen: 7291K->864K(9216K)] 7291K->872K(19456K), 0.0008256 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    0.117: [Full GC (System.gc()) [PSYoungGen: 864K->0K(9216K)] [ParOldGen: 8K->630K(10240K)] 872K->630K(19456K), [Metaspace: 2707K->2707K(1056768K)], 0.0052436 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
		

最前面的数字 “0.116” 和 “0.117” : 代表GC 发生的时间,这个数字的含义是从Java 虚拟机启动以来经过的秒数。

GC日志开头的“ [GC ” 和 “ [Full GC ”说明这次GC 发生的停顿类型,而不是用来区分新生代和老年代GC 的。

如果有“Full ”说明这次GC 是发生了 Stop The World 的 如果是调用System.gc() 方法所触发的收集,那么就显示[Full GC (System.gc())

接下来“[PSYoungGen ” 、“[ParOldGen”、“[Metaspace” 这里的显示的区域名与使用的GC收集器有关 如[PSYoungGen 是Parallel Scavenge 收集器的新生代 名称,[DefNew 是Serial收集器的新生代名称,[ParNew 是ParNew收集器 新生代名称,老年代和永久代同理。

这里[Metaspace 是元数据空间(大家有兴趣 可以查查)
替代了永久代,随着JDK8的到来,JVM不再有PermGen。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Nativememory)中。

后面的方括号内部的 “ 7291K->864K(9216K)” 含义 “GC 前该内存区域已使用容量 -> GC 后该内存区域已使用容量(该内存区域总容量)”。
而方括号之外的 “7291K->872K(19456K)” 表示“GC 前Java堆已使用容量 -> GC后Java 堆已使用容量(Java 堆总容量)”

再往后,“ 0.0008256 secs” 表示 该内存区域 GC 所占用的时间,单位是秒。”Times: user=0.00
sys=0.00, real=0.00 secs“ 是更具体时间数据 user 、sys、real 和Linux 的time
命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU 时间和操作从开始到结束所经过的墙钟时间(Wall Clock
Time)。CPU时间与墙钟时间的区别是,墙钟时间包括各自非运行算的等待耗时,例如等待I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU
或者多核的话,多线程操作会叠加这些CPU 时间,所以看到user 或者 sys 超过 real 时间是完全正常的。


3.6 内存分配与回收策略

3.6.1 对象优先在 Eden 分配

对象优先在Eden 分配 大多数情况下新生代Eden区中分配,当Eden区没有足够空间进行分配时虚拟机将发起一次Minor GC

package jvm1_7;

/**
 * 
 * VM Args: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 * 
 * 验证
 *
 */
public class ObjEdenFirst3_6_1 {
    
    

	private static final int _1MB = 1024 *1024;
	
	
	public static void main(String[] args) {
    
    
        
		byte[] allocation1,allocation2,allocation3,allocation4;
		allocation1 = new byte[2 * _1MB];
		allocation2 = new byte[2 * _1MB];
		allocation3 = new byte[2 * _1MB];
		allocation4 = new byte[4 * _1MB]; //出现一次Minor GC
		
		
		/** 
		 * 尝试分配3个2MB大小 和 1个4MB大小的对象,
		 * 在运行时通过-Xms20M -Xmx20M -Xmn10M 参数限制了Java堆大小为20MB,
		 * 不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。
		 * -XX:SurvivorRatio=8 决定了新生代中Eden区和一个Survivor区的空间比例是8:1
		 * 从输出的结果:
		 * eden space 8192K, 
		 * from space 1024K, 
		 * to   space 1024K, 
		 * 新生代总可用空间为9MB=9216KB(Eden区+1个Survivor区的总容量)
		 * 
		 * 分配allocation4对象的语句时会发生GC的原因:
		 * 给allocation4分配内存时,发现Eden已经被占用6MB
		 * 剩下空间(3MB)已不足分配allocation4所需的4MB内存,因此发生Minor GC。
		 * GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间(只有1MB),所以只好
		 * 通过分配担保机制提前转移到老年代上
		 * 
		 * 这个GC 结束后,4MB的allocation4对象顺利分配在Eden中,因此程序执行完的结果:
		 * Eden: allocation4(占4MB) Survivor:空闲                                                                                                                     
		 * 老年代:allocation1,allocation2,allocation3 (占6MB)
		 * 
		 */
		
		
		
	}

}


3.6.2 大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java 对象,最典型的大对象就是那种很长的字符串以及数组。
大对象对虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来”安置“它们。
PretenureSizeThreshold :令大于这个设置值的对象直接在老年代分配(只对Serial 和 ParNew两款收集器有效)
这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集内存)

package jvm1_7;


/**
 * 大对象直接进入老年代
 * VM Args: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 	-XX:PretenureSizeThreshold=3145728
 * PretenureSizeThreshold :令大于这个设置值的对象直接在老年代分配(只对Serial 和 ParNew两款收集器有效)
 * 
 * 验证
 *
 */
public class PretenureSizeThreshold3_6_2 {
    
    
	
	private static final int _1MB = 1024 *1024;
	
	public static void main(String[] args) {
    
    
		byte[] allocation4;
		allocation4 = new byte[5 * _1MB];
	}

}


3.6.3 长期存活的对象将进入老年代

分代收集的思想来管理内存,那么就需要内存回收时就必须能识别哪些对象应放在新生代,哪些放在老年代。
为了做到这一点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden 出生并经过第一次Minor GC后仍然存活,并且能被Survivor 容纳的话,将被移动到Survivor 空间中,并且对象年龄设为1。对象在Survivor 区中每”熬过“ 一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象被晋升老年代的年龄阈值,可以通过参数
-XX:MaxTenuringThreshold 设置。

这个就理解为 对象产生后 经过一次GC 就增长1 岁,到一定的岁数就老了


3.6.4 动态对象年龄判定

为了更好地适应不同程序地内存状况虚拟机并不是永远地要求对象地年龄必须达到MaxTenuringThreshold
才能晋升老年代,如果在Survivor 空间中相同年龄所有对象大小地总和大于Survivor 空间地一般,年龄大于或等于该年龄地对象就可以直接进入老年代,无须等到MaxTenuringThreshold 中要求地年龄。


3.6.5 空间分配担保

在发生GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么GC 可以确保是安全的。如果不成功,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么继续检查老年代最大可用的连续是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次GC,尽管这次GC 是有风险的; 如果小于,或者HandlePromotionFailure 设置不允许冒险,那这次也要改为进行一次Full GC

理解:老年代给新生代GC 时 提供贷款空间,借给新生代使用,在借用前,先看看你以前借用的记录,要是比以往的都大,还要Full GC 来腾空间


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

这一大章,对运维的工作有很大帮助,刚工作时,项目运行遇到问题,只会苦兮兮的查日志,人肉分析,很大工具都不会用,现在想来那也是很大的财富,学到很多工具简化了这些排查工作,如果一上来就使用,自己也可能过于依赖工具了,因为各个项目所处的环境各不相同,有些工具还不好使。

4.2 JDK 的命令行工具

受不同版本的JDK 影响,命令有所不同

本章介绍 工具全部基于Windows平台 的JDK1.6,
生产运行环境一般是Linux 的,这些命令的作用就受限了,有些干脆就用不起来

命令 作用
jps 显示指定系统内所有的HotSpot 虚拟机进程
jstat 收集HotSpot 虚拟机各方面的运行数据
jinfo 显示 虚拟机配置信息
jmap 生成虚拟机的内存转储的内存快照 (heapdump文件)
jhat 用于分析heapdump 文件,它会建立一个HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果
jstack 显示 虚拟机的线程快照

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 ] ] ]

interval 和 count 代表查询间隔 和次数,如果省略这两个参数,说明只查询一次

这是我经常用的命令:

jstat -gc 2764 250 20 //每250毫秒查询一次进程2764 垃圾收集状况,一共查询20次

option 是可选的参数,主要分三类:类加载,垃圾收集,运行期编译状况

选项 作用
-class 只输出LVMID 省略主类的名称
-gc 监视Java 堆状况,包括Eden 区、两个survivor区、老年代、永久代等容量、已用空间、GC时间合计等信息
-gccapacity 监视内容与-gc基本相同,但输出主要关注Java 堆各个区域使用到的最大、最小空间
-gcutil 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause 监视内容与-gcutil基本相同,但额外输出导致上一次GC产生的原因
-gcnew 监视新生代GC状况
-gcnewcapacity 监视新生代GC状况,但输出主要关注Java 堆各个区域使用到的最大、最小空间
-gcold 监视老年代GC状况
-gcoldcapacity 监视老年代GC状况,但输出主要关注Java 堆各个区域使用到的最大、最小空间
-gcpermcapacity 输出永久代使用到的最大、最小空间
-compiler 输出JIT编译器编译过的方法、耗时等信息
-printcompilation 输出已经被JIT编译的方法

jstat -gcutil 进程号 //这个经常使用

选项 作用
S0 Survivor 0区的百分比
S1 Survivor 1区的百分比
E 新生代Eden区的百分比
O 老年代 Old区的百分比
P 永久代的百分比
YGC 共发生minor GC 的次数
YGCT 共发生minor GC 的总耗时
FGC 共发生Full GC 的次数
FGCT 共发生Full GC 的总耗时
GCT 共发生所有GC 的总耗时

4.2.3 jinfo :Java 配置信息工具

命令格式 jinfo [option] pid

jinfo 的作用是实时地查看和调整虚拟机各项参数


4.2.4 jmap:Java 内存映像工具

命令格式 jmap [option] vmid

选项 作用
-dump 生成Java 堆转储快照。格式为: -dump:[live,] format=b,file=,其中live 子参数说明是否只dump 出存活地对象
-finalizerinfo 显示在F-Queue 中等待 Finalizer 线程执行finalizer 方法地对象,只在Linux/Solaris平台有效
-heap 显示Java 堆详细信息,如使用哪种回收器,参数配置,分代状况等,只在Linux/Solaris平台有效
-histo 显示堆中对象统计信息
-permstat 以ClassLoader 为统计口径显示永久代内存状况,只在Linux/Solaris平台有效
-F 当虚拟机进程对-dump 选项没有响应时,可使用这个选项强制生成dump 快照只在Linux/Solaris平台有效
 jmap -dump:format=b,file=eclipse.bin 3500  //使用jmap 生成一个正在运行地Eclipse的dump 快照文件的例子,例子中3500是通过jps 查到的LVMID

4.2.5 jhat:虚拟机堆转储快照分析工具

Sun JDK提供jhat 与 jmap 搭配使用,来分析jmap 生成的堆转储快照

大家可以了解下,后面又更好的分析工具,这个说实话 不常用了


4.2.6 jstack: Java堆栈跟踪工具

命令格式 jstack [option] vmid

jstack用于生成虚拟机当前时刻的线程快照。
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因

选项 作用
-F 当正常输出的请求不被响应时,强制输出线程堆栈
-l 除堆栈外,显示关于锁的附加信息
-m 如果调用到本地方法的话,可以显示C/C++ 的堆栈

用代码的方式 实现jstack 功能

package jvm1_7;

import java.util.Map;


/**
 * 查看每一条线程正在执行的方法堆栈的集合信息
 *
 */
public class GetAllStackTraces4_2_6 {
    
    

	public static void main(String[] args) {
    
    

		for(Map.Entry<Thread, StackTraceElement[]> stackTrace : Thread.getAllStackTraces().entrySet()) {
    
    
			Thread thread = stackTrace.getKey();
			StackTraceElement[] stack = stackTrace.getValue();
			if(thread.equals(Thread.currentThread())) {
    
    
				continue;
			}
			System.out.println("\n 线程: "+thread.getName() + "\n");
		
			for(StackTraceElement element : stack) {
    
    
				System.out.println("\t"+element+"\n");
			}
		}
	}
}


4.2.7 HSDIS: JIT生成代码反汇编

HSDIS是Sun 推荐的HotSPot虚拟机JIT 编译代码的反汇编插件

不常用,大家有兴趣可以了解下


4.3 JDK的可视化工具

主要是 JConsole 和 VisualVM ,在JDK bin 目录下,都有现成的

我自己这边已经是JDK1.8了,工具其实都差不多
在这里插入图片描述

4.3.1 JConsole: Java监视与管理控制台

演示代码:

package jvm1_7;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;


/**
 * 问题程序 用来展现JConsole的监视功能 
 * 
 * VM args: -Xms100m -Xmx100m -XX:+UseSerialGC
 * -XX:+UseSerialGC使用串行回收器进行回收
 */
public class JConsoleTest4_3_1 {
    
    

	/**
	 * 内存占位符对象,一个OOMObject 大约占 64KB
	 *
	 */
	static class OOMObject {
    
    
		public byte[] placeholder = new byte[64 * 1024];
	}
	
	public static void fillHeap(int num) throws InterruptedException {
    
    
		List<OOMObject> list = new ArrayList<>();
		for(int i = 0; i< num ; i++) {
    
    
			//稍作延时,令监视曲线的变化更加明显
			Thread.sleep(50);
			System.out.println(i);
			list.add(new OOMObject());
		}
		System.gc();
	}
	
	/**
	 * 线程死循坏演示
	 */
	public static void createBusyThread() {
    
    
		Thread thread = new Thread(new Runnable() {
    
    

			@Override
			public void run() {
    
    
				while(true);	
			}
			
		},"testBusyThread");
		
		thread.start();
	}
	
	/**
	 * 线程锁等待演示
	 * @param lock
	 */
	public static void createLockThread(final Object lock) {
    
    
		Thread thread = new Thread(new Runnable() {
    
    

			@Override
			public void run() {
    
    
				synchronized (lock) {
    
    
					try {
    
    
						lock.wait();
					} catch (Exception e) {
    
    
						e.printStackTrace();
					}
				}
			}
			
		},"testLockThread");
		
		thread.start();
	}
	
	/**
	 * 线程死锁等待演示
	 *
	 */
	static class SynAddRunable implements Runnable {
    
    
		int a,b;
		public SynAddRunable(int a,int b) {
    
    
			this.a =a;
			this.b =b;
		}
		@Override
		public void run() {
    
    
			synchronized (Integer.valueOf(a)) {
    
    
				synchronized (Integer.valueOf(b)) {
    
    
					System.out.println("a:"+a+" b: "+b+"  a+b:"+a+b);
				}
			}	
		}
		
	}
	
	
	
	
	public static void main(String[] args) throws InterruptedException, IOException {
    
    
		//1.使用内存页签进行监视,观察曲线和柱状指示图的变化
		//fillHeap(1500);
		
		//2.使用线程页签进行监视,选择main线程 观察线程状态
		/*BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		br.readLine();
		createBusyThread();
		br.readLine();
		Object obj = new Object();
		createLockThread(obj);*/
				
				
		//3.使用线程页签进行监视,检查死锁	
		for(int i = 0; i < 100; i++) {
    
    //循环加快触发死锁的概率
			new Thread(new SynAddRunable(1, 2)).start();
			new Thread(new SynAddRunable(2, 1)).start();
		}
				
				
				
		
	}

}

运行java程序 ,打开JConsole,选择本地进程(要是访问服务器,就是远程进程),选择进程

在这里插入图片描述

1.使用内存页签进行监视,观察曲线和柱状指示图的变化 fillHeap(1500) 太小的话 可以调大数值
可以看出 新生代 的内存变化

在这里插入图片描述

2.使用线程页签进行监视,选择main线程 观察线程状态

堆栈追踪显示 main 方法 在运行后 在readButes方法中 等待 System.in 的键盘输入
这是的线程为Runnable 状态,Runnable 状态的线程会被分派运行时间,但readBytes 方法检查到流没有更新时会立刻归还执行令牌,这种等待只消耗很小的CPU 资源
在这里插入图片描述
输入后,接着监控 testBusyThread 线程,从图中可看出,testBusyThread 线程一直在执行空循环
线程为Runnable 状态,而且没有归还线程执行令牌的动作,会在空循环上用尽执行时间直到线程切换,这种等待会消耗较多的CPU资源
在这里插入图片描述
再次输入后,testLockThread 线程在等待着lock 对象的 唤醒方法的出现,线程处于WAITING状态,再被唤醒前不会被分配执行时间。
在这里插入图片描述

3.使用线程页签进行监视,检查死锁 演示死锁的情况
运行线程后,找到“线程” 菜单,下面有“检测死锁” 按钮,点击后,会出现“死锁” 页签

在这里插入图片描述
打开 死锁 页签,可以清楚的看到
一共3个线程陷入死锁,Thread-1 在等待 Thread-0 ,Thread-0 在等待 Thread-1 ,Thread-199 在等待 Thread-1
在这里插入图片描述


4.3.2 VisualVM

很强大的运行监视和故障处理程序 也有堆栈转储的功能


五 调优案例分析与实战

此章 都是些实战案例,说实话,很多没做过,感触不是很深


六 类文件结构

此章 啃起来费劲,大量的指令 直接抄下来 也没有太多意义,后期消化后再酌情增加


七 虚拟机类加载机制

此章 的类加载机制 还是对开发运维时很有帮助,在测试时,经常遇到加载的不是想要的版本包。脑海里有双亲委派机制的模型 就能很好的排查问题

7.1 概述

虚拟机把描述类的数据从Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java 模型,这就是虚拟机的类加载机制


7.1 类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载 7个阶段。其中验证、准备、解析3个部分统称为连接。

发生顺序如图所示:
在这里插入图片描述
后面对各个阶段进行详细解释,这里 不想抄了,有兴趣再翻书再看一遍
下面比较重要

7.4.1 类与类加载器

类加载器虽然只用于实现类的加载动作,但它再Java程序中起到的作用却远远不限于类加载阶段。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java 虚拟机中的唯一性,每个类加载器,都拥有一个独立的类名称空间。
通俗来说 比较两个类是否相等,只用在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class 文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

这里 很重要两个类是否相等的条件之一:同一个类加载器加载才行
大家看演示:

package jvm1_7;

import java.io.IOException;
import java.io.InputStream;

/**
 * 类加载器与instanceof 关键字演示
 *
 */
public class ClassLoaderTest7_4_1 {
    
    

	public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    
    

		//自定义的加载器
		ClassLoader myLoader = new ClassLoader() {
    
    
		
			@Override
			public Class<?> loadClass(String name) throws ClassNotFoundException {
    
    
        			try {
    
    
						String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
						
						InputStream is = getClass().getResourceAsStream(fileName);
						if(is == null){
    
    
							return super.loadClass(name);
						}
						byte[] b = new byte[is.available()];
						is.read(b);
						return defineClass(name,b,0,b.length);
					} catch (IOException e) {
    
    
						throw new ClassNotFoundException(name);
					}
    		}
		};
		
		Object obj = myLoader.loadClass("jvm1_7.ClassLoadTest7_1").newInstance();
	
		System.out.println(obj.getClass());
	   System.out.println(obj instanceof jvm1_7.ClassLoadTest7_1);
	
	   /**
	    * myLoader 是自定义构造的一个简单的类加载器,他可以加载与自己在同一路径下的Class文件
	    * 使用myLoader类加载器去加载了"jvm1_7.ClassLoadTest7_1"类,并实例化了这个类的对象
	    * 输出结果:
	    * 第一句 这个对象确实是类jvm1_7.ClassLoadTest7_1 实例化出来的对象
	    * 第二句,这个对象与jvm1_7.ClassLoadTest7_1 类做所属类型检查的时候却返回false
	    * 这是因为虚拟机中存在了两个ClassLoadTest7_1类,一个由系统应用程序类加载器加载的,另一个是由
	    * 我们自定义的类加载器加载的,虽然来自同一个Class文件,但依然是两个独立的类
	    */
	   
	}
}


7.4.2 双亲委派模型

从Java 虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java 语言实现,独立于虚拟机外部,并且全部都继承自抽象类 java.lang.ClassLoader.
从Java 开发人员的角度来看,类加载器还可以划分得更细致一些,绝大部分Java 程序都会使用到以下3种系统提供得类加载器

启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java 程序直接引用,用户在编写自定义加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null 代替即可

扩展类加载器(Extension ClassLoader): 这个加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher$AppClassLoader实现。由于这个类加载器时ClassLoader 中的getSystemClassLoader()方法的返回值,所有一般也称为它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序默认的类加载器。

我们的应用成勋都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。

在这里插入图片描述

图上展示的即使双亲委派模型

双亲委派模型要求除顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父类加载器的代码。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只要当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

使用双亲委派模型来组织类加载器之间的关系,有个显而易见的好处就是Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。

未完待续…全靠手打

总结

看了这篇文章受到了鼓舞 Java虚拟机(JVM)你只要看这一篇就够了! 整理的很好 前期也是看这这篇文章学习的

猜你喜欢

转载自blog.csdn.net/sinat_34979884/article/details/116448923