JVM基础介绍
JVM是Java Virtual Machine的简称,意为Java虚拟机
- 虚拟机:指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统
有哪些虚拟机
- VMWare、Visual Box、Virtual PC、JVM…
- VMWare或者Visual Box都是使用软件模拟物理CPU的指令集,JVM使用软件模拟Java 字节码的指令集
JVM发展史
- 1996年 JDK1.0 Sun Classic VM,纯解释运行,使用外挂进行JIT
- 1998年 JDK1.2 Solaris Exact VM,JIT 解释器混合,提升的GC性能
- 2000年 JDK1.3 Hotspot VM,作为默认虚拟机,Sun JDK和OpenJDK共同虚拟机
- 2014年 JDK8 Hotspot VM整合JRockit VM优点
- IBM J9 VM、 Sun Mobile-Embedded VM…
- AliJVM,基于 OpenJDK HotSpot VM,是国内第一个优化、定制且开源的服务器版 Java 虚拟机
write once,run anywhere
- JVM是Java程序的操作系统
- JVM的可执行文件就是.class文件
- JVM在执行字节码时,把字节码解释成具体平台上的机器指令执行
- JVM屏蔽了操作系统之间的差异,使得Java程序能够跨平台
Java平台逻辑结构
Java程序执行流程
JVM物理结构
备注:Java虚拟机的主要任务是装载class文件并且执行其中的字节码,字节码由执行引擎来执行
一个虚拟机实例的行为是分别按照子系统、内存区、数据类型和指令来描述的,这些组成部分一起展示了抽象的虚拟机的内部体系结构
类装载器子系统负责查找并装载类型信息
Java虚拟机有两种类装载器:系统装载器和用户自定义装载器。前者是Java虚拟机实现的一部分,后者则是Java程序的一部分
执行引擎相当于线程,是JVM的核心,执行引擎的作用就是解析JVM字节码指令,得到执行的结果。执行引擎由各个厂家实现
SUN的hotspot是一种基于栈的执行引擎
JVM内存模型与垃圾收集机制
方法区
- 存放类元数据信息,线程共享
- 类的类型信息:类的完整名称、父类的完整名称、类型修饰符(public/protected/private)、类型的直接接口类表
- 常量池:类方法、域等引用的常量信息
- 域信息:域名称、域类型、域修饰符
- 方法信息:方法名称、返回类型、方法参数、方法修饰符、方法字节码、操作数栈、方法帧栈的局部变量区的大小、异常表(大部分来自class文件)
- 方法区无法满足内存分配需求,抛出OutOfMemoryError异常
- 常量池无法申请到内存,抛出OutOfMemoryError异常
备注:方法区中常量池中的常量没有被任何方法引用即可被回收
方法区对类元数据的回收, 虚拟机确认该类的所有实例已经被回收,并且加载该类的ClassLoader已经被回收即有可能被GC回收
JDK1.7Hotspot已经将字符串常量池从方法区中移出
JDK1.8中方法区彻底移除,替代为元空间(Metaspace)(移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代)
由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen
虚拟机栈
- 存放函数调用堆栈信息,线程私有
- 栈帧:虚拟机栈保存上下文数据的数据结构,存放方法的局部变量表、操作数栈、动态连接方法和返回地址信息
- 局部变量表:用于存放方法的参数和方法内部的局部变量,以“字”为单位内存划分
- 操作数栈:数据的运算操作工作区
- 请求的栈深度大于最大可用的栈深度抛出StackOverflowError
- 没有足够的内存空间支持Java栈动态扩展抛出OutodMemoryError
备注:方法调用到执行完成的过程,对应着栈帧在虚拟机栈中入栈出栈的过程
一个字占用32位长度,long和double变量占用2个字,其余类型占用1个字
Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈
函数嵌套调用的次数由栈的大小决定,栈越大,函数嵌套调用次数越多,对一个函数,参数越多,内部局部变量越多,栈帧就越大,嵌套调用次数就会减少
本地方法栈
- 存放函数调用堆栈信息,类似虚拟机栈
- 虚拟机栈管理Java函数调用,本地方法栈管理本地方法(C实现的)调用
- 同样存在两种异常:StackOverflowError和OutodMemoryError
程序计数器
- 程序所执行的字节码的行号指示器,线程私有
- 唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域
备注:程序计数器,线程执行Java方法,计数器记录正在执行的虚拟机字节码指令的地址,线程执行Native方法,计数器值为空
堆
- 存放Java程序运行时所需的对象、数组等数据,线程共享内存区域
- 所有的对象实例以及数组都要在堆上分配内存
- 堆是垃圾收集器管理的主要区域,称之为GC堆
- 线程共享Java堆可划分多个线程私有的分配缓冲区(TLAB)
- 堆中内存无法分配对象实例或无法扩展堆内存,抛出OutOfMemoryError
- 堆从垃圾回收角度可细分为:新生代和老年代,新生代细分为:Eden区间、From Survivor区间、To Survivor区间
备注:未来随着JIT编译器发展,对象并不一定绝对要分配在堆上
Direct Memory直接内存
- 使用Native函数库直接分配堆外内存,DirectByteBuffer对象引用此内存操作
- 作为提高性能,避免Java堆和Native堆来回复制数据
- 本地直接内存分配不受Java堆大小限制,但是受本机总内存限制,抛出OutOfMemoryError
JVM对象
对象的创建
- 指针碰撞(Bump the pointer)
- 空闲列表Free List
- 本地线程分配缓冲TLAB
对象的内存布局
- 对象头(Header):运行时数据和类型指针
- 实例数据(Instance Data)
- 对齐填充(Padding)
对象的访问定位
- 使用句柄
- 直接指针(HotSpot使用)
备注:运行时数据:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;如果对象是一个Java数组,必须有记录数组长度的数据
实例数据:对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容
对齐填充:HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全
JVM-OutOfMemoryError
堆溢出
虚拟机栈和本地方法栈溢出
方法区和运行时常量池溢出
本机直接内存溢出
垃圾收集思考问题
- 哪些内存要被回收?
- 什么时候回收?
- 如何回收?
JVM的垃圾回收
- 针对不确定性的Java堆和方法区的内存分配回收
- 程序计数器、虚拟机栈、本地方法栈随线程而生,随线程而灭,内存分配回收具备确定性
垃圾回收算法
引用计数法Reference Counting
- 垃圾回收时,只用收集计数为0的对象
- 无法处理循环引用的情况
标记-清除算法Mark-Sweep
- 引用根节点开始标记所有被引用的对象
- 遍历整个堆,把未标记的对象清除,效率不高
- 暂停整个应用,同时会产生内存碎片
复制算法Copying
- 内存空间划为两个相等的区域,每次只使用其中一个区域
- 垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中
- 需要两倍内存空间
标记-压缩算法Mark-Compact
- 结合了“标记-清除”和“复制”两个算法的优点
- 从根节点开始标记所有被引用对象
- 遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放(等同于标记-清除后,进行一次内存碎片整理)
备注:GC Root
虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI(即一般说的Native方法)引用的对象
垃圾回收思想
分区对待
- 增量收集Incremental Collecting
实时垃圾回收,应用进行的同时进行垃圾回收
- 分代思想Generational Collecting
取长补短,针对对象生命周期分析选择合适算法,提高效率
新生代-复制算法,老年代-标记-清除/标记-压缩算法
- 分区思想Region
堆空间划分区域,减少GC停顿
线程划分
- 串行收集
单线程处理所有垃圾回收工作
适合单处理器机器,或小数据量情况下的多处理器机器
- 并行收集
多线程处理垃圾回收工作
结合CPU数目,速度快,效率高
- 并发收集
相对串行和并行,停顿较短,用户线程和GC线程切换
JVM垃圾收集器
串行垃圾回收器
- 新生代串行Serial:单线程独占式,使用复制算法
- 老年代串行Serial Old:单线程独占式,使用标记-压缩算法
并行垃圾回收器
- 新生代并行ParNew:多线程,使用复制算法
- 新生代并行Parallel Scavenge:吞吐量优先,使用复制算法
- 老年代并行Parallel Old:多线程,使用标记-压缩算法
CMS回收器
- Concurrent Mark Sweep,并发,标记-清除算法
- 初始标记、并发标记、重新标记、并发清除
G1回收器
- 取代CMS,Garbage First
- 并行与并发、分代收集、空间整合、可预测的停顿
- 初始标记、并发标记、最终标记、筛选回收
垃圾回收器的使用
- -XX:+UserSerialGC,指定新生代、老年代使用串行回收器
- -XX:+UserParNewGC,指定新生代并行,老年代串行
- -XX:+UseParallelOldGC,指定新生代并行,老年代并行
- -XX:+UserParallelGC,指定新生代并行,老年代串行
- -XX:+UseConcMarkSweepGC,指定新生代使用并行,老年代使用CMS+串行
- -XX:+UseG1GC,指定使用G1
备注:吞吐量(Throughput) = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
CMS改进:增量式并发收集器i-CMS
内存分配与回收策略
对象优先在Eden区分配
- Eden内存不够,虚拟机会发起一次Minor GC
大对象直接进入老年代
- 典型的是很长的字符串与数组
- 比遇到一个大对象更坏的是遇到一群朝生夕灭的短命大对象
- -XX:PretenureSizeThreshold,设置大对象直接进入老年代的阈值
长期存活的对象将进入老年代
- 对象年龄,Minor GC一次,年龄加1
- -XX:MaxTenuringThreshold,设置对象进入老年代的年龄的最大值
动态对象年龄判定
- Survivor区同龄所有对象大小总和大于Survivor区的一半,年龄大于或等于该年龄的对象直接进入老年代
空间分配担保
- 老年代最大可用的连续空间是否大于新生代所有对象总和或历次晋升的平均大小决定Minior GC和Full GC
备注:PretenureSizeThreshold只对Serial和ParNew收集器有效,Parallel Scavenge不识别此参数,一般无需设置,若必须使用此参数,可考虑ParNew+CMS
常用GC参数
跟踪GC,读懂虚拟机日志
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintHeapAtGC,打印GC前后堆信息
- -XX:+PrintGCTimeStamps,打印GC时间
- -XX:+PrintGCApplicationConcurrentTime,打印应用程序执行时间
- -XX:+PrintGCApplicationStoppedTime,打印应用程序停顿时间
- -XX:+PrintReferenceGC,打印系统内引用信息
- -Xloggc,指定GC日志文件输出路径
类加载、卸载的跟踪
- -verbose:class,跟踪类加载和协助
- -XX:+TraceClassLoading
- -XX:+TraceClassUnLoading
- -XX:+PrintClassHistogram,打印系统类分布情况
系统参数查看
- -XX:+PrintVMOptions,打印虚拟机接收的命令行显式参数
- -XX:+PrintCommandLineFlags,打印虚拟机接收的显式和隐式参数
- -XX:+PrintFlagsFinal,打印所有系统参数
虚拟机工作模式
- -client
- -server
堆的参数配置
- 最大堆和初始堆
- -Xmx、-Xms
- 设置相等,减少程序运行时进行的垃圾回收次数,提高性能
新生代的配置
- -Xmn,一般新生代大小设置为整个堆的1/3-1/4
- -XX:SurvivorRatio,设置eden区与survivor区比例
堆溢出处理
- -XX:+HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath
- 记录溢出发生现场的堆dump文件
非堆内存参数配置
方法区
- -XX:PermSize、-XX:MaxPermSize、-XX:MaxMetaspaceSize
栈
- -Xss,指定线程的栈大小
直接内存
- -XX:MaxDirectMemorySize
控制GC
- -XX:+DisableExplicitGC,禁止显式GC
- -Xnoclassgc,不需要回收类
- -Xincgc,增量式GC
备注:java -version来查看jvm默认工作在什么模式,Server VM启动比Client VM慢大概10%,运行比Client VM快至少有10倍
增量式GC使用特定算法让GC线程和应用线程交叉进行,减少应用程序因GC而产生的停顿时间
JVM类文件结构与类加载机制
语言无关性
- Class文件与Java虚拟机绑定,与任何语言无关
Class文件
- 是一组以8字节为基础单位的二进制流
- 各个数据项目严格按照顺序紧凑排列在class文件中,中间没有任何分隔符
- Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表
无符号数
- 属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节
表
- 是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾
- 表主要用于描述有层次关系的复合结构的数据,比如方法、字段
整个Class文件本质上就是一张表
- 主要分为魔数、Class文件的版本号、常量池、访问标志、类索引(还包括父类索引和接口索引集合)、字段表集合、方法表集合、属性表集合
魔数Magic Number
- Class文件的头4个字节,值为0xCAFEBABE
- 唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件
版本号
- 次版本号(minor_version)
前2字节用于表示次版本号
- 主版本号(major_version)
后2字节用于表示主版本号
-
Class文件的版本号超过虚拟机版本,将被拒绝执行
-
常量池
- Class文件结构中与其它项目关联最多的数据类型
- 文件中第一个出现的表类型数据项目,占用Class文件空间最大的数据项目之一
- 常量池计数器(constant_pool_count)
- 常量池数据区(constant_pool[contstant_pool_count-1])
- 常量池cp_info结构
备注:0xCAFEBABE(咖啡宝贝)象征着著名咖啡品牌Peet’s Coffice中深受欢迎的Baristas咖啡,预示着日后Java商标的出现
常量池数据从索引1开始,索引值0表示特定情况表达不引用任何一个常量池项目的含义
常量池的14中常量类型各自均有自己的内部结构
访问标志
- 用于识别一些类或者接口层次的访问信息
- 共有16个标志位可用,目前只定义8个,未使用标志位一律为0
类索引、父类索引和接口索引集合
- u2类型数据和数据集合,确定类的继承关系
- 类索引确定类的全限定名
- 父类索引确定父类的全限定名(除java.lang.Object外,所有Java类的父类索引都不为0)
- 接口索引集合描述类实现哪些接口(入口为接口计数器,表示索引表的容量)
字段表集合
- 描述接口或者类中声明的变量
- 字段包括类级变量以及实例级变量,不包括方法内局部变量
方法表集合
- 表示当前类或接口中某个方法的完整描述,类似于字段表集合
属性表集合
- 在class文件,字段表,方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息
类加载机制
- 把描述类的数据从Class文件加载到内存,并对数据进行校验,转换,解析和初始化,最终形成可以被虚拟机直接使用的Java类型
类生命周期
- 加载、链接、初始化、使用。卸载
类加载过程
- 加载,验证,准备, 解析, 初始化五个阶段
加载
- 通过类的全限定名来获取定义类的二进制字节流,将这个字节流转换为方法区的运行时结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区内这个类各种数据的访问入口
验证
- 确保Class文件字节流包含信息符合规范,不危害虚拟机自身安全
- 文件格式验证、元数据验证、字节码验证、符合引用验证
准备
- 正式为类变量分配内存并设置变量初始值的阶段(不包括实例变量)
解析
- 将常量池内的符号引用替换为直接引用的过程
- 类或接口的解析、字段解析、类方法解析、接口方法解析
初始化
- 通过程序初始化类变量和其他资源,执行类构造器<clinit>()方法的过程
备注:类的加载,连接和初始化过程都是在程序运行期间完成的也就是动态性,虽然会增加类的加载性能开销,但是这也为java应用程序提供高度的灵活性
文件格式验证:魔数、版本号、常量tag标志…
元数据验证:类是否有父类、父类是否继承不允许被继承的类、不是抽象类是否实现父类或接口要求实现的方法…
字节码验证:保证跳转指令不会跳转到方法体以外的字节码指令上、保证方法体中的类型转换是有效的…
符号引用验证:符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用中的类,字段,方法的访问性是否可以被当前类访问…
什么是类加载器
- 把类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到JVM外部去实现,以便让程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为“类加载器”
类加载器分类
- 启动类加载器(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 应用程序类加载器(Application ClassLoader):一般情况下这个是程序默认的类加载器
双亲委派模型
- Java类带有优先级层次关系,安全稳定,避免重复加载,避免自定义的类覆盖了JDK的类
备注:类加载器收到类加载请求,首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传递到顶层的启动类加载器中,只有当父加载器反馈自己无法完成加载请求,只加载器才会尝试自己去加载
Java性能调优工具
Linux命令行工具
Top
- 宏观查看系统各个进程CPU占用、内存占用
- 监控java线程数
ps -eLf | grep java | wc –l
- 监控网络客户连接数
netstat -n | grep tcp | grep 侦听端口 | wc -l
Sar
- 查看I/O信息、内存信息、CPU占用
- sar [options] [<interval> [<count>]]
- Sar选项(interval-采样间隔,count-采样次数)
-A:所有报告的总和,
-u:输出CPU使用情况的统计信息,
-v:输出inode、文件和其他内核表的统计信息,
-d:输出每一个块设备的活动信息,
-r:输出内存和交换空间的统计信息,
-b:显示I/O和传送速率的统计信息,
-a:文件读写情况,
-c:输出进程统计信息,每秒创建的进程数,
-R:输出内存页面的统计信息,
-y:终端设备活动情况,
-w:输出系统交换活动信息
Vmstat
- 查看CPU占用、内存占用、swap使用、上下文切换、时钟中断
Iostat
- 查看磁盘I/O信息
- iostat [options] [<interval> [<count>]]
- -C:显示CPU使用情况
-d:显示磁盘使用情况
-k:以:KB:为单位显示
-m:以:M:为单位显示
-N:显示磁盘阵列(LVM):信息
-n:显示NFS:使用情况
-p[磁盘]:显示磁盘和分区的情况
-t:显示终端和CPU的信息
-x:显示详细信息
-V:显示版本信息
Pidstat
- CPU使用率监控、 I/O使用监控、内存监控
- pidstat [option] [<interval> [<count>]]
- Pidstat选项
-u:cpu使用情况统计
-r:内存使用情况统计
-d:IO情况统计
-p:针对特定进程统计
JDK命令行工具
Jps
- 类似linux的ps命令,列出系统中所有的Java应用程序,查看Java进程的启动类、传入参数、JVM参数信息
- jps [options] [hostid]
Jstat
- 详细的查看Java应用程序的堆使用情况及GC情况
- jstat [options vmid [interval[s|ms]] [count] ]
Jinfo
- 查看运行时某一个JVM参数的实际取值,也可运行时修改部分参数,使之立即生效
- jinfo [option] pid
Jmap
- 生成Java应用程序的对快照和对象的统计信息
- jmap [option] vmid
jstack
- 导出Java应用程序的线程堆栈,自动进行死锁检查,输出死锁信息
- jstack [option] vmid
图形化工具
Jconsole
- 基于JMX的可视化监视、管理工具
- 远程连接,需要设置运行参数
-Dcom.sun.management.jmxremote.port=8999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
Jvisualvm
- 升级版的Jconsole,多合一故障处理
MAT
- 基于Eclipse的内存分析工具
- 类似于IBM HeapAnalyzer
Jmc
- Java Mission Control
- Java Flight Recorder进行内存分配分析
性能测试工具
Apache Jmeter