JVM基础知识及拓展
Posted huaiyinmarquis
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM基础知识及拓展相关的知识,希望对你有一定的参考价值。
我们可以吧JVM的基本结构分为四块:类加载器、执行引擎、运行时数据区和本地接口。一般来说Java程序在JVM中的执行流程如下:
①、首先我们会利用javac命令将我们所编写的.java源代码文件变异成.class文件 ;
②、类加载器将.class文件加载到运行时数据区;
③、利用执行引擎调用本地接口(本地方法库)执行程序;
这样我们的java程序也就跑起来了。下面我们通过上面所说的四块JVM结构来进行深入学习。
类加载器分类为三种:
BootStrap Classloader:引导类加载器,负责Java核心类(rt.jar)的加载。
Extension Classloader:扩展类加载器,负责JRE扩展目录中jar包的加载。
App Classloader:系统类加载器,负责classpath环境变量所指定的class文件以及jar
类的装载经历了从加载、连接(连接包括了验证、准备和解析三个部分)、初始化、实例化、使用、卸载这六个阶段。类的加载过程如下:
①通过类的全限定名获取定义此类的二进制流
②将字节流所代表的静态存储结构转化为方法区的运行时数据结构
③在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
关于类的加载我们还得注意双亲委派这个问题,双亲委派通俗来说指的是类加载器在加载的过程当中如果有parent的话最终会利用parent来进行实际的操作。我们可以通过观察ClassLoader的源码即可得知:
1 protected Class<?> loadClass(String name, boolean resolve) 2 throws ClassNotFoundException 3 { 4 synchronized (getClassLoadingLock(name)) { 5 // First, check if the class has already been loaded 6 Class<?> c = findLoadedClass(name); 7 if (c == null) { 8 long t0 = System.nanoTime(); 9 try { 10 if (parent != null) {//如果父类加载器不为空便调用父类加载器的方法进行加载类的操作 11 c = parent.loadClass(name, false); 12 } else { 13 c = findBootstrapClassOrNull(name); 14 } 15 } 16 ...... 17 return c; 18 } 19 }
连接过程操作其中也包含了验证、准备和解析的过程。它们详细的处理如下:
验证过程:确保Class文件中字节流包含的信息符合当前虚拟机的要求,并且不会虚拟机自身的安全。包括:文件格式验证、元数据验证、字节码验证、符号引用验证。
准备过程:正式为变量分配内存并设置类变量初始值(零值)的阶段,这些变量所有的内存都在方法区进行分配
解析过程:虚拟机将常量池内的符号引用(符号引用以一组符号来描述所引用的目标,可以是任何形式的字面量,只要使用时无歧义的定位到目标即可,与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中)替换为直接引用(直接引用是可以直接指向目标的指针、相对偏移量或是间接定位到目标的句柄。与内存布局相关,必定已经加载到内存中)的过程。解析动作主要针对类或者接口、字段、类方法、方法类型、方法句柄、调用点限定符这七类符号引用进行解析。
初始化过程:初始化阶段是执行类类构造器<client>()方法的过程,在<client>()方法中,静态语句块中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,在前面的静态语句块可以赋值,但不能访问。
ps:关于类加载这一块更加详细的代码验证,大家可以看看BaseDemo这个项目中"/src/test/java/com/exampleDemo/ClassloaderTest"这个地址下面的关于类加载器的一系列测试代码,以帮助自己更深入的了解类加载器。
我们再类初始化的五种条件:
①、遇到new、gerstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
②、对类进行反射调用的时候
③、初始化一个类的时候,其父类还没有被初始化
④、虚拟机启动时初始化main()方法所在的类
⑤、当使用JDK1.7动态语言支持时
说完类的初始化我们再看看对象的实例化做的事情:
①、当虚拟机遇到一个new的指令时,首先检查这个new的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、连接和初始化过。如果没有则执行相应的类加载的过程。
②、类加载通过后,由虚拟机给新生对象分配内存(内存大小在类加载的过程中就已经确定),等于把一块确定大小的内存从Java堆中划出来。
③、虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。
④、设置对象头:
1,存储对象自身运行时数据:如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;
2,类性指针(即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例)。
⑤、执行<init>方法。
关于Java对象的生命周期我们应该熟知一下几个阶段:
创建阶段:1、为对象分配存储空间;2、开始构造对象;3、从超类到子类对static成员进行初始化;4、超类成员变量顺序初始化,递归调用超类构造方法;5、子类成员变量初始化,子类构造方法调用。
应用阶段:对象至少被一个强引用持有
不可见阶段:程序本身不再持有该对象的任何强引用,虽然这些引用依然存在。简单来说就是程序的执行已经超出该对象的作用域了。这种情况下,该对象可能被JVM等系统已经装载的静态变量或者线程或者JNI(GC ROOT)等强引用持有,可能存在
不可达阶段:对象不再被任何强引用持有。
收集阶段:可能执行finalize()方法
对象空间的重新分配
我们可以谈谈对象(普通对象,不包括数组和Class对象)的创建:
首先检查该new指令的参数是否能在常量池定位到一个类的符号引用,并检查这个符号所代表的类是否已经被加载、解析和初始化过。如果没有则先执行相应的类加载的过程。在类加载通过后,虚拟机为新生对象分配内存。分配内存有“指针碰撞(内存是绝对规整的,仅仅移动指针实现)”和“空闲列表(虚拟机会维护一个记录了哪些内存块可用的列表,在列表中找到内存可用的空间,分配完毕后更新列表)”两种方式。分配方式由垃圾收集器是否带有压缩整理的功能决定(Serial、ParNew等带有Compact过程,系统采用指针碰撞;CMS使用的是Mark-Sweep算法,系统采用空闲列表)。给新对象分配内存空间可能出现线程不安全的问题,两种解决方案:一,对分配内存空间进行同步处理;二,把内存分配的动作按照线程划分在不同的内存空间中进行,即每个线程在Java堆中预先分配一小块内存(本地线程分配缓冲Thread Local Allocation Buffer,TLAB)。只有当TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。对象在内存中的存储布局分为三部分:对象头、实例数据和对齐填充。对象的访问方式有两种:使用句柄和使用直接指针。
JVM运行时数据区
java运行时数据区也是我们需要了解的重中之重,我们先从它的结构说起。
程序计数器:可以看做当前线程所执行的字节码的行号指示器。通过改变计数器的值选取下一条所要执行的字节码指令(分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来实现)。每一个线程都有独立的程序计数器。如果当前线程执行的事Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的Native方法,这个计数器值为Undefined。此内存区域是唯一在Java虚拟机规范中没有定义OutOfMemoryError的区域。
虚拟机栈:线程私有,生命周期跟线程相同。每个方法执行的时候都会创建一个栈帧,里面记录了方法里面的局部变量表、操作栈、动态链接、方法出口等信息。每个方法到执行完,就对应入栈到出栈的过程。栈是由栈帧组成的,而栈帧则分为三个部分。它们分别是局部变量区(一个数组,存放了方法内局部变量的值)、操作数栈(临时数据存储区域,通过入栈和出栈来操作数据)和帧数据区(支持常量池解析、正常方法返回以及异常派发机制)。
本地方法区:对应native方法,也会抛出StackOverFlowError和OutOfMemoryError。
方法区:存储了已被虚拟机加载的类信息、静态变量、常量、即时编译器编译后的代码等。在Hotspot虚拟机中也叫永久代,GC的主要目标是对常量池的回收和对类型的卸载。存放Class文件(包含类的版本、字段、方法、接口的等描述信息)叫非运行时常量池;用于存放编译期间生成的各种字面量和符号引用,这部分内容存放在运行时常量池。
直接内存(堆外内存):直接内存不是虚拟机运行时数据区的一部分,不会受到Java堆大小的限制。会受到本机总内存大小以及处理器寻址空间的限制。我们经常会使用java.nio.DirectByteBuffer来进行堆外内存的管理和使用。使用堆外内存的优点是减少垃圾回收和加快复制速度;其相应的缺点是使内存难以控制,发生内存溢出问题排查起来很困难。
垃圾回收机制
判断对象是否将被回收有两种算法:
引用计数算法:给对象添加一个引用计数器,对象每被引用一次引用计数器都会+1;当引用失效的时候,引用计数器都会-1。任何时刻引用计数器为0的对象就是不能再被使用的。引用计数算法最大的缺陷是很难解决对象相互循环引用的问题。Java虚拟机不是通过引用计数法来判断对象是否存活。
根搜索算法:以GC Root对象为起点,从这些对象往下搜索,搜索的路径为“引用链”,如果一个对象通过引用链不可达GC Root对象,则该对象应该被回收。GC Root对象包括:1、虚拟机栈(栈帧中的本地变量表)引用的对象;2、常量所引用的对象;3、静态变量;4、本地方法栈中JNI的对象所引用的对象。
在根搜索算法中不可达的对象,在被回收前要经历两个标记的过程:
第一次标记的时候,除了被标记还会进行一次筛选。筛选(该对象是否有必要执行finalize()方法)的条件是该对象有没有覆盖finalize()方法且是否已经执行过finalize()方法。如果对象覆盖了finalize()方法且没有执行过finalize()方法,则对象会被放入一个名为“F-Queue”的队列中,并且在稍后由一个系统自动创建的,优先级低的Finalizer线程去执行(会触发这个方法,但是不承诺会运行结束,因为怕执行缓慢或者死循环等导致整个内存回收系统崩溃)。finalize()方法是对象逃脱被回收的最后一次机会。一个对象的finalize()方法只会被系统调用一次。
第二次标记的时候,如果对象(通过finalize()将this赋值出去)的引用链能够到达GC Root,则会将该对象移除“即将回收”的集合。
永久代的垃圾收集分为两部分内容:
废弃常量:回收条件与堆中对象非常类似。
无用的类:1、堆中不存在该类的任何实例;2、加载该类的ClassLoad已经被回收;3、该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。对满足了以上三个条件的类可以进行回收,是否进行回收HotSpot提供了-Xnoclassgc参数进行控制。在大量使用反射、动态代理、CGLib等bytecode框架场景,以及动态生成JSP和OSGi这类自定义ClassLoad的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
在Java当中有四种引用类型:
强引用(Strong Reference):Object obj = new Object();只要强引用还存在,垃圾回收器永远不会回收被引用的对象。
软引用(Soft Reference):用来描述还有用但非必须的对象。在系统将要发生内存溢出之前,系统会收集软引用对象列入回收范围之中并进行二次回收。如果这次回收之后内存还是不够,才会抛出内存溢出。JDK提供了SoftReference类来实现软引用。
弱引用(Weak Reference):非必须对象,只能活到下一次垃圾收集发生之前。JDK 提供了WeakReference类实现了弱引用。
虚引用(Phantom Reference):幽灵引用或幻灵引用。一个对象是否有虚引用的存在完全不会对其存在时间构成影响,也无法通过虚引用获取对象实例。其唯一的作用是通过虚引用获取对象在被垃圾收集器回收时受到一个系统通知。JDK提供了PhantomReference类来实现虚引用。
垃圾收集器算法一般有以下几种:
标记-清除(Mark-Sweep)算法:首先标记要被回收的对象,标记完成后统一回收掉所有被标记的对象。缺点是:标记清除的效率都不高;在标记清除之后会产生大量不连续的内存碎片。
复制算法:将内存分为相同的两块,每次只使用其中一块,当内存快使用完的时候,将还存活的对象复制到另外一块未使用的内存,然后把已使用的内存空间一次清理掉。缺点是可使用内存变为原来的一半。一半用来回收新生代。一般分为三个空间:Eden、Survivor1和Survivor2。比例一般为8:1:1。
标记-整理(Mark-Compact)算法:首先标记要被回收的对象,标记完成后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集(Generational Collection)算法:根据对象的存活周期的不同将内存划分为几块。一般把Java堆划分为新生代和老年代,根据各个年代的特点采用不同的收集算法。
接下来我们看看JVM历代所使用的垃圾收集器:
Serial收集器:(Client模式下虚拟机默认新生代收集器)单线程的收集器。特点是“Stop The Wrold”,当它在工作时必须暂停其他所有的工作线程。简单高效,没有线程交互的开销。
ParNew收集器:(Server模式下虚拟机首选新生代收集器)Serial收集器的并行多线程版本。只有它能与CMS收集器配合工作。使用-X:+UseConcMarkSweepGC选项后默认新生代收集器,也可以使用-X:+UseParNewGC强制指定。使用-XX:ParalelGCThreads参数来限制垃圾收集的线程数。
Parallel Scavenge收集器:(新生代收集器)并行多线程收集器。目标是达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。高吞吐量可以高效利用CPU时间,尽快完成程序运行任务,主要适合后台运算不需要太多交互的任务。最大垃圾收集停顿时间使用-XX:MaxGCPauseMillis设置,直接设置吞吐量大小用-XX:GCTimeRatio参数设置。自适应调节策略(GC Ergonomics):-XX:+UseAdaptiveSizePolicy是一个开关参数。当这个参数打开后,就不需要手动指定新生代大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代年龄(-XX:PretenureSizeThreshold)等详细参数,虚拟机回收机会根据当前系统运行的情况收集性能监控信息,动态调整参数以提供最适合的停顿时间或最大吞吐量。
Serial Old收集器:(Serial收集器的老年代版本)单线程收集器。使用了标记-整理算法。用途:在client模式下的虚拟机使用、在jdk1.5之前与Parallel Scavenge收集器搭配使用、作为CMS收集器的后备预案。
Parallel Old收集器:(Parallel Scavenge收集器的老年代版本)多线程使用标记-整理算法。jdk1.6之后提供。与parallel Scavenge搭配成“吞吐量优先”组合。
CMS收集器(Concurrent Mark Sweep):(Concurrent Low Pause Collector)并发低停顿收集器(互联网站或B/S)目标是获取最短回收停顿时间。重视服务响应速度,适用于需要与用户交互的程序。基于标记-清除算法实现。
CMS收集器运作过程分为四步:初始标记、并发标记、重新标记和并发清除。其具体过程如下:
初始标记:(需要“Stop The World”)仅仅标记了一下GC Root能够关联到的对象,速度很快;
并发标记:就是进行GC Root Tracing的过程;
重新标记:(需要“Stop The World”)为了修正并发标记期间,因为用户程序运作导致标记产生变动的那一部分对象的标记记录,这一阶段时间比初始标记阶段稍长,远比并发标记阶段时间短;
CMS收集器的缺点也是很明显的,其主要缺点及其应对方案如下:
对CPU资源敏感:并发阶段导致总吞吐量降低。CMS默认启动的回收线程数是(CPU数量+3)/4。如果CPU数量不足4个时,CPU分出过多的运算能力执行收集器线程可能导致用户程序突然降低。为解决这种问题提供了“增量式并发收集器”的CMS变种,原理是尽量减少GC线程独占资源的时间,这样会导致垃圾收集过程变长,但是对用户程序影响变小。
CMS收集器无法处理浮动垃圾:因为在并发清理阶段,用户程序还在运行,这时候会产生一部分在标记过程之后没有标记的浮动垃圾。老年代在进行CMS垃圾收集的时候需要预留足够用户程序运行的空间,CMS不能像其他收集器那样等老年代几乎被填满了再进行收集。CMS收集器默认在老年代空间使用了68%的时候被激活。可以利用-XX:CMSInitiatingOccupancyFraction参数来调高触发百分比。如果CMS运行期间预留的内存无法满足程序需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启动Serial Old收集器来重新进行老年代的垃圾收集,这样停顿的时间就会很长。
基于标记-清除算法会导致大量碎片化的内存空间:如果是大对象无法找到足够大的连续空间来分配对象,则不得不提前触发Full GC。CMS收集器提供了-XX:+UseCMSCompactAtFullCollection参数开关,用于在Full GC完之后附送一个碎片整理的过程,但是加入这个过程,停顿的时间就变长了。CMS收集器还提供了-XX:CMSFullGCsBeforeCompaction参数用于设置执行多少次不压缩的Full GC之后,跟着来一次带压缩的Full GC。
G1收集器(Garbage First):在G1之前其他收集器收集的范围是整个新生代或者老年代。使用G1收集器时,Java堆的内存布局跟其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的,他们都是一部分(不需要连续的)Region的集合。特点:
1、并行与并发:能够充分利用多CPU多核的环境下的硬件优势,缩短Stop-The-World停顿时间。部分其他收集器原理需要停顿Java线程执行的GC动作,G1收集器可以用过并发的方式让Java程序继续执行;
2、分代收集:分代的概念在G1中得以保留,G1可以不需要其他收集器配合就可以独立管理整个GC堆;
3、空间整合:G1从整体来看是基于“标记整理(Mark-Compact)”算法实现,从局部(两个Region)看是基于复制算法实现。两种算法都不会产生空间碎片。
4、可预测的停顿:能够让使用者明确在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。G1收集器之所以能够建立可以预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据予许的时间优先回收价值最大的Region。
接下来我们再谈谈对象创建时的内存分配的几个准则:
1、对象优先在Eden区分配:如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数也可能直接分配在老年代。具体的分配细节取决于当前使用的垃圾收集器组合还有虚拟机中内存相关参数的设置。
2、大对象直接进入老年代:虚拟机提供了-XX:PretenureSizeThreShold参数(只对Serial和ParNew两款收集器有效),另大于这个设置的的对象直接在老年代中分配。这样做事避免Eden区和两个Survivor区中产生大量的内存拷贝。
3、长期存活的对象进入老年代:对象每经历过一次Minor GC,年龄增加1岁。默认15岁时,就会晋升老年代。通过-XX:MaxTenuringThreshold参数来设置晋升年龄。
4、动态年龄判定:如果在Survivor空间中,年龄相同的对象大小总和大于Survivor空间的一半,则在Survivor空间中年龄大于该年龄的对象就可以直接进入老年代,无须等待MaxTenuringThreshold设置的年龄。
5、空间分配担保:在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代剩余的空间大小;如果大于,则改为直接进行一次Full GC;如果小于,则查看HandlePromotionFailure设置是否予许担保失败,如果予许就只会进行Minor GC,如果不予许则也要进行一次Full GC。
JDK的命令行工具介绍
jsp(JVM Process Status Tool):虚拟机进程状况工具。显示系统内所有HotSpot虚拟机进程。可以列出正在运行的虚拟机进程、显示虚拟机执行主类和这些进程在虚拟机中的唯一ID(LVMID)。
命令格式:jsp [options] [hostid]
options属性选择如下:
-q:只输出LVMID,省略主类的名称
-m:输出虚拟机进程启动时传递给main函数的参数
-l:输出主类的全名,如果执行的是jar包则输出jar路径
-v:输出虚拟机进程启动时JVM参数
hostid为RMI注册表中注册的主机名
jstat(JVM Statistics Monitoring Tool):虚拟机统计信息监视工具。用于监视虚拟机各种运行状态信息的命令行工具。它可以显示虚拟机中的类装载、内存、垃圾收集、JIT编译等运行数据。是定位虚拟机性能问题的首选工具。
命令格式:jstat [option vmid [interval[s|ms] [count]]]
如果是本地进程则VMID和LVMID是一样的,如果是远程虚拟机进程VMID的格式为:[protocol:][//]LVMID[@hostname[:port]/servername]
interval和count代表查询间隔和次数,如果省略这两个参数则代表只查询一次。
option代表用户希望查询的虚拟机信息:
-class:监视类装载、卸载、总空间以及类装载所耗费的时间
-gc:监视堆状况,包括Eden区、2个Survivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息
-gccapacity:监视内容基本与-gc相同,但输出主要关注java堆各区域所使用的最大和最小空间
-gcutil:监视内容基本与-gc相同,但输出主要关注已使用空间占总空间的百分比
-gccause:与-gcutil功能一样,但是会额外输出上次GC产生的原因
-gcnew:监视新生代的状况
-gcnewcapacity:监视内容基本与-gcnew相同,输出主要关注使用到的最大和最小空间
-gcold:监视老年代状况
-gcoldcapacity:监视内容基本与-gcold相同,输出主要关注使用到的最大和最小空间
-gcpermcapacity:输出永久代用到的最大和最小空间
-compiler:输出JIT编译过的方法、耗时等信息
-printcompilation:输出已经被JIT编译的方法
输出内容缩写:E(Eden区),S0/S1(Survivor区),O(Old区 老年代),P(Permanent区 永久代),YGC(Young GC),FGC(Full GC),FGCT(Full GC Time),GCT(GC Time GC总耗时)
jinfo(Configuration Info for Java):Java配置信息工具。可以实时的查看和调整虚拟机的各项参数。
命令格式:jinfo [option] pid
jmap(Memory Map for Java):Java内存映像工具。用于生产堆转储快照(一般称为heapdump或dump文件),查询finalize执行队列,查询堆和永久代的详细信息(例如空间利用率、当前使用的垃圾收集器等)。
生成dump文件还有其他几种方式:
-XX:HeapDumpOnOutOfMemoryError参数可以让虚拟机在OOM异常之后自动生成dump文件
通过-XX:+HeapDumpOnCtrlBreak参数则可以使用【ctrl】+【Break】键让虚拟机生成dump文件
在Linux系统下用kill -3 命令“恐吓”虚拟机,拿到dump文件
命令格式:jmap [option] vmid
option属性:
-dump:生成Java堆转储快照。格式为-dump:[live,]format=b,file=<filename>,其中live子参数说明是否只dump出存活的对象
-finalizerinfo:显示在F-Queue中等待的Finalizer线程执行finalize()方法的对象,只在Linux/Solaris平台下有效
-heap:显示Java堆详细信息。如使用哪种垃圾收集器、参数配置、分代状况等,只在Linux/Solaris平台下有效
-histo:显示堆中对象统计信息,包括类、实例数量和合计容器
-permstat:以classload为统计口径显示永久代内存状况,只在Linux/Solaris平台下有效
-F:当虚拟机对-dump选项没有反应时,可使用这个选择强制生成dump快照。只在Linux/Solaris平台下有效
jhat(JVM Heap Analysis Tool):虚拟机堆转储快照分析工具。用来分析jmap生成的堆转储快照。
jstack(Stack Trace for Java):Java堆栈跟踪工具。用于生成虚拟机当前时刻的线程快照(一般称为Threaddump或者Javacore文件)。线程快照就是当前虚拟机每一条线程正在执行的方法堆栈的集合,目的是定位线程出现长时间停顿的原因。
命令格式:jstack [option] vmid
option合法值及其含义如下:
-F:当正常输出请求不配响应时,强制输出线程堆栈
-l:除了堆栈外,显示关于锁的附加信息
-m:如果调用到本地方法的话,可以显示C/C++的堆栈
JDK可视化工具:
JConsole(Java Monitoring and Management Console):基于JMX的可视化监视和管理工具。
VisualVM(All-in-One Java Troubleshooting Tool):多合一故障处理工具。使用BTrace进行性能监视、定位连接漏洞、内存泄漏、解决多线程竞争问题等。
为了提高热点代码的运行效率,虚拟机会将这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译期就是即时编译器(Just In Time Compiler,JIT编译器)。
HotSpot内置两个JIT编译器:Client Compiler(更高的编译速度)和Server Compiler(更好的编译质量)。程序用哪个编译器,取决于虚拟机的运行模式。HotSpot会根据自身版本和宿主机器的性能自动选择运行模式,用户也可用使用-client和-server参数强制指定使用哪种模式。解释器与编译器搭配使用被称为混合模式(Mixed Mode),可用-Xint强制虚拟机运行解释模式(Interpreted Mode这时候编译器完全不介入工作),可用-Xcomp强制虚拟机运行编译模式(Compliled Mode这时候优先采用编译器工作,解释器在其无法执行的时候执行)。
为了在响应速度和运行效率之间达到平衡,HotSpot会采用分层编译的策略。分层编译根据编译器编译、优化的规模和耗时划分不同层次,其中包括:
第0层:程序解释执行,解释器不开启性能监控功能(Profiling),可触发第一层编译。
第1层:也称为C1编译,将字节码编译为本地代码,进行简单可靠的优化,如有必要将加入性能监控的逻辑。
第2层:也称为C2编译,也是将字节码编译为本地代码,但是会启动 一些编译耗时较长的优化,甚至可能会根据性能监控进行一些不可靠的激进优化。
对于热点代码判断的探测方式有以下几种:
基于采样的热点探测:即虚拟机会周期性的检查每个线程的栈顶,如果发现某个(某些)方法经常出现在栈顶,那它就是"热点方法"。优点是简单高效,容易获取方法调用关系(将调用堆栈展开);缺点是很难精确方法的热度,容易受线程阻塞或外界因素干扰。
基于计数器的热点探测:即虚拟机会为每个方法(甚至代码块)建立计数器,统计执行次数,如果达到了某个阀值,即为"热点方法"。优点是统计结果准确,缺点为麻烦、且不能获取方法调用关系。
HotSpot使用的是第二种方式,它为每个方法准备了两种计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter):
方法调用计数器:默认阀值在Client模式下为1500,在Server模式下为10000.这个阀值可通过-XX:CompileThreshold来设定。当一个方法被调用时会检查是否存在JIT编译过的版本,如果有则优先使用本地代码执行;如果没有方法调用计数器+1,然后判断是否超过阀值,如果超过则向即时编译器提交该方法的代码编译请求。默认设置下,执行引擎不会同步等待编译请求完成。方法调用计数器记录的是一段时间方法被调用的次数,如果一段时间后没有满足热点代码的阀值,方法的调用计数器就减半,这个过程叫热度衰减(是在虚拟机进行垃圾收集时顺便进行进行的),这段时间叫半衰周期,可以用-XX:-UseCounterDecay来关闭热度衰减,可以用-XX:CounterHalfLifeTime参数设置半衰周期,单位是秒。
回边计数器:用于统计方法体中方法执行的次数。
以上是关于JVM基础知识及拓展的主要内容,如果未能解决你的问题,请参考以下文章