JVM虚拟机(一)
JVM虚拟机
自动内存管理
对于Java
程序,虚拟机自动内存管理使开发人员不需要为每一个new
操作写对应的delete
、free
操作,所以不容易出现内存泄漏和内存溢出
Java虚拟机
在执行Java
程序的过程中会将虚拟机管理的内存分为不同的区域
程序计数器
是一个较小的内存空间,可看作当前线程执行字节码的行号显示器,用于指定下一条需执行的语句。由于
Java虚拟机
内的多线程是通过线程轮流切换、分配处理器执行时间的方式实现,每一个线程都需要有一个独立的程序计数器
,各条线程之间互不影响,独立存储。(线程隔离)若线程执行
Java
方法,计数器
记录的就是正在执行的虚拟机字节码指令的地址;若线程执行本地(Native)方法,计数器值为空。
此区域是唯一一个不会引起
Out Of Memory Error
情况的区域
虚拟机栈
生命周期与线程相同。每个方法在执行的时候,
Java虚拟机
都会为其创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从出栈到入栈的过程。(线程隔离)局部变量表存放了编译期间的各种
Java
基本数据类型、对象引用和Return Address类型。这些数据类型在局部变量表中的存储空间以局部变量槽表示,其中64位的long和double类型数据会占用两个变量槽。当进入一个方法时,这个方法在栈帧中分配多大的局部变量空间是确定的,在方法运行期间不会改变局部变量槽的数量。
若线程请求的栈深度大于虚拟机允许的深度时,将抛出
Stack Overflow Error
异常;若
Java虚拟机
允许虚拟机栈动态扩展,当栈扩展无法申请到足够的内存会抛出Out Of Memory Error
异常。
本地方法栈
与虚拟机栈的功能作用相似。区别是虚拟机栈在执行Java方法(字节码)时使用,
本地方法栈
在执行本地方法(Native)时使用。(线程隔离)若线程请求的栈深度大于虚拟机允许的深度时,将抛出
Stack Overflow Error
异常;若
Java虚拟机
允许虚拟机栈动态扩展,当栈扩展无法申请到足够的内存会抛出Out Of Memory Error
异常。
Java堆
Java堆
是虚拟机所管理内存中最大的一块,Java堆
在虚拟机启动时创建,用于存放对象实例,几乎所有的对象实例都在这里分配内存。(线程共享)
Java堆
是垃圾收集器管理的内存区域,也被称为GC堆
,现代垃圾收集器大部分基于分代收集理论设计,故会出现“新生代
”、“老年代
”、“永久代
”等概念。
Java堆
可以处于物理上不连续的内存空间,但在逻辑上应被视为连续的。
Java堆
可以被实现为固定大小,也可以是可扩展的(通过参数-Xmx
和-Xms
进行设定)。
方法区
方法区
用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
方法区
的约束是非常宽松的,不需要连续的内存和可以选择固定大小或可扩展,也可以不实现垃圾收集。
方法区
的未完全回收会导致内存泄漏,从而导致bug
方法区
无法满足新的内存分配需求时,将抛出Out Of Memory Error
异常
运行时常量池
运行时常量池是方法区的一部分,HotSpot将运行时常量池移入了堆中。
垃圾回收
Java运行时,程序计数器、虚拟机栈、本地方法栈三个区域为线程私有,随线程而生,随线程而亡。栈中的栈帧随着方法的进入和退出进行入栈和出栈的操作,这三个区域的内存回收具有确定性,方法结束或线程结束时,内存会随之而回收。
需要开发者考虑的内存回收区域只有堆和方法区,因为编译运行期间不能完全确定会创建多少对象。
垃圾回收判断
引用计数算法
每创建一个对象,都为创建的对象增加一个引用计数器。每当对象被引用,引用计数器+1;每当引用失效,引用计数器-1。当引用计数器为0时,表明该对象为无效对象,可进行垃圾回收。
但这种算法无法解决对象循环引用的情况,故Java回收不考虑该算法。
根搜索算法
通过GC Roots作为对象的终点,当一个对象无法通过引用到达GC Roots,该对象即可被回收。
在Java中有四种对象可作为GC Roots:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中的静态变量属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈(JNI)中引用的对象
垃圾回收算法
标记-清除算法
首先根据根搜索算法标记出需回收的对象,标记完成后,统一回收被标记的对象
缺点:
- 效率低:标记和清除的过程效率较低
- 容易产生内存碎片:内存的申请通常是连续的,清除部分对象后,会产生大量不连续的内存碎片;碎片过多时,当大对象需要分配内存时,便会造成没有足够连续内存分配而提前触发垃圾回收,甚至直接抛出OutOfMemoryException异常
复制算法
将可用内存划分为大小相等的两块区域,每次使用其中一块,内存用完后,将存活的对象复制到另一块区域,将已使用的区域进行内存回收
优点:每次只对一半内存进行回收,不考虑内存碎片问题,分配内存只需要移动堆顶指针,简单高效
缺点:将内存分为两块,可使用内存有一半为空闲,造成资源浪费。若对象存活率高时,每次都需要复制大量对象,效率会降低
标记-整理算法
若对象存活率高,基本不需要进行垃圾回收时,考虑标记-整理算法
标记所存活的对象,将所有存活的对象向一端移动,最后清理到端边界以外的内存
局限性:只有对象存活率高时,标记整理算法效率才会高
分代回收算法
根据对象存活的周期不同将内存分为几块,根据不同的区域采用不同的回收算法
- 新生代:存货周期短,每次都有大量对象死亡,只有少量对象存活,即采用复制算法
- 老年代:存货周期长,没有额外空间进行分配担保区域,采用标记-整理算法或标记-清除算法
新生代分为三个部分,分为Eden、From Survivor、Survivor区域,比例为8:1:1
每次使用一块Eden区域和一块Survivor区域,进行垃圾回收时,将Eden和Survivor区域所有存活的对象复制到另一块Survivor区,然后清理刚存放对象的区域,依次循环
开发者可以调用System.gc()方法,手动对内存进行回收。
新生代垃圾收集器
Serial收集器
Serial收集器是最基本的收集器,单线程。Serial收集器只使用一个CPU、一条收集线程完成垃圾收集工作,而且在收集过程中必须暂停其他所有的工作线程,直至其收集完成。Serial收集器是虚拟机运行在Client模式下的默认新生代收集器,简单且高效。由于没有线程交互的开销,专心做垃圾回收可以获得最高的单线程收集效率。
ParNew收集器
ParNew收集器是Serial收集器的多线程版本,除了使用多线程收集垃圾之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全相同。ParNew收集器是虚拟机运行在Server模式下的默认新生代收集器,其中与性能无关但很重要的原因是,除了Serial收集器外,只有ParNew收集器能与CMS收集器配合工作。
CMS作为老年代收集器,无法与JDK1.4中已经存在的新生代收集器Parallel Scavenge配合工作。在JDK1.5中,老年代收集器CMS只能选择ParNew或Serial收集器中的其中一个配合工作。
ParNew收集器在单CPU环境中不会比Serial有更好的效果,甚至存在线程交互的开销。随着可使用的CPU数量增加,ParNew收集器对于GC时系统资源的有效利用有好处,默认开启的收集线程数与CPU数量相同可以使用,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
Parallel Scavenge收集器
Parallel Scavenge收集器是新生代收集器,是使用复制算法的收集器,而且是并行的多线程收集器。CMS收集器关注的点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器目标是达到一个可控制的吞吐量(CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / 运行用户代码时间 + 垃圾收集时间)
停顿时间越短越适合需要与用户交互的程序,良好的响应速度能提升用户体验;而高吞吐量可以高效率利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis参数,直接设置吞吐量大小-XX:GCTimeRatio参数
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能保证内存回收花费的时间不超过设定值。GC停顿时间缩短是以牺牲吞吐量和新生代空间换取的:系统将新生代调小一些,也有可能导致垃圾收集发生更频繁,停顿时间会下降,但吞吐量也将下降
GCTimeRatio参数的值应当是一个大于0小于100的整数,即垃圾收集时间占总时间的比率,相当于吞吐量的倒数
UseAdaptiveSizePolicy参数是一个开关参数,若打开该参数就不需要手工指定新生代大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)与晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数。这样的调节方式称为GC自适应调节策略(GC Ergonomics),当手工优化存在困难时,可以将内存管理交给虚拟机,只需要将基本的内存数据设置好(-Xmx设置最大堆),使用MaxGCPauseMillis参数或GCTimeRatio参数给虚拟机设立优化目标,这就是Parallel Scavenge收集器与ParNew收集器的重要区别
老年代垃圾收集器
Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,使用标记-整理算法。主要适用于虚拟机在Client模式下使用,在Server模式下,在JDK1.5版本以前可以与Parallel Scavenge收集器配合使用,或者作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,是一个多线程收集器,使用标记-整理算法。Parallel Old收集器在JDK1.6后才开始提供,在此之前,新生代选择Parallel Scavenge收集器,老年代只能选择Serial Old收集器。但Serial Old收集器在服务端应用上性能的拖累,会导致Parallel Scavenge收集器不能在整体应用中获得吞吐量最大化的效果,由于单线程的老年代收集器无法充分利用CPU等资源,在老年代很大且硬件高级的环境中,这样的组合吞吐量效果较差
直到Parallel Old收集器出现,吞吐量优先的收集器组合出现,在注重吞吐量和CPU资源的场合,优先使用Parallel Scavenge收集器和Parallel Old收集器的组合
CMS收集器
CMS(Concurrent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器,互联网和B/S系统的服务端重视服务的响应速度,希望系统停顿时间最短,给用户带来良好体验,CMS收集器符合该类应用的需求。CMS收集器基于标记-清除算法,整个过程分为四个步骤:
- 初始标识(CMS initial mark)
- 并发标识(CMS concurrent mark)
- 重新标识(CMS remark)
- 并发清除(CMS concurrent sweep)
初始标识和重新标识仍然需要“Stop The World”,初始标识仅仅标识GC Roots能关联到的对象,速度快,并发标识就是进行GC Roots Tracing的过程;而重新标识是为了修正并发标识期间因用户程序发生继续运作而导致标识产生变动的一部分对象的标记记录。初始标识和重新标识的时间略短
并发标识和并发清除是整个过程中耗时最长的,在清除过程中,收集器线程可以与用户线程一起工作。故从整体过程来看,CMS收集器的内存回收过程是与用户线程并发执行的
CMS收集器的优点在于并发收集、低停顿;而CMS收集器的缺点在于
- CMS收集器对CPU资源十分敏感,在并发期间,虽然不会导致用户线程停顿,但因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,随着CPU数量增加而下降;当CPU数量不足4个时,CMS对用户程序的影响就会变得很大,为了应付这样的情况,虚拟机提供了增量式并发收集器(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种,在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的资源独占时间,垃圾收集的过程会更长,但对于用户程序的影响会少一点
- CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure失败而导致另一次Full GC的出现。CMS并发清理阶段用户线程还在运行,伴随程序运行还会有新垃圾产生,新垃圾出现在标识过程后,CMS无法在当次收集中处理,只有留在下次GC时清理,这一部分新垃圾称为浮动垃圾。CMS收集器不会等老年代几乎完全填满后再进行收集,需要预留部分空间提供并发收集时的程序运作使用。JDK1.5默认设置下,CMS收集器当老年代使用了68%的空间后激活,在应用中老年代增长不是太快的情况下,参数-XX:CMSInitiatingOccupancyFraction的值可以调高,从而调高触发的百分比,以降低内存回收次数从而获取更好的性能。JDK1.6中,CMS收集器的启动阈值已提升至92%,若CMS运行期间预留的内存无法满足程序需要,就会出现Concurrent Mode Failure失败,虚拟机就会启动后备预案,临时启用Serial Old收集器重新进行老年代的垃圾收集,可如此会导致停顿时间变长。故该参数设置过高容易导致大量Concurrent Mode Failure失败的产生,性能反而降低
- CMS收集器基于标记-清除算法实现的收集器,收集结束时会有大量空间碎片产生,空间碎片过多将会给对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,无法找到足够大的连续空间分配当前对象,不得不提前触发Full GC,CMS收集器提供了-XX:+UseCMSCompactAtFullCollection开关参数(默认开启)用于在CMS收集器要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程无法并发,空间碎片问题消失,但停顿时间会变长。虚拟机提供了-XX:CMSFullGCsBeforeCompaction参数,用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的Full GC
新垃圾收集器
G1收集器
Garbage First(G1)收集器是面向服务端应用的收集器,G1收集器已经取代了Parallel Cavenge收集器和Parallel收集器的组合,成为了服务端模式下的默认垃圾收集器,CMS则变为不推荐使用的垃圾收集器。对JDK1.9以上版本使用参数-XX:+UseConcMarkSweepGC来开启CMS收集器会受到一个警告信息。
GC日志
阅读GC日志是处理Java虚拟机内存问题的基础技能
GC日志前的数字代表了GC发生的时间,含义是从Java虚拟机启动以来经历的秒数
GC日志开头的GC和Full GC说明了垃圾收集的停顿类型,并不是区分新生代GC和老年代GC。若有Full,说明这次的GC发生了Stop-The-World
存在Stop-The-World的收集器会出现Full GC的标志,例如ParNew收集器发生GC时,日志会显示Full GC;若调用System.gc()方法触发收集,会显示Full GC (System)
DefNew、Tenured、Perm等表示GC发生的区域,与使用的GC收集器密切相关;Serial收集器的新生代名为DefNew,Default New Generation;ParNew收集器的新生代名为ParNew,Parallel New Generation;Parallel Scavenge收集器的新生代名为PSYoungGen。老年代与永久代同理,由收集器名称决定
- 方括号内部的3324K->152K(3712K)表示GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)
- 方括号外部的3324K->152K(11904K)表示GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)
0.0025952 secs表示该内存区域GC所占用时间,某些收集器会给出更具体的时间数据[Times:user=0.01 sys=0.00,real=0.02secs]
- user表示用户态消耗的CPU时间
- sys表示内核态消耗的CPU时间
- real表示操作从开始到结束所经过的墙钟时间(Wall Clock Time)
JVM参数
标准参数
参数 | 描述 |
---|---|
UseSerialGC | 虚拟机运行在Client模式下的默认收集器,打开此开关后,使用Serial收集器和Serial Old收集器组合进行内存回收 |
UseParNewGC | 打开此开关后,使用ParNew收集器和Serial Old收集器组合进行内存回收 |
UseConcMarkSweepGC | 打开此开关后,使用ParNew收集器、CMS收集器和Serial Old收集器组合进行内存回收。Serial Old收集器作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用 |
UseParallelGC | 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge收集器和Serial Old收集器的组合进行内存回收 |
UseParallelOldGC | 打开此开关后,使用Parallel Scavenge收集器和Parallel Old收集器组合进行内存回收 |
SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Survivor=8:1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小,设置该参数后,大于该参数的对象将直接在老年代分配 |
MaxTenuringThreshold | 晋升到老年代的对象年龄,每个对象在坚持过一次Minor GC后,年龄+1,超过该参数后就进入老年代 |
UseAdaptiveSizePolicy | 动态调整Java堆中各个区域的大小以及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
GCTimeRatio | GC时间占总时间的比率,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge收集器时生效 |
MaxGCPauseMillis | 设置GC的最大停顿时间,仅在使用Parallel Scavenge收集器时生效 |
CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后触发垃圾收集,默认值为68%,仅在使用CMS收集器时生效 |
UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是是否要进行一次内存碎片整理,仅在使用CMS收集器时生效 |
CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用CMS收集器时生效 |