目录
jps(JVM Process Status Tool)虚拟机进程监控工具
jstat(JVM Statistics Monitoring Tool)虚拟机统计信息监视工具
jinfo(Configuration Info for Java)配置信息工具
jmap(Memory Map for Java)内存映像工具
jstack(JVM Stack Trace)java堆栈跟踪工具
JVM体系结构
虚拟机是无力及其德软件实现。Java的开发遵循write once run anywhere(“一次编写到处乱跑”)理念,它运行在VM(虚拟机)上。编译器将Java文件编译成Java.class文件,之后,将.class文件输入到JVM中,加在并执行该类文件。下图为JVM的体系结构
JVM各个模块简介
- 运行时数据区:经过编译生成的字节码文件(class文件),由classloader(类加载子系统)加载后交给执行引擎执行。在执行引擎执行的过程中产生的数据会存储在一块内存区域。这块内存区域就是运行时区域
- 程序计数器:由于纪录当前线程的正在执行的字节码指令位置。由于虚拟机的多线程是切换线程并分配cpu执行时间的方式实现的,不同线程的执行位置都需要记录下来,因此程序计数器是线程私有的
- 虚拟机栈:虚拟机栈是Java方法执行的内存结构,虚拟机会在每个Java方法执行时创建一个“栈帧”,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。当方法执行完毕时,该栈帧从虚拟机栈中出栈。其中局部变量包含基本数据类型和对象引用。
- 在Java虚拟机规范中,对这个区域规定了两种异常状态:如果线程请求的栈的深度大于虚拟机允许的深度,将跑出StackOverFlowError异常(栈溢出),如果虚拟机栈可以动态扩展(现在大部分Java虚拟机都可以动态扩展,只不过Java虚拟机规范中也允许固定长度的Java虚拟机栈),如果扩展时无法申请到足够的内存空间,就会抛出OutOfMemoryError异常(没有足够的内存)
- 本地方法栈:类似Java方法的执行有虚拟机栈,本地方法的执行则对应有本地方法栈
- 方法区:用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。线程共享(看存储的数据就知道了)
- Java堆(Heap):堆的主要作用是存放程序运行过程中创建的对象实例,因为要存放的对象实例有可能会极多,因此也是虚拟机内存管理中最大的一块。并且由于硬件条件有限,所以需要不断回收已“无用”的实例对象来腾出空间给新生成的实例对象;因此Java的垃圾回收主要是针对堆进行回收的(还有方法区的常量池),Java堆很多时候也被称为GC堆(Garbage Collected Heap)。
- 类加载机制(Class Loader):类加载子系统是根据一个的全限定名来加载该类的二进制流到内存中,在JVM中将形成一份描述Class结构的元信息对象(方法区),通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能。
JVM是如何工作的
如上面的体系结构图所示,JVM分为三个主要的子系统:
- 类加载器子系统
- 运行时数据区
- 执行引擎
类加载器子系统
Java的动态类加载功能是由类加载器子系统处理的。它负责加载、链接,并且在运行时首次引用类的时候初始化类,而不是在编译期间。
加载
这个组件负责加载类。BootStrap类加载器、Extension类加载器和Application类加载器是实现这个功能的三大类加载器。
- BootStrap类加载器——负责从classpath加载类,如果没有类存在,将只加载rt.jar。这个加载器的优先级最高;
- Extension类加载器——负责加载**扩展文件夹(jre\lib)**中的类;
- Application类加载器——负责加载应用级classpath和环境变量指向的路径下的类。
上述类加载器在加载类文件时遵循委托层次结构算法。
链接
- 校验——字节码验证器将校验生成的字节码是否正确,如果校验失败,我们将获得校验错误信息;
- 准备——对于所有的静态变量,内存将被申请并分配默认值;
- 解析——所有标记的内存引用从方法区域被替换成的原始引用。
初始化
这是类加载的最后阶段,所有的静态变量都将被分配原值,静态代码块将被执行。
运行时数据区被划分为五个主要部分:
- 方法区——所有类级数据都将存储在这里,包括静态变量。每一个JVM只有一个方法区,并且它是一个共享资源。
- 堆区——所有对象及其对应的实例变量和数组等存储在此,每个JVM同样只有一个堆区。由于方法区和堆区是多线程内存共享,因此存储的数据是非线程安全的。
- 栈区——每个线程都会创建一个单独的运行时栈。在每一次方法调用,都会在栈内存中创建一个栈帧(Stack Frame)。所有局部变量将在栈内存中创建。栈区是线程安全的,因为它不是一个共享资源。栈帧可以被划分为三个实体:
- 局部变量数组:与方法中有多少局部变量有关,相应的值将存储在此处
- 操作数栈:如果任何的中间操作需要被执行,操作数栈将作为运行时工作区来执行操作
- 帧数据:与方法相对应的所有符号存储在此,在任何异常情况下,catch块的信息被保留在帧数据中
- PC寄存器——每一个线程都有单独的PC寄存器,一旦执行指令,PC寄存器将被下一条指令更新,保存当前执行指令的地址。
- 本地方法栈——本地方法栈保存本地方法信息,每一个线程都会创建一个单独的本地方法栈。
执行引擎
分配到运行时数据区的字节码将被执行引擎执行,执行引擎读取字节码并逐一执行。
- 解释器——解释器能更加快速地解释字节码,但是执行缓慢。解释器的缺点是当多次调用一个方法时,每次都要重新解释。
- JIT编译器——JIT编译器弥补了解释器的不足,执行引擎使用解释器来转换字节码,当它发现重复的代码时,它将使用JIT编译器来编译整个字节码并转换为本地代码。本地代码将直接被重复的方法所调用,从而提高系统性能。
- 中间代码生成器——生成中间代码。
- 代码优化器——负责优化上述生成的中间代码。
- 分析器——一个特殊的组件,负责查找热点代码,比如一个方法是否被调用多次。
- 垃圾回收器——回收并删除未引用的对象,可以通过**System.gc()**来触发垃圾回收,但不能保证它执行。JVM的垃圾回收是回收被创建的对象。
Java本地接口(JNI):JNI与本地方法库交互,并为执行引擎提供本地方法库。
本地方法库(Native Method Libraries):它是执行引擎所需的本地库集合。
运行时数据区域
程序计数器(线程私有)
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
- 多个线程竞争时被挂起,程序计数器记录执行到哪里
- 唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
虚拟机栈(线程私有)
每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息,从调用直至完成的过程,就对应着一个栈帧在Java虚拟机栈中入栈和出栈过程。
- 补充:栈帧中还存在动态链接、出口(返回地址)等。
可以通过-Xss这个虚拟机参数来指定一个程序的Java虚拟机栈内存大小:
java -Xss=512M HackTheJava
该区域可能抛出以下异常:
- 当线程请求的栈深度超过最大值,会抛出StackOverflowError异常:
- 栈进行动态扩展时如果无法申请到足够内存,会抛出OutOfMemoryError异常。
本地方法栈(线程私有)
本地方法一般是用其他语言(C、C++或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
本地方法栈与Java虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
堆
所有对象实例都在这里分配内存。
是垃圾收集的主要区域(“GC堆”)。现代的垃圾收集器基本都是采用分代收集算法(因为对象的生命周期不一样),主要思想是针对不同的对象采取不同的垃圾回收算法。虚拟机把Java堆分成以下三块:
-
新生代(Toung Generation)
- 对象优先进入的区域
-
老年代(Old Generation)
- 在新生代中经历了N次垃圾回收仍然存活的对象就会进入老年代,配置(-XX:MaxTenuringThreshold=15)
- 大对象直接进入老年代,配置 (-XX:PretenureSizeThreshold)
- 当Survivor空间不够时,需要依赖老年代进行分配担保,所以大对象直接进入老年代,配置(-XX:+HandlePromotionFailure)
-
元空间(MataSpace)
当个一个对象被创建时,首先进入新生代,之后有可能被转移到老年代中。
新生代存放着大量的生命很短暂的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间:
- Eden(伊甸园)
- From Survivor(幸存者)
- To Survivor
Java堆不需要连续内存,并且可以动态增加内存,增加失败会抛出OutOfMemoryError。可以通过-Xms和-Xmx两个虚拟机参数来指定一个程序的Java堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms=1024M -Xmx=1024M Test.java
方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单来说,所有定义的方法的信息都保存在该区域,此区属于共享区间。
- 类信息
- 类的版本
- 字段
- 方法
- 接口
- 静态变量
- 常量
- 类信息(构造方法/接口定义)
- 运行时常量池
方法区:永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装在进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
如果出现java.lang.OutOfMemoryError : PermGen spave,说明是Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。
JDK1.8之后:无永久代,常量池在元空间中存储。
直接内存
在JDK1.4中加入NIO,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
判断一个对象是否可以被回收
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对Java堆和方法区进行。
引用计数算法
描述:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器会加一;当引用失效时,计数器值会减一;任何时刻计数器为0的对象就是不可能再被使用的。
缺陷:GC频繁影响性能,很难解决对象间相互循环引用的问题
可达性分析算法
通过GC Roots作为起点进行搜索,能够到达的对象都是存活的,不可达的对象可被回收。
能够作为GC Roots对象的:
- 虚拟机栈(局部变量表中的)
- 方法区的类属性所引用的对象
- 方法区的常量所引用的对象
- 本地方法栈所引用的对象
引用类型
无论是通过引用计算还是可达算法,判定对象是否可被回收都与引用有关。在JDK1.2之后,Java对引用的概念进行了扩充,分为以下三种:
强引用(Strong Reference)
被强引用关联的对象不会被回收,使用new关键字创建为强引用
Object obj = new Object();
软引用(Soft Reference)
被软引用关联的对象只有在内存不够的情况下才会被回收
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; //使对象只被软引用关联
弱引用(Weak Reference)
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
虚引用(Phantom Reference)
又称为幽灵引用或者欢迎引用。一个对象是否又虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
方法区的回收
Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的“性价比”一般比较低:在堆中,尤其在新生代中,常规的应用一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
finalize()
垃圾收集算法
标记-清除
首先标记出所有需要回收的对象,在标记完成后同一回收所有标记的对象。
不足
- 效率问题:标记和清楚的效率都不高
- 空间问题:标记清楚之后会产生大量不连续的内存碎片,导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另外一次垃圾收集。
标记-整理
标记过程仍然与“标记-清楚”算法一样高,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一段移动,然后直接清除掉边界意外的内存。
复制回收
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。主要不足是只是用了内存的一半。
现在的商业虚拟机都采用这种收集算法来回收新生代,但并不是将新生代划分为大小相等的两块,而是分为一块较大Eden和两块较小Survivor空间,每次使用Eden空间和其中一块Survivor。在回收时,将Eden和Survivor中海存活着的对象一次性复制到另一块Survivor空间上,最后清理Eden和使用过的那一块Survivor。
HotSpot虚拟机的Eden和Survivor的大小比例是8:1:1,保证了内存的利用率达到90%。如果每次回收有多余10%的对象存活,那么一块Survivor空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。
分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
- 新生代使用:复制回收
- 老年代使用:标记-清楚 或 标记-整理 算法
垃圾收集器
以上HotSpot虚拟机中的7个垃圾收集器,连线表示垃圾收集器可配合使用。
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;
- 穿行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器的用户程序同时执行。除了CMS和G1之外,其他垃圾收集器都是以串行的方式执行。
Serial
Serial为串行,也就是说它以串行的方式执行,它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
优点:高效,对于单个CPU环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率
他是Client模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒内,只要不是太频繁,这点停顿是可以接受的。
ParNew
它是Serial收集器的多线程版本。是Server模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了Serial收集器,只有它能与CMS收集器配合工作。
在JDK1.5时期,HotSpot推出了CMS收集器(Concurrent Mark Sweep),它是HotSpot虚拟机中第一款真正意义上的并发收集器。
Parallel Scavenge
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,优势并行的多线程收集器。它的目标是达到一个可控制的吞吐量,它被称为吞吐量优先收集器。
- 吞吐量:CPU用于运行用户代码时间与CPU消耗总时间的比值。
- 吞吐量=执行用户代码时间/(执行用户代码时间+垃圾回收使用的时间)
停顿时间越短越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序地运算任务,主要适合在后台运算而不需要太多交互地任务。
缩短停顿时间以牺牲吞吐量和新生代空间来换取地:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
可以通过一个开关参数打开GC自适应地调节策略,就不许要手动指定新生代地大小等参数了。虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适地停顿时间或最大地吞吐量,这种调节方式称为GC自适应地调节策略。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:
- 最大垃圾收集停顿时间:-XX:MaxGCPauseMillis
- 吞吐量大小:-XX:GCTimeRatio
MaxGCPauseMillis参数允许地值是一个大于0的毫秒数,收集器将尽可能地保证内存回收所花费地时间不超过设定值。但GC地停顿时间缩短是以牺牲吞吐量和新生代空间来换取地。停顿时间下降,但吞吐量也降下来了。
GCTimeRatio参数地值是一个大于0且小于100地证书,也就是垃圾收集时间占总时间地比例,相当于吞吐量地倒数。区间1/(1+99)~1/(1+1),即1%~50%。
由于与吞吐量关系密切,Parallel Scavenge收集器经常称为吞吐量优先收集器
-XX:+UserAdaptiveSizePolicy:GC自适应调节此策略,打开参数后,就不需要手工指定新生代地大小等参数了。
Serial Old
Serial Old是Serial收集器地老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的最主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:
- 在JDK1.5以及之前版本中与Parallel Scavenge收集器搭配使用。
- 作为CMS收集器的后预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
CMS
CMS(Concurrent Mark Sweep),Mark Sweep指的是 标记-清楚 算法。CMS是一款优秀的收集器,主要优点:并发收集、低停顿,Sun公司称之为并发低停顿收集器
流程:
- 初始标记:仅仅只是标记以下GC Roots能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行GC Roots Tracing的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清楚:不需要停顿。
在整个过程中耗时最长的并发标记和并发清楚过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
缺点:
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不够高。
- 无法处理浮动垃圾,可能出现Concurrent Mode Failure。浮动垃圾是指并发清楚阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下次GC时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着CMS收集不能像其他收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现Concurrent Mode Failure,这是虚拟机将临时启用Serial Old来替代CMS 。
- 标记-清楚算法导致的空间碎片,玩玩出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次Full GC。
- CMS提供了一个开关参数-XX:+UserCMSCompactAtFullCollection(默认开启),用于在CMS收集器顶不住要进行Full GC时开启内存碎片合并整理过程,内存整理的过程是无法并发的。
- 参数-XX:CMSFullGCsBeforeCompaction用于设置执行多少次不压缩的Full GC后,跟着来以此带压缩的。(默认值为0)
G1
G1的第一篇paper发表于2004年,在2012年才在jdk1.7中可用。oracle官方计划在jdk9中将G1编程默认的垃圾收集器,以替代CMS。
- 为何oracle要极力推荐G1呢,G1有哪些优点?
- 首先,G1的设计原则就是简单可行的性能调优
- 其次,G1将新生代,老年代的物理空间划分取消了
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多CPU和大内存的场景下有很好的性能。HotSpot开发团队赋予它的使命是未来可以替换掉CMS收集器。
堆被分为新生代和老年代,其他收集器进行收集的范围都是整个新生代或者老年代,而G1可以直接对新生代和老年代一起回收。
G1把堆划分成多个大小相等的独立区域,新生代和老年代不再物理隔离。
通过引入Region的概念,从而将原来的一块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测大的停顿时间模型称为可能。通过记录每个Region垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
每个Region都有一个Remembered Set,用来记录该Region对象的引用对象所在的Region。通过使用Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
具备如下特点:
- 空间整合:整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒。
比较
收集器 | 单线程/并行 | 串行/并发 | 新生代/老年代 | 收集算法 | 目标 | 适用场景 |
Serial | 单线程 | 串行 | 新生代 | 复制 | 响应速度优先 | 单CPU环境下的Client模式 |
ParNew | 并行 | 串行 | 新生代 | 复制 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 串行 | 新生代 | 复制 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Serial Old | 单线程 | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后背预案 |
Parallel Old | 并行 | 串行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并行 | 并发 | 老年代 | 标记-清楚 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并行 | 并发 | 新生代+老年代 | 标记-整理+复制 | 响应速度优先 | 面向服务端应用,将来替换CMS |
内存分配与回收策略
什么时候进行Minor GC,Full GC
- Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此Minor GC会频繁执行,执行的速度一般也会比较快。
- 新生代中的垃圾收集动作,采用的是复制算法
- 对于较大的对象,在Minor GC的时候可以直接进入老年代
- Full GC:发生在老年代上,老年代对象其存活时间长,因此Full GC很少执行,执行速度会MinorGC慢很多。
- Full GC是发生在老年代的垃圾收集动作,采用的是标记-清楚/整理 算法
- 由于老年代的对象几乎都是在Survivor区,不会那么容易死掉。因此Full GC发生的次数不会有Minor GC那么频繁,并且Time(Full GC)>Time(Minor GC)
内存分配策略
对象优先在Eden分配
大多数情况下,对象在新生代Eden区分配,当Eden区空间不够时,发起Minor GC。
大对象直接进入老年代
大对象指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常会出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在Eden区和Survivor区之间的大量内存复制。
长期存活的对象进入老年代
为对象定义年龄计数器,对象在Eden出生并经过Minor GC依然存活,将移动到Survivor中,年龄就增加1岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold用来定义年龄的阈值。
动态对象年龄判定
虚拟机并不是永远的要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor中相同年龄所有对象大小的综合大于Survivor空间的一般,则年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
空间分配担保
在发生Minor GC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的化,那么Minor GC可以确认是安全的。
如果不成立的化虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那么就要进行一次Full GC。
Full GC触发条件
对于Minor GC,其触发条件非常简单,当Eden空间满时,就会触发一次Minor GC。而Full GC相对复杂,有以下条件:
调用System.gc()
只是建议虚拟机执行Full GC,但是虚拟机不一定真正执行。不建议使用这种方式,而是让虚拟机管理内存。
老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过-Xmn虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过-XX:MaxTenuringThreshold调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
空间分配担保失败
使用复制算法的Minor GC需要老年代的内存空间作担保,如果担保失败会执行一次Full GC。
Concurrent Mode Failure
执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足(可能GC过程中浮动垃圾过多导致暂时性地空间不足),便会报Concurrent Mode Failure错误,并触发Full GC。
类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机类加载机制。(类是在运行期间动态加载的)
懒加载:要用的时候再去加载
类的生命周期
包括以下7个阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持Java的动态绑定。
这七个阶段中的:加载、验证、准备、初始化、卸载的顺序时固定的。但他们并不一定时严格同步串行执行,特们之间可能会有交叉,但总是以“开始”的顺序总是按部就班的。至于解析则有可能在初始化之后才开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
类初始化时机
主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列5种情况必须对类进行初始化(加载、验证、准备都会随之发生):
- 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这4条指令的场景是:使用new关键字实例化对象的时候;读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类;
- 当使用JDK1.7动态语言支持时,如果一个java.lang.invoke.MethodeHandle实例最后的解析结果为REF_getStatic, REF_putStatic, REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先出发其初始化;
被动引用
- 通过子类引用父类的静态字段,不会导致子类初始化。
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
-
通过数组定义引用类,不会触发此类的初始化。
SuperClass[] sca = new SuperClass[10];
- 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
System.out.println(ConstClass.HELLOWORLD);
类加载过程
加载
加载是类加载的一个阶段。加载过程完成以下三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构
- 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
加载源:
- 文件
- 网络
- 计算生成一个二进制流
- 其他文件生成
- 数据库
验证
目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
解析
初始化
类加载器
类与类加载器
类加载器分类
双亲委派类型
Java虚拟机工具
jps(JVM Process Status Tool)虚拟机进程监控工具
列出正在运行的虚拟机进程,并显示虚拟机执行主类名称,以及这些进程的本地虚拟机唯一ID。
options参数选项说明如下:
-q 不输出类名、Jar名和传入main方法的参数
-m 输出传入main方法的参数
-l 输出main类或Jar的全限名
-v 输出传入JVM的参数
使用jps -lv 查看所有java进程。
jstat(JVM Statistics Monitoring Tool)虚拟机统计信息监视工具
这个命令用于监视虚拟机各种运行状态信息。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,虽然没有GUI图形界面,知识提供纯文本控制台环境的服务器上,但它是运行期间定位虚拟机性能问题的首选工具。
[root@docker-r720-3 ~]# jstat -options
-class
-compiler
-gc
-gccapacity
-gccause
-gcmetacapacity
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcutil
-printcompilation
为了研究,我们来一一尝试这些命令
jinfo(Configuration Info for Java)配置信息工具
这个命令可以实时查看和调整虚拟机各项参数
例如:查看MaxPerm大小
[root@Bill-8 bin]# jinfo -flag MaxPermSize 2788
-XX:MaxPermSize=134217728
jmap(Memory Map for Java)内存映像工具
用于生成堆转存的快照,一般是heapdump或者dump文件。如果不适用jmap命令,可以使用 -XX:+HeapDumpOnOutOfMemoryError参数,当虚拟机发生内存溢出的时候就会产生快照。或者使用kill -3 pid也可以产生。jmap的作用不仅仅是为了获取dump文件,它可以查询finalize执行队列,java堆和永久代的详细信息,如空间使用率,当前用的哪种收集器。
jhat虚拟机堆转储快照分析工具
jstack(JVM Stack Trace)java堆栈跟踪工具
这个命令用于查看虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。
生成线程快照的主要目的是:定位线程出现长时间停顿的原因,入线程间死锁、死循环、请求外部资源导致的长时间等待都是导致线程长时间停顿的原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的县茨城到底在后台做些什么事情。
jconsole
可以监视JVM内存的使用情况、线程堆栈跟踪、已装入的类和VM信息以及CE MBean。
是一个java GUI监视工具,可以以图标化的形式显示各种数据。并可以通过远程连接监视远程的服务器VM。用Java写的GUI程序,用来监控VM,并可以监控远程VM,非常易用,而且功能非常强。
jvisualvm