何为Java虚拟机
- JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
- JVM有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统。
- JVM本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。
jvm-1.png
jvm排兵布阵
Java虚拟机在执行Java程序的过程中把它所管理的内存划分成若干个不同的数据区域.这些区域各有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁.落到现实中,我感觉jvm就是一个分工明确,配备齐全的作战部队.根据不同的功能和角色进一步又将部队分成多个部门(后勤部,通讯部,司令部等),部门的划分也是为了分工明确充分发挥其战队能力.
计数器-战场
类比
代表部队中的指挥部,向下级发号施令.在作战过程中担当部队大脑,控制着整个作战的走向,时刻调兵遣将.
简介
程序计数器:程序计数器是JVM中的一块比较小的内存空间,他可以看作是当前线程所执行的字节码的行号指示器.通过改变单个线程计数器来完成代码中的分支、循环、跳转、异常处理等.
存在必要性
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令.因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要又一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存.
特点
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域.
虚拟机栈-战场
类比
类比于部队中的后勤储备,在整个作战过程中至关重要.古代作战常说“兵马未动粮草先行”,非常明确清晰的阐述了其重要性.每个线程代表着一个作战部队,每个部队都要配备独立的后勤部,是部队中不可或缺的一部分.
简介
虚拟机栈:和程序计数器一样,虚拟机栈也是线程私有的,它的生命周期与线程相同.虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存放局部变量表,操作数栈,动态链接,方法出口信息等.每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈到出栈的过程.
必要性
局部变量表存放了编译期可知的各种基本数据类型(boolean byte char short int float long double),对象引用和returnAddress(指向了一条字节码指令的地址)
特点
在Java虚拟机规范中对这个区域规定了两种异常状况:1.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackoverflowError异常;2.如果虚拟机栈可以动态扩展如果扩展时无法申请到足够的内存就会抛出OutOfMemoryError异常.
本地方法栈-战场
性质和虚拟机栈相同,不同的是针对的目标不同.可能是骑兵连的后勤部(特殊的喂马需求),也可能是其它性质的部队的后勤部,但其本质上都是为作战服务.
方法区-战场
类比
军队中的兵工厂,为所有军队生产子弹,炮弹,坦克等不同的武器.该军工厂为所有部队提供服务,也可以满足单个部队的特殊需求.
简介
方法区:与堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的累信息,常量,静态变量,即使编译器编译后的代码等数据.HotSpot虚拟机设计上把方法区设计为“永久代”,在这片区域上和堆一样不需要连续的内存可以选择固定大小或者可扩展.对这片区域内存的回收目标主要是针对常量池的回收和对类型的卸载.
运行时常量池:
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在累加载后进入方法区的运行时常量池存放.
堆-战场
类比
双军交战的主战场,在这块区域时刻存在着生命的结束和新生力量的投入.
简介
垃圾堆:Java虚拟机管理的内存中最大的一块,被所有的线程共享,在虚拟机启动时创建.此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配.Java堆是垃圾收集器管理的主要区域,又具体细分为:新生代和老年代;在细致一点有Eden区,From Survivor空间,ToSurvivor空间等.从内存分配角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB).无论怎么划分都与存放内容无关,无论哪个区域存储的对象仍然是对象实例,进一步划分只是为了更好的回收内存.
特点
Java堆可以是物理上不连续的内存空间,只要逻辑上是连续的即可,可以通过参数(-Xmx和-Xms)控制堆的大小.在Java虚拟机规范中除了程序计数器外,虚拟机内存的其它几个运行时区域都有发生OOM异常的可能.
堆内存使用过程
1.触发校验
当在代码中通过new关键字生成对象的时候会触发一系列准备工作,过程如下:(1).检查方法区常量池中是否存在类符号引用(2).检验该类是否完成:加载->验证->准备->解析->初始化
2.内存分配
(1).“指针碰撞”取决于内存是不是规整的,进一步取决于垃圾收集器是否有整理内存的功能
(2).“空间列表”虚拟机维护一个列表记录内存分配的地址信息.在分配的过程中要保证内存非配是线程安全的,当多个线程同时请求内存时,确保不会发生内存覆盖.可以采用CAS乐观锁实现或者对每个线程在堆中预先分配一小块内存(TLAB本地线程分配缓冲).
(3).内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零(不包括对象头)
3.初始对象头信息
Java中对象映射到内存上分为三个区域:对象头,实例数据,对齐填充.头信息中又包括一下运行时数据:哈希码,GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等.在初始化完成之后虚拟机开始对对象进行必要的设置,例如填充对象的头信息等.
jvm清理战场
何为垃圾回收器
垃圾回收器所做的就是清理工作,类比于战场上的清理人员.需要对战场上的枪械物资(内存)进行回收二次利用,对牺牲/受伤的战士处理.jvm中的垃圾收集器则是对线程(类比于作战部队)在执行任务的时候产生的废物资源进行回收,释放占用的无用资源方便再次利用.
垃圾收集流程
死亡证明
- 引用技术发:给对象添加一个计数器,每当添加一个引用该对象的计数器+1.当触发资源回收时,根据计数器是否为0判断该对象的死活.
- 可达性分析:以“GC Roots”对象作为起始点,从这些节点开始向下搜索,所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象时不可用的.
-
GC Roots对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量音容的对象、本地方法栈引用的对象
jvm-gcroots.jpg
自我救赎
当对象达到可回收的条件(大于MaxTenuring)时开始被判定为死刑等待执行,这时候对象允许二审给予对象自我救赎的机会.救赎机会的判断:1.该对象重写了finalizer()方法 2.该方法没有被Java虚拟机调用过.获取到机会之后还要看该对象的自我造化,如果自辨成功才能获得重生.
何时回收
Minor GC
发生在年轻代的垃圾收集叫做minor gc,当JVM不能为一个新对象分配空间时,minorgc被触发。例如:Eden区域被填满时。因此,你的应用分配对象的频率越高,minor gc发生的越频繁.
Major GC
通过内存分配策略当对象达到一定当条件(eg:年龄)的时候会进入到进入到老年代,当进入老龄化阶段随着老年代的对象越来越多最终会达到临界点触发一次Major GC.
Full GC
对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
- 调用System.gc()
- 老年代空间不足
- 空间分配担保失败
- JDK 1.7 及以前的永久代空间不足
- Concurrent Mode Failure
执行CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
如何回收
标记清除
原理:首先将需要回收的对象进行标记,然后清理掉被标记的对象。
不足:标记和清除过程效率都不高;会产生大量不连续的内存碎片,导致无法给大对象分配内存。
标记整理
原理:让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。(对应于内存分配中的指针碰撞)
复制
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
不足:只使用了内存的一半,浪费一半资源
分代回收
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。一般将 Java 堆分为新生代和老年代。
- 新生代使用:复制算法
- 老年代使用:标记 - 清理 或者 标记 - 整理 算法
垃圾收集器
收集器分类
Serial 收集器
Serial 翻译为串行,可以理解为垃圾收集和用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序。这对于要求实时响应的程序有点不合适,就像在双军交战过程中要求停战休息一会尴尬.除了 CMS 和 G1 之外,其它收集器都是以串行的方式执行。CMS 和 G1 可以使得垃圾收集和用户程序同时执行,被称为并发执行。
ParNew 收集器
它是 Serial 收集器的多线程版本。是Server模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了Serial收集器,只有它能与CMS收集器配合工作。默认开始的线程数量与CPU数量相同,可以使用-XX:ParallelGCThreads 参数来设置线程数。
Parallel Scavenge 收集器
与ParNew一样是并行的多线程收集器。其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它[图片上传中...(jvm-cms.jpeg-b6c52f-1578496328487-0)]
的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指CPU用于运行用户代码的时间占总时间的比值。提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的 -XX:GCTimeRatio参数(值为大于0且小于100的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。还提供了一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)
Serial Old 收集器
是Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本。在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
CMS 收集器
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
特点:并发收集、低停顿。并发指的是用户线程和 GC 线程同时运行。
主要包括一下四个步骤:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿
G1 收集器
[图片上传失败...(image-9f8931-1578495668987)]
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多CPU和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉CMS收集器。Java堆被分为新生代、老年代和永久代,其它收集器进行收集的范围都是整个新生代或者老生代,而 G1 可以直接对新生代和永久代一起回收。
垃圾收集器比较
[图片上传失败...(image-4014b1-1578495668987)]
收集器配合
内存分配和回收策略
内存分配
- 对象优先在Eden分配,新new出的对象在堆中Eden区分配内存.当Eden区没有足够空间进行分配的时候虚拟机触发一次Minor Gc
- 大对象直接进入老年代,大对象即需要大量连续内存空间的Java对象例如长字符串/数组.
- 长期存活的对象将进入老年代,虚拟机为给每个对象定义了一个对象年龄(Age)计数器.对象在Eden区出生,当发生第一次Minor GC会进入到Survivor空间中并且此时年龄为1.以后每熬过一次Minor GC年龄增加1岁,当年龄达到一定程度(默认15岁,跟狗狗差不多)将会晋升到老年代.
- 动态对象年龄判定:如果在Survivro空间中的相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代.
回收策略
- Minor GC担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于那么此次Minor GC被认为是安全的.如果不成立,则虚拟机会查看HandlePromotionFailure设置是否允许担保失败.如果允许,则检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者handlePromotionFailure设置不允许冒险,那进行一次Full GC.
上帝视角监督虚拟机
JDK命令行监控工具
jstat
JVM Statstics Monitoring Tool,用于收集HotSpot虚拟机各方面的运行数据
jinfo
Configuration Info for java,显示虚拟机配置信息
jmap & jhat
Memory Map for Java & JVM Heap Dump Broswer.配合使用,jmap生成虚拟机的内存转储快照(heapdump文件),jhat用于分析heapdump文件.
jstack
Stack Trace for Java,显示虚拟机的线程快照.
JDK可视化工具
jconsole
VisualVM
提高jvm即战力
寻找性能瓶颈
性能瓶颈主要分成4部分:1.CPU 2.文件IO 3.网络IO 4.内存资源
CPU消耗分析
在Linux中CPU主要用于(1)中断(2)内核(3)用户进程,优先级为中断>内核>用户进程
涉及CPU的三个概念:
- 上下文切换:每个线程分配时间片,运行结束后切换到其他进程
- 运行队列:每个CPU都维护一个可运行的线程队列
- 利用率:CPU利用率为CPU在用户进程,内核,中断处理,IO及等待/空闲五个部分使用百分比
查看CPU利用率:
- top: top -hv | -bcHiOSs -d secs -n max -u|U user -p pid(s) -o field -w [cols]
CPU:us用户进程 sy内核线程处理 ni表示被nice命令改变优先级的任务所占的百分比 id空闲时间 wa:等待IO hi:硬件终端 si:软中断 - pidstat 用法: pidstat [ 选项 ] [ <时间间隔> [ <次数> ] ]
[ -d ] [ -h ] [ -I ] [ -l ] [ -r ] [ -s ] [ -t ] [ -U [ <username> ] ] [ -u ]
[ -V ] [ -w ] [ -C <command> ] [ -p { <pid> [,...] | SELF | ALL } ]
[ -T { TASK | CHILD | ALL } ]
eg:pidstat -p xxxx -t 1 5 查看进程下每个线程的cpu使用情况
对java应用而言CPU消耗主要体现在us,sy两个值:
- 当us过高表示运行的应用消耗了大部分cpu-找到消耗cpu的线程所执行的代码.
- sy高时,表示频繁进行线程切换.原因是线程较多且都处于不断的阻塞和执行状态的变化过程中.
文件IO分析
- 跟踪线程的文件io的消耗: pidstat -d -t -p [pid]
kB_rd/s表示每秒读取的KB数,kB_wr/s表示每秒写入的kb数 - iostat
用法: iostat [ 选项 ] [ <时间间隔> [ <次数> ] ]
选项:
[ -c ] [ -d ] [ -h ] [ -k | -m ] [ -N ] [ -t ] [ -V ] [ -x ] [ -y ] [ -z ]
[ -j { ID | LABEL | PATH | UUID | ... } ]
[ [ -T ] -g <用户组名> ] [ -p [ <设备> [,...] | ALL ] ]
[ <设备> [...] | ALL ]
网路IO消耗分析
sar -n FULL 1 2
内存资源分析
jvm消耗过多会导致GC频繁,CPU消耗增加,应用程序的执行速度严重下降甚至导致OutMemoryError
- pidstat -r -p xxxx 1 2
- jstat eg: jstat -gc xxxx 1 2
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
jstat -options
-class 加载的类信息
-compiler
-gccapacity gc容量
-gccause gc原因
-gcmetacapacity
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcutil
-printcompilation
vmid:进程号 interval:时间间隔 count:次数
程序本身执行慢的原因
- 锁竞争激烈
- 未充分使用硬件资源
- 数据量增长
JVM调优
对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量.
发生Full GC场景
- 旧生代空间不足
调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 - Pemanet Generation空间不足
(1). 增大Perm Gen空间,避免太多静态对象
(2). 统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间
(3). 控制好新生代和旧生代的比例 - System.gc()被显示调用
垃圾回收不要手动触发,尽量依靠JVM自身的机制
调优手段
调优手段主要是通过控制堆内存的各个部分的比例和GC策略来实现,
内存调优
内存比例不良设置会导致一些不良后果:
- 新生代设置过小
一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC - 新生代设置过大
一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;二是新生代GC耗时大幅度增加.
一般说来新生代占整个堆1/3比较合适 - Survivor设置过小
导致对象从eden直接到达旧生代,降低了在新生代的存活时间 - Survivor设置过大
导致eden过小,增加了GC频率。另外,通过-XX:MaxTenuringThreshold=n来控制新生代存活时间,尽量让对象在新生代被回收 - 堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小
GC调优
由内存管理和垃圾回收可知新生代和旧生代都有多种GC策略和组合搭配,选择这些策略对于我们这些开发人员是个难题,JVM提供两种较为简单的GC策略的设置方式。
- 吞吐量优先
JVM以吞吐量为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n来设置 - 暂停时间优先
JVM以暂停时间为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这个值可由-XX:MaxGCPauseRatio=n来设置
jdk提供了好几种GC策略,串行GC性能太差实际场景主要是并行和并发GC - 收集器参数优化
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
参数调优
- 垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
- 并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
- 并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
问题
分析一下代码执行过程中各个区域的使用情况
package io.daocloud.jvm.demo;
/**
* @Author: gaoqiang
* @Date: 2020/1/8 5:11 PM
*/
public class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void wang() {
System.out.println("dog:" + name + "wang wang");
}
}
--------------
package io.daocloud.jvm.demo;
/**
* @Author: gaoqiang
* @Date: 2020/1/8 5:07 PM
*/
public class DemoInfo {
public String target;
private Dog myDog;
public DemoInfo(String target) {
this.target = target;
}
/**
* 领养一只狗狗
*/
public void adapt() {
myDog = new Dog("heheda", 1);
}
/**
* 狗狗长大
*/
public void upDogAge() {
int oldAge = myDog.getAge();
int newAge = oldAge + 1;
myDog.setAge(newAge);
}
public static void main(String[] args) {
//基础类型赋值过程
int step = 1; //(1)
//第一步创建对象,demo是引用类型
DemoInfo demo = new DemoInfo("demo");//(2)
//第二步收养一只狗的过程
step = 2;(3)
demo.adapt();//(4)
//第三步狗狗长大了
step = 3;
demo.upDogAge();//(5)
}
}
分析(1)~(5)过程中的内存分配情况?