openjdk platform binary占内存JVM即时编译工作方式导致的游戏延迟如何解决?

前言描述:本文主要用来记录JVM知识一、知识点闲杂笔记0、参数设置备注-Xmx600m:设置初始堆内存空间 600m
-Xmx600m:设置最大堆内存空间 600m
-XX:SurvivorRatio=8:设置 Eden空间与另外2个 Survivor的比例为:8:1:1
-XX:NewRatio=2:设置 新生代与老年代的比例为:1:2
-Xmn:设置新生代的空间的大小
(一般不设置)
-XX:-UserAdaptiveSizePolicy :关闭自适应的内存分配策略 (暂时用不到)
-XX:TLABWasteTargetPercent:设置TLAB空间所占用Eden空间的百分比大小。
-XX:UseTLAB:设置是否开启TLAB空间
查看命令:()内为打印结果jps:看进程
jinfo -flag SurvivorRatio 14600: (-XX:SurvivorRatio=8)查看14600进程的比例
jinfo -flag NewRatio 14600:(-XX:NewRatio=2)查看新生代占1,老年代占2,新生代占整个堆的1/3
jstat -gc 14600:()查看堆内各区的内存使用情况
0.1设置堆空间大小的参数-Xms 用来设置堆空间(年轻代+老年代)的初始内存大小-X 是 jvm的运行参数mx 是memory start-Xms 用来设置堆空间(年轻代+老年代)的最大内存大小默认堆空间的大小初始内存大小:物理电脑内存大小 /64最大内存大小:物理电脑内存大小 /4手动设置:-Xmx600m -Xmx600m开发中建议将初始堆内存和最大堆内存设置成相同的值查看设置的参数:方式一:jsp / jstat -gc 进程id方式二: -XX:+PrintGCDetails0.2常用调优工具 JDK命令行
Eclipse : Memory Analyzer Tool
Jconsole
VisualVM
Jprofiler
Java Flight Recorder
Gcviewer
GC Easy 1、内存中的栈与堆栈是运行时的单位,而堆是存储的单位。- 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据
- 堆解决的是数据存储的问题,即数据怎么放,放在哪里。
2、java虚拟机栈是什么每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
其是虚拟机私有的。
生命周期和线程一致。
作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
拓展:栈的特点 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
JVM直接对Java栈的操作有两个:(1)每个方法执行,伴随着进栈 (2)执行结束后的出栈工作
对于栈来说不存在垃圾回收问题 (GC;OOM(耗尽内存)) 3、栈中可能出现的异常jvm规定java栈的大小是动态的或者是固定不变的
(1)是固定的-抛出StackOverFlowError 异常原因:线程请求分配的栈容量超过Java虚拟机栈允许的最大容量(2)动态扩展-抛出 OutOfMemoryError 异常原因:在尝试扩展的时候无法申请到足够的内存 或者 在创建新的线程时没有足够的内存区创建对应的虚拟机栈4、栈帧的内部结构 局部变量表(Local Variables)
操作数栈 (Operand Stack)或(表达式栈)
动态链接(Dynamic Linking)或 (指向运行时常量池的方法引用)
方法返回地址(Return Address)或(方法正常退出或者异常退出的定义)
一些附加信息 4.1局部变量表(local variables) 局部变量表 被称为局部变量数组或本地变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,
不存在数据安全问题,原因是 其建立在线程的栈上,是线程的私有数据
局部变量表所需的容量大小是在编译期确定下来的 4.1.1关于Slot的理解 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
局部变量表,最基本的存储单元是Slot(变量槽)
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),retturnAddress类型
在局部变量表里,32位的类型只用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
byte 、 short 、 char在存储前被转换为int,boolean也被转换为int,0表示false ,非0表示true。
long和double 则占据两个slot。
JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量)
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。 补充说明:
4.1.2 变量的分类1、按照数据类型分:基本数据类型 和 引用数据类型2、按照在类中声明的位置分:(1)成员变量:在使用前,都经历过默认初始化赋值
- 类变量:linking 的 prepare阶段:给类变量默认赋值 -->
initial 阶段:给类变量显示赋值即静态代码块赋值
- 实例变量:随着对象的创建,会在对空间中分配实例变量空间,并进行默认赋值
(2)局部变量:在使用前,必须要进行显示赋值的!否则,编译不通过
4.2 操作数栈(Operand Stack) 每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-out)的操作数栈,也可以称之为表达式栈(Expression stack) 。
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。
- 比如:执行复制、交换、求和等操作
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值。
栈中的任何一个元素都是可以任意的Java数据类型。
- 32bit的类型占用一个栈单位深度
- 64bit的类型占用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。 4.2.1栈顶缓存技术由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-stack Cashing)技术,将栈顶元素全部缓存在物理cPu的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。4.3动态链接又被称为 指向运行时常量池的方法引用
描述:类被加载之后,Class文件中的常量池会被复制一份到方法区,成为“运行时常量池”弹幕:常量池属于方法区,方法区是线程共享的;JDK8一共三种常量池:class常量池,运行时常量池,string常量池。问题1 - 为什么需要常量池?答:常量池的作用,就是为了提供一些符号和常量,便于指令的识别。4.3.1 方法的调用在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。弹幕:静态方法、私有方法、实例构造方法、父类方法 都是静态链接。绑定机制:早期绑定 和 晚期绑定绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。非虚方法:子类对象的多态性的使用前提:(1)类的继承关系(2)方法的重写虚拟机调用指令解析:其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。关于 invokedynamic 指令:关于 动态类型语言和静态类型语言4.3.2 方法重写的本质: 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
如果在类型c中找到与常量中的描述符合简单名称都相符的方法,财进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java . lang.IllegalAccessError 异常。
否则,按照继承关系从下往上依次对c的各个父类进行第2步的搜索和验证过程。
如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。 IllegalAccessError 介绍:程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。4.3.3 虚方法表: 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
那么虑方法表什么时候被创建? 答:虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。 4.4.4 类的加载过程:链接阶段包括 验证(Verify)、准备(Prepare)、解析(Resolve)验证:准备: 为类变量分配内存并且设置该类变量的默认初始值,即零值。
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
这里不会为实例变量分配初始化,类变量会KQ在方法区中,而实例变量是会随着对象一起分配到Java堆中。 解析: 将常量池内的符号引用转换为直接引用的过程。
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref info等 4.4 方法返回地址: 存放调用该方法的pc寄存器的值。
一个方法的结束,有两种方式:正常执行完成 ;出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
本质上,方法的退出就是当前伐帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。 当一个方法开始执行后,只有两种方式可以退出这个方法:1、执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;2、在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异准处理器,就会导致方法退出。简称异常完成出口。方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。4.4.1 虚拟机栈的面试题: 举例栈溢出的情况? (StackOverflowError)
调整栈大小,就能保证不出现溢出吗?
分配的栈内存越大越好吗?
垃圾回收是否会涉及到虚拟机栈?
方法中定义的局部变量是否线程安全? 1、答:通过设置 -Xss设置栈的大小;OOM2、不能;3、不是;4、不会;5、具体情况具体分析;4.4.2本地方法的理解:什么是本地方法:一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。4.4.3本地方法栈:Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。本地方法栈,也是线程私有的。允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)它的具体做法是Native Method stack中登记native方法,在Execution Engine执行时加载本地方法库。 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
它甚至可以直接使用本地处理器中的寄存器
直接从本地内存的堆中分配任意数量的内存。
并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。 加载:将字节码加载到内存;验证:检查字节码是否符合规范;准备:初始化类变量为零值;解析:将符号引用转换成直接引用;初始化:执行静态代码加载: 根据类的全限定类名获取定义此类的二进制字节流,将二进制字节流所代表的静态存储结构转换为方法区的运行时数据结构,并且在内存中生成代表此类的Class对象,作为方法区这个类的各种数据访问入口(通俗的说,就是给类变量显示赋值,并且执行类中的静态代码块)5、堆的核心概述:一个java程序对应一个进程;一个进程对应一个jvm实例;一个jvm实例中只有一个运行时数据区;一个运行时数据区只有一个方法区和堆;一个进程中的多个线程需要共享同一个方法区,堆空间 Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
堆内存的大小是可以调节的
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( ThreadLocal Allocation Buffer,TLAB)。 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area fromwhich memory for all class instances and arrays is allocated我要说的是:“几乎”所有的对象实例都在这里分配内存。—从实际使用角度数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。堆,是GC( Garbage collection,垃圾收集器)执行垃圾回收的重点区域。5.1年轻代与老年代:存储在JVM中的Java对象可以被划分为两类:Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen)其中年轻代又可以划分为Eden空间、Survivor0空间和survivor1空间(有时也叫做from区、to区)。
下面这参数开发中一般不会调:
配置新生代与老年代在堆结构的占比。在Hotspot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1当然开发人员可以通过选项“一xX:SurvivorRatio”调整这个空间比例。比如-xx:survivorRatio=8几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。IBM 公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。可以使用选项"-xmn"设置新生代最大内存大小
这个参数一般使用默认值就可以了。
5.2 对象的分配过程:概述:为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑Gc执行完内存回收后是杏会在内存空间中产生内存碎片。 new 的对象先放在Eden区。此区有大小限制
当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
然后将伊甸园中的剩余对象移动到幸存者0区。
如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
如果再次经历垃圾回收,此时会重新放回幸存者o区,接着再去幸存者1区。啥时候能去养老区呢?可以设置次数。默认是15次。 ·可以设置参数:-XX:MaxTenuringThreshold=进行设置。总结:针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to.关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。能放下就在survivor.放不下就尝试放old,如果old放不下,触发FGC,还是放不下就考虑扩容,如果无法扩容就OOM5.3 Minor GC、Major GC 与 Full FCMinor GC-年轻代;Major GC-老年代;Full GC-整堆收集JVM在进行Gc时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。针对HotSpot VM的实现,它里面的Gc按照回收区域又分为两大种类型:一种是部分收集(Partial GC) ,一种是整堆收集(Fu11 GC)部分收集:不是完整收集整个ava堆的垃圾收集。其中又分为:
新生代收集(Minor Gc / Young Gc):只是新生代的垃圾收集
老年代收集(Major Gc / old Gc):只是老年代的垃圾收集。
目前,只有cNS Gc会有单独收集老年代的行为。
注意,很多时候Major Gc会和Full Gc混淆使用,需要具体分辨是老年代回收还是整堆回收。
混合收集(Mixed Gc):收集整个新生代以及部分老年代的垃圾收集。
目前,只有c1 Gc会有这种行为
整堆收集(Ful1 Gc):收集整个java堆和方法区的垃圾收集。5.3.1 年轻代GC(Minor GC)触发机制: 当年轻代空间不足时,就会触发Minor Gc,这里的年轻代满指的是Eden代满,Survivor满不会引发Gc。(每次 Minor GC会清理年轻代的内存。)
因为Java 对象大多都具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
Minor GC会引发STw,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
最简单的分代式GC策略的触发条件
5.3.2 老年代GC (Major GC/Fu11 GC)触发机制: 指发生在老年代的Gc,对象从老年代消失时,我们说“Major Gc”或“Full Gc”发生了。
出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallelscavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC Major GC的速度一般会比Ninor Gc慢10倍以上,STW的时间更长。如果Major GC后,内存还不足,就报OOM了。 5.3.3 Full Gc触发机制:(后面细讲)触发Fu1l GC执行的情况有如下五种:(1〉调用system.gc()时,系统建议执行Fu11 Gc,但是不必然执行(2)老年代空间不足(3)方法区空间不足(4〉通过Ninor Gc后进入老年代的平均大小大于老年代的可用内存(5)由Eden区、survivor spacee (From Space)区向survivor space1 (Tospace)区复制时,对象大小大于To space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小说明: full gc是开发或调优中尽量要避免的。这样暂时时间会短一些。堆空间分代思想:为什么需要把java堆分代?不分代就 不能工作了吗?答:其实不分代完全可以,分代的唯一理由就是优化Gc性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。cc的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。5.4 内存分配策略:针对不同年龄段的对象分配原则如下所示: 优先分配到Eden
大对象直接分配到老年代
尽量避免程序中出现过多的大对象。 长期存活的对象分配到老年代
动态对象年龄判断
如果survivor 区中相同年龄的所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。 空间分配担保
-Xx :HandlePromotionFailure5.4.1对象分配过程:TLAB为什么有TLAB(ThreadLocal Allocation Buffer)? 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
由于对象实例的创建再JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
为避免多个线程操作同一地址,需要使用枷锁机制,进而影响分配速度 什么是TLAB? 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
据我所知所有openJDK衍生出来的JVM都提供了TLAB的设计。 TLAB的补充说明: 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
在程序中,开发人员可以通过选项“-xx:UseTLAB”设置是否开启TLAB空间。
默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。 总结:(1)总结:堆是共享的,访问需要加锁。而new对象是件很频繁的事情,为了提高效率,所以创建了TLAB区域。(2)这个区域是线程独占,就无须考虑并发问题。(3)分配对象就先分配到TLAB区域。满了或对象过大,就还分配到外边。(4)这就要求我们创建对象时候尽量创建小对象。提高效率弹幕:TLAB的存在是为了解决多个线程同时分配内存并发操作同一内存地址的线程安全问题除了eden区,其他区不需要考虑该问题,因为绝大多数对象分配内存都是在eden如果eden分配需要保证内存分配同步机制,避免多个线程同时分配同一块内存TLAB不是为了解决线程安全问题,是解决加锁后性能不佳的问题,TLAB那一块还是存在线程安全TLAB是为了解决线程内存分配的问题,两个线程可能争抢一块内存区域。但是TLAB内存的数据还是可以被线程共享的,还是存在线程安全问题5.4.2 堆空间的参数设置参数举例:-XX:+PrintFlagsInitial :查看所有的参数的默认初始值
-XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改不再是初始值)
-xms:初始堆空间内存(默认为物理内存的1/64)
-xmx:最大堆空间内存(默认为物理内存的1/4)
-xmn:设置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比
一XX:SurvivorRatio:设置新生代中Eden和s0/s1空间的比例-XX
:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的Gc处理日志
>打印gc简要信息:(1) -XX:+PrintGc (2)-verbose:gc
-XX:HandlePromotionFailure:是否设置空间分配担保
在发生Minor Gc之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。 如果大于,则此次Minor Gc是安全的
如果小于,则虚拟机会查看-xX:HandlePromotionFailure设置值是否允许担保失败。
在JDK6 Update24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor Gc,否则将进行Ful1 GC。 5.4.3 堆是分配对象存储的唯一选择吗? 在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
此外,前面提到的基于openJDR深度定制的TaoBaoVM,其中创新的GCIH (Gcinvisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且Gc不能管理GcIH内部的Java对象,以此达到降低Gc的回收频率和提升Gc的回收效率的目的。 5.5 逃逸分析概述 如何将堆上的对象分配到栈,需要使用逃逸分析手段。
这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象动态作用域:
参数设置:结论:开发中能使用局部变量的,就不要使用在方法外定义5.5.1 逃逸分析:代码优化使用逃逸分析,编译器可以对代码做如下优化: 一、栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
二、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。 5.5.2 代码优化-栈上分配5.5.3 代码优化-同步省略(消除)5.5.4 代码优化-标量替换标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。参数:-XX:+EliminateAllocations:开启了标量替换(默认开启),允许将对象打散分配在栈上
5.5.5 小结 年轻代是对象的诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。
老年代放置长生命周期的对象,通常都是从survivor区域筛选拷贝过来的Java对象。当然,也有特殊情况,我们知道普通的对象会被分配在TLAB上:如果对象较大,JVM会试图直接分配在Eden其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM就会直接分配到老年代。
当Gc只发生在年轻代中,回收年轻代对象的行为被称为MinorGc。当Gc发生在老年代时则被称为MajorGC或者FullGC。一般的,MinorGc 的发生频率要比MajorGc高很多,即老年代中垃圾回收发生的频率将大大低于年轻代。 6、方法区 栈、堆、方法区的交互关系
方法区的理解
设置方法区大小与OOM
方法区的内部结构
方法区使用举例
方法区的演进细节
方法区的垃圾回收
总结 6.1 栈、堆、方法区的交互关系6.2 方法区的基本理解 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java.lang. outOfMemoryError:PermGen space 或者java. lang. outOfMemoryError: Metaspace
加载大量的第三方的jar包;Tomcat部署的工程过多(30-50个);大量动态的生成反射类 关闭JVM就会释放这个区域的内存。 6.3 设置方法区大小与OOM命令行:jps
jinfo -flag MetaspaceSize 进程号
参数:-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
jdk7以前
-XX:PermSize=100m -XX:MaxPermSize=100m
问题1:如何解决这些OOM?1、要解决oOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(MemoryLeak)还是内存溢出(Memory Overflow)。2、如果是内存泄漏,可进一步通过工具查看泄漏对象到6c Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。3、如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。6.4 方法区的内部结构方法区存储什么?《深入理解Java 虚拟机》书中对方法区 (Method Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。类型信息:对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息: 这个类型的完整有效名称(全名-包名.类名)
这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类
这个类型的修饰符(public, abstract, final的某个子集)
这个类型直接接口的一个有序列表 域信息: JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称、域类型、域修饰符(public, private,protected,static,final, volatile,transient的某个子集) 方法信息:JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序: 方法名称
方法的返回类型(或void)·方法参数的数量和类型(按顺序)
方法的修饰符(public,private, protected,static, final,synchronized, native,abstract的一个子集)
方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
异常表( abstract和lnative方法除外)
每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引non-final的类变量:non - final 指的是static 修饰的类中变量运行时常量池与常量池:1、为什么需要常量池?一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池,之前有介绍。2、常量池中有什么?几种在常量池内存储的数据类型包括: 数量值
字符串值
类引用
字段引用
方法引用 小结:常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。3、运行时常量池 运行时常量池( Runtime Constant Poo1)是方法区的一部分。
常量池表(Constant Pool Table)是class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
运行时常量池,相对于class文件常量池的另一重要特征是:具备动态性。 运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却比符号表要更加丰富一些。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛outofMemoryError异常。 6.6 方法区的演进细节首先明确:只有Hotspot才有永久代。BEA JRockit、IBM J9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。Hotspot中方法区的变化: jdk1.6及之前有永久代(permanent generation),静态变量存放在永久代上
jdk1.7有永久代,但己经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中
jdk1.8及之后无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆 问题:永久代为什么被元空间替换?问题2:StringTable为什么要被调整?jdk7中将stringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而ful gc是老年代的空间不足、永久代不足时才会触发。这就导致stringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。问题3:静态变量放在哪里?静态引用对应的对象实体始终都存在堆空间6.7 方法区的垃圾回收 有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的zGC收集器就不支持类卸载)。
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,咆括下面三类常量:
1、类和接口的全限定名
2、字段的名称和描述符
3、方法的名称和描述符
HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
回收废弃常量与回收Java堆中的对象非常类似。
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“下再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如osGi、JSP的重加载等,否则通常是很难达成的。
该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+Traceclass-Loading、-XX:+TraceclassUnLoading查看类加载和卸载信息
在大量使用反射、动态代理、cGLib等字节码框架,动态生成JSP以及osGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。 6.8 常见面试题 说一下VM内存模型吧,有哪些区?分别干什么的?
Java8的内存分代改进JVM内存分哪几个区,每个区的作用是什么?一面:JVM内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个survivor区?二面: Eden和survior的比例分配
jvm内存分区,为什么要有新生代和老年代
二面: Java的内存分区二面:讲讲jvm运行时数据库区什么时候对象会进入老年代?
VM的内存结构,Eden和survivor比例。JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。
一面: Jvm内存模型以及分区,需要详细到每个区放什么。一面:VM的内存模型,Java8做了什么修改
JVM内存分哪几个区,每个区的作用是什么?
java内存分配jvm的永久代中会发生垃圾回收吗?一面: jvm内存分区,为什么要有新生代和老年代? 答案区域:8.2 - 进入老年代有很多种情况,age到达,过大对象,当幸存者区相同年龄大于其内存一般,该年龄及以上也会进入,幸存者区满了也会进入7、对象的实例化、内存布局与访问定位面试题: 对象在JVM是怎么存储的?
对象头信息里面有哪些东西? 7.1 对象的实例化1.创建对象的方式2.创建对象的步骤给对象的属性赋值的操作: 属性的默认初始化
显式初始化
代码中初始化
构造器中初始化 对象实例化的过程: 加载类元信息
为对象分配内存
处理并发问题
属性的默认初始化(零值初始化)
设置对象的头信息
属性的显式初始化、代码中初始化、构造器中初始化 7.2 对象的内存布局7.2.1 对象头(Header)说明:如果是数据,还记录数组的长度包含两部分:运行时元数据(Mark Word)和 类型指针。运行时元数据: 哈希值
GC分代年龄
锁状态标志
线程持有的锁
偏向线程ID
偏向时间戳 类型指针:指向类元数据 InstanceClass,确定该对象所属的类型7.2.3 实例数据(Instance Data)说明:它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)规则:对齐填充:不是必须的,也没有特别含义,仅仅起到占位符的作用7.3 对象的访问定位问题:JVM 是如何通过栈帧中的对象引用访问到其内部的对象实例呢?答:定位,通过栈上 reference 访问对象访问方式主要有两种:句柄访问 和 直接指针7.4 直接内存概述: 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
直接内存是在Java堆外的、直接向系统申请的内存区间。
来源于NIo,通过存在堆中的DirectByteBuffer操作Native内存
通常,访问直接内存的速度会优于Java堆。即读写性能高。
直接内存的OOM与内存大小的设置参数:-XX:MaxDirectMemorySize=10m 也可能导致outofMemoryError异常
由于直接内存在Java堆外,因此它的大小不会直接受限于-xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存
缺点
分配回收成本较高
不受JVM内存回收管理
直接内存大小可以通过MaxDirectMemorysize设置
如果不指定,默认与堆的最大值-xmx参数值一致 }
学习 JVM 相关的知识,必然绕不开即时编译器,因为它太重要了。了解了它的基本原理及优化手段,在编程过程中可以让我们有种打开任督二脉的感觉。比如,很多朋友在面试当中还会遇到这样的问题:Java 是基于编译执行还是基于解释执行?当你了解了 Java 的即时编译器,不仅能够轻松回答上述问题,还能如数家珍的讲出 JVM 在即时编译器上采用的优化技术,而且在实践过程中更深刻的理解代码背后的原理。本文便带大家全面的了解 Java 即时编译器。即时编译器在部分的商用虚拟机中,比如 HotSpot 中,Java 程序先通过解释器(Interceptor)进行解释执行。这也是为什么称 Java 是基于解释执行的原因。但当虚拟机发现某块代码或方法运行的特别频繁,便会将其标记为 “热点代码”(Hot Spot Code)。针对热点代码,虚拟机会采用各种措施来提升其执行效率,因为执行比较频繁,如果能够提升其执行效率,性价比还是比较高的。为此,在运行时,虚拟机会把这些代码编译成与本地平台相关的机器码,并进行各层次的深度优化。而这些优化操作便是通过编译器来完成的,也称作即使编译器(Just In Time Compiler,简称 JIT 编译器)。因此,准确的来说,像 HotSpot 等虚拟机,Java 是基于解释执行和编译执行的。下面用一张图来解释该过程:解释器与编译器的并存首先,我们需要知道并不是所有的 Java 虚拟机都采用解释器与编译器并存的架构,但许多主流的商用虚拟机(如 HotSpot),都同时包含解释器和编译器。既然即时编译器进行了各层次的优化,那么为什么 Java 还使用解释器来 “拖累” 程序的性能呢?这是因为,解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释器执行节约内存,反之可以使用编译执行来提升效率。此外,如果编译后出现“罕见陷阱”,可以通过逆优化退回到解释执行。Java 虚拟机运行时,解释器和即时编译器能够相互协作,取长补短。无论采用解释器进行解释执行,还是采用即使编译器进行编译执行,最终字节码都需要被转换为对应平台的本地机器码指令。某些服务并不看重启动时间,而某些服务却非常看重,这就需要采用解释器与即时编译器并存来换取一个平衡点。我们可以从解释器和编译器的编译时间开销和编译空间开销两方面进行对比。首先,看编译的时间开销。我们所说的 JIT 比解释器快,仅限于对 “热点代码” 编译之后的代码执行起来要比解释器解释执行的快。通过上图可以看出,如果是只是单次执行的代码,JIT 编译比解释器要多出一步“执行编译”,因此,只执行一次时,JIT 是要比解释器慢的。只执行一次的代码通常包括只被调用一次的代码(比如构造器)、没有循环的代码等,此时使用 JIT 显然得不偿失。其次,再来看看编译空间方面的开销。对一般的 Java 方法而言,编译后代码的大小相对于字节码,膨胀比达到 10 倍是很正常的。只有对执行频繁的代码才值得编译,如果把所有代码都编译则会显著增加代码所占空间,导致 “代码爆炸”。这就是为什么有些 JVM 不会单一使用 JIT 编译,而是选择用解释器 + JIT 编译器的混合执行引擎。HotSpot 的两种即时编译器HotSpot 虚拟机为了使用不同的应用场景,内置了两个即时编译器:Client Complier 和 Server Complier,简称为 C1、C2 编译器,分别用在客户端和服务端。Client Complier 可获取更高的编译速度,Server Complier 可获取更好的编译质量。JVM Server 模式与 client 模式最主要的差别在于:-server 模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。原因是:当虚拟机运行在 - client 模式时,使用的是一个代号为 C1 的轻量级编译器,而 - server 模式启动的虚拟机采用相对重量级代号为 C2 的编译器。C2 比 C1 编译器编译的相对彻底,服务起来之后,性能更高。默认情况下,使用 C1 还是 C2 编译器,要取决于虚拟机运行的模式。HotSpot 虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用 “-client” 或“-server”参数去强制指定虚拟机运行在 Client 模式或 Server 模式。目前主流的 HotSpot 虚拟机中默认是采用解释器与其中一个编译器配合的方式工作,这种配合称作混合模式(Mixed Mode)。用户可以使用参数 - Xint 强制虚拟机运行于 “解释模式”(Interpreted Mode),这时候编译器完全不介入工作。使用 - Xcomp 强制虚拟机运行于 “编译模式”(Compiled Mode),这时将优先采用编译方式执行,但是解释器仍然要在编译无法进行的情况下接入执行过程。通过虚拟机 java -version 命令可以查看当前默认的运行模式。在上述示例中我们不仅能够看到采用的模式为 mixed mode,还能看到出采用的是 Server 模式。热点探测上面解释了 JIT 编译器的基本功能,那么它是如何判断热点代码的呢?判断一段代码是不是热点代码的行为,也叫热点探测(Hot Spot Detection),通常有两种方法:基于采样的热点探测和基于计数器的热点探测 (HotSpot 使用此方式)。基于采样的热点探测(Sample Based Hot Spot Detection):虚拟机会周期的对各个线程栈顶进行检查,如果某些方法经常出现在栈顶,会被定义为 “热点方法”。实现简单、高效,很容易获取方法调用关系。但很难确认方法的 reduce,容易受到线程阻塞或其他外因扰乱。基于计数器的热点探测(Counter Based Hot Spot Detection):为每个方法(甚至是代码块)建立计数器,执行次数超过阈值就认为是 “热点方法”。统计结果精确严谨,但实现麻烦,不能直接获取方法的调用关系。HotSpot 虚拟机默认采用基于计数器的热点探测,有两种计数器:方法调用计数器和回边计数器。当计数器数值大于默认阈值或指定阈值时,方法或代码块会被编译成本地代码。方法调用计数器,记录方法调用的次数。Client 模式默认阈值是 1500 次,在 Server 模式下是 10000 次,可以通过 -XX:CompileThreadhold 来设定。如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内的方法被调用的次数。当超过一定的时间限度,但调用次数仍然未达到阈值,那么该方法的调用计数器就会被减半,称为方法调用计数器热度的衰减(Counter Decay),这段时间称为此方法的统计半衰周期( Counter Half Life Time)。进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数 -XX:CounterHalfLifeTime 参数设置半衰周期的时间,单位是秒。JIT 编译交互图如下:回边计数器,统计一个方法中循环体代码执行的次数。在字节码中遇到控制流向后跳转的指令称为 “回边”(Back Edge),建立回边计数器统的目的是为了触发 OSR 编译。计数器的阈值, HotSpot 提供了 - XX:BackEdgeThreshold 来进行设置,但当前的虚拟机实际上使用了 - XX:OnStackReplacePercentage 来间接调整阈值,计算公式如下:在 Client 模式下, 公式为 “方法调用计数器阈值(CompileThreshold)X OSR 比率(OnStackReplacePercentage)/ 100” 。其中 OSR 比率默认为 933,那么,回边计数器的阈值为 13995。
在 Server 模式下,公式为 “方法调用计数器阈值(Compile Threashold)X (OSR 比率 (OnStackReplacePercentage) - 解释器监控比率(InterpreterProfilePercent))/100”。其中 onStackReplacePercentage 默认值为 140,InterpreterProfilePercentage 默认值为 33,如果都取默认值,那么 Server 模式虚拟机回边计数器阈值为 10700。
对应的流程图如下:与方法计数器不同,回边计数器没有计数热度衰减的过程,因此统计的就是该方法循环执行的绝对次数。当计数器溢出时,它还会把方法计数器的值也调整到溢出状态,这样下次再进入该方法的时候就会执行标准编译过程。不同模式的性能对比了解 JVM 的不同编译模式,下面写一个简单的测试例子来测试一下不同编译器的性能。需要注意的是以下测试程序和场景并不够严谨,只是从大方向上带大家了解一下不同模式之间的区别。如果需要精准的测试,最好的方式应该是在严格的基准测试下测试。public class JitTest {
private static final Random random = new Random();
private static final int NUMS = 99999999;
public static void main(String[] args) {
long start = System.currentTimeMillis();
int count = 0;
for (int i = 0; i < NUMS; i++) {
count += random.nextInt(10);
}
System.out.println("count: " + count + ",time cost : " + (System.currentTimeMillis() - start));
}
}
在测试的过程中,通过添加虚拟机参数 “-XX:+PrintCompilation” 来打印编译信息。首先,来看纯解释执行模式,JVM 参数添加 “-Xint -XX:+PrintCompilation”,然后执行 main 方法,打印信息如下:count: 449945612,time cost : 33989
花费了大概 34 秒。同时,控制台并未打印出编译信息,侧面证明了即时编译器没有参与工作。下面采用编译器模式执行,修改虚拟机参数:“-Xcomp -XX:+PrintCompilation”,执行 main 方法,打印如下信息:其中,代码中相关消耗时间打印信息为:count: 450031537,time cost : 10593
只用了 10 秒,同时会产生大量的编译信息。最后,采用混合模式再测试一次,修改虚拟机参数为 “-XX:+PrintCompilation”,执行 main 方法:打印了编译信息,同时发现执行同样的代码只需要不到 1 秒的时间。经过上述粗略的测试,会发现在上述示例中耗时由小到大顺序为:混合模式 < 纯编译模式 < 纯解释模式。当然,如果需要更精准和更准确的测试,还需要严格的基准测试条件。编译优化技术即时编译器之所以快,还有另外一个原因:在编译本地代码时,虚拟机设计团队几乎把所有的优化措施都使用上了。所以,即时编译器产生的本地代码会比 javac 产生的字节码更优秀。下面看一下即时编译器在生产本地代码时都采用了哪些优化技术。第一,语言无关的经典优化技术之一:公共子表达式消除。如果一个表达式 E 已经计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生变化,那么 E 的这次出现就成为了公共子表达式。对于这种表达式,没必要花时间再对它进行计算,只需要直接使用前面计算过的表达式结果代替 E 就可以了。例子:int d = (c*b) * 12 + a + (a+ b * c) -> int d = E * 12 + a + (a+ E)。第二,语言相关的经典优化技术之一:数组范围检查消除。在 Java 语言中访问数组元素的时候系统将会自动进行上下界的范围检查,超出边界会抛出异常。对于虚拟机的执行子系统来说,每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,这无疑是一种性能负担。Java 在编译期根据数据流分析可以判定范围进而消除上下界检查,节省多次的条件判断操作。第三,最重要的优化技术之一:方法内联。简单的理解为把目标方法的代码 “复制” 到发起调用的方法中,消除一些无用的代码。只是实际的 JVM 中的内联过程很复杂,在此不分析。第四,最前沿的优化技术之一:逃逸分析。逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到它,则可进行一些高效的优化:栈上分配:将不会逃逸的局部对象分配到栈上,那对象就会随着方法的结束而自动销毁,减少垃圾收集系统的压力。
同步消除:如果该变量不会发生线程逃逸,也就是无法被其他线程访问,那么对这个变量的读写就不存在竞争,可以将同步措施消除掉。
标量替换:标量是指无法在分解的数据类型,比如原始数据类型以及 reference 类型。而聚合量就是可继续分解的,比如 Java 中的对象。标量替换如果一个对象不会被外部访问,并且对象可以被拆散的话,真正执行时可能不创建这个对象,而是直接创建它的若干个被这个方法使用到的成员变量来代替。这种方式不仅可以让对象的成员变量在栈上分配和读写,还可以为后后续进一步的优化手段创建条件。
其他更多优化项,可参考 OpenJKD 的 Wiki:wiki.openjdk.java.net/display/hot…小结通过上面的学习,想必大家已经对即时编译的运作原理、使用场景、使用流程、判断代码、优化技术项等有了更深刻的了解。当了解了这些底层的原理,在写代码、排查问题、性能调优方面均有帮助。对于本文中提到的内容,也建议大家实践、体验一下,以便加深印象。原文链接:《深入浅出了解 Java 即时编译器原理及实战》参考文献:1.《深入理解 Java 虚拟机》2.blog.csdn.net/riemann_/ar…3.www.jianshu.com/p/fbced5b34…程序新视界公众号 “ 程序新视界”,一个让你软实力、硬技术同步提升的平台,提供海量资料}
毫无疑问,Java是世界上最大的技术平台之一,拥有大约900万到1000万开发人员(根据Oracle的数据)。按照设计,许多开发人员不需要知道他们所使用的平台的底层复杂性。这导致了开发人员只有在客户抱怨性能时,才会想到去接触Java调优。然而,对于对性能感兴趣的开发人员来说,理解JVM技术堆栈的基础知识是很重要的。理解JVM技术使开发人员能够编写更好的软件,并为研究与性能相关的问题提供必要的理论背景。本章介绍JVM如何执行Java,以便为本书后面对这些主题的深入研究提供基础。特别是,第9章将深入讨论字节码。读者的一个策略可能是现在就阅读这一章,然后在理解了其他一些主题之后,再结合第9章重读这一章。2.1 Interpreting and Classloading根据定义Java虚拟机(通常称为VM规范)的规范,JVM是基于堆栈的解释机器。这意味着它不使用寄存器(如物理硬件CPU),而是使用部分结果的执行堆栈,并通过对堆栈的顶值(或多个值)进行操作来执行计算。JVM解释器的基本行为本质上可以看作是“while循环中的一个switch”——独立于最后一个操作码处理程序的每个操作码,使用计算堆栈保存中间值。NOTE:当我们深入研究Oracle/OpenJDK VM (HotSpot)的内部时,我们将看到,对于真正的生产级Java解释器来说,情况可能更加复杂,但是switch-inside-while是目前可以接受的心理模型。当我们使用java HelloWorld命令启动应用程序时,操作系统将启动虚拟机进程(即java二进制文件)。这将设置Java虚拟环境,并初始化将实际执行HelloWorld类文件中的用户代码的堆栈机器。应用程序的入口点将是HelloWorld.class的main()方法。为了将控制权交给这个类,它必须在开始执行之前由虚拟机加载。为此,使用了Java类加载机制。当一个新的Java进程初始化时,将使用一个类加载器链。初始加载器称为引导类加载器【Bootstrap classloader】,它包含核心Java运行时【runtime】中的类。在Java8以及之前的版本中,这些都是从rt.jar加载的。在Java9和更高版本中,运行时【runtime】被模块化,类加载的概念有所不同。引导类加载器【Bootstrap classloader】的主要目的是获得最小的类集(包括java.lang.Object、Class和Classloader等基本类),以允许其他类加载器启动系统的其余部分。NOTE:Java将类加载器建模为其自身运行时和类型系统中的对象,因此需要某种方式来创建初始类集。否则,在定义什么是类装入器时就会出现循环问题。接下来创建扩展类加载器【Extension classloader】;它将其父类定义为引导类加载器【Bootstrap classloader】,并在需要时将委托给其父类加载器。该扩展类加载器并没有被广泛使用,但是可以为特定的操作系统和平台提供重写【override】和本地【native】代码。值得注意的是,Java 8中引入的Nashorn JavaScript runtime是由扩展类加载器【Extension classloader加载的。最后,创建应用类加载器【Application classloader】;它负责从定义的classpath下加载用户类。不幸的是,有些文本将其称为“系统【System】”类加载器。应该避免使用这个术语,原因很简单,它不加载系统类(这是引导类加载器【Bootstrap classloader的职责)。应用类加载器【Application classloader】非常常见,它的父类是扩展类加载器【Extension classloader】。当在程序执行过程中第一次遇到新类时,Java会加载它们的依赖项。如果类加载器找不到类,通常会将查找委托给父类。如果查找链到达引导类加载器【Bootstrap classloader】,仍然没有找到该类,则会抛出ClassNotFoundException开发人员使用的构建过程必须有效地使用与生产环境中使用的类路径完全相同的类路径进行编译,因为这有助于缓解这个潜在的问题。在正常情况下,Java只加载一个类一次,并在运行时环境【runtime environment】中创建一个Class对象来表示该类。然而,重要的是要认识到同一个类可能会被不同的类加载器加载两次。因此,系统中的类由用于加载它的类加载器以及完全限定的类名(包括包名)来唯一标识。2.2 Executing Bytecode重要的是要理解Java源代码在执行之前经历了大量的转换。第一个转换就是使用Java编译器javac的编译步骤,它通常作为较大的构建过程的一部分而被调用。javac的工作是将Java代码转换成包含字节码的.class文件。它通过对Java源代码进行相当简单的转换来实现这一点,如图2-1所示。在javac编译期间很少进行优化,在反汇编工具(如标准的javap)中查看时,得到的字节码仍然是可读的和可识别的Java代码。图2-1 Java class file compilation字节码是一种不绑定到特定机器架构的中间表示形式。与机器架构的解耦提供了可移植性,这意味着已经开发(或编译)的软件可以在JVM支持的任何平台上运行,并提供了来自Java语言的抽象。这为我们了解JVM执行代码的方式提供了第一个重要视角。NOTE:Java语言和Java虚拟机现在在一定程度上是独立的,因此JVM中的J可能有点误导人,因为JVM可以执行任何可以生成有效类文件的JVM语言。实际上,图2-1可以很容易地显示Scala编译器scalac生成字节码以便在JVM上执行。无论使用何种源代码编译器,生成的类文件都具有VM规范指定的定义良好的结构(表2-1)。在允许运行之前,JVM加载的任何类都将被验证是否符合预期的格式。Table 2-1 类文件的剖析ComponentDescriptionMagic number0xCAFEBABE类文件格式的版本类文件的次要版本和主要版本Constant pool:常量池类的常量池Access flags:访问标识类是否是抽象的、静态的等等This class:该类当前类的名字Superclass:超类超类的名字Interfaces:接口类中的任何接口Fields:字段属性类中的任何字段属性Methods:方法类中的任何方法Attributes类的任何属性(例如,源文件的名称,等等)每个类文件都以数字0xCAFEBABE开始,十六进制中的前4个字节表示与类文件格式的一致性。下面的4个字节表示用于编译类文件的次要版本和主要版本,将对它们进行检查,以确保目标JVM的版本不低于用于编译类文件的版本。类加载器检查主版本和次版本,以确保兼容性;如果它们不兼容,将在运行时抛出UnsupportedClassVersionError,表明运行时【runtime】的版本比编译后的类低。NOTE:Magic numbers为Unix环境提供了一种识别文件类型的方法(而Windows通常使用文件扩展名)。因此,一旦做出决定,就很难改变。不幸的是,这意味着在可预见的将来,Java将无法使用令人尴尬且带有性别歧视的0xCAFEBABE,尽管Java 9为模块文件引入了神奇的数字0xCAFEDADA。常量池在代码中保存常量值:例如,类、接口和字段的名字。当JVM执行代码时,常量池表用于引用值,而不是必须在运行时依赖于内存布局。访问标志用于确定应用于类的修饰符。标志的第一部分用于标识一般属性,比如类是否是公共的,然后是它是否是final的,不能被子类化。该标志还确定类文件是表示的接口还是抽象类。标志的最后一部分指出类文件是否表示的是源代码中不存在的合成类、注释类型或枚举。This class【该类】、Superclass【超类】和Interfaces【接口】项是常量池中的索引,用于标识属于该类的类型层次结构。Fields【字段属性】和Methods【方法】定义类似签名的结构,包括应用于字段属性或方法的修饰符。然后使用一组Attributes【属性】来表示更复杂和非固定大小结构的结构化项。例如,方法使用Code属性来表示与该特定方法关联的字节码。图2-2 提供了用于记住结构的助记符图2 - 2 类文件结构的助记符在这个非常简单的代码示例中,可以观察运行javac的效果:public class HelloWorld {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println("Hello World");
}
}
}
Java附带了一个名为javap的类文件反汇编器,以允许我们检查.class文件。获取HelloWorld类文件并运行javap -c HelloWorld,会得到以下输出:public class HelloWorld {
public HelloWorld();
Code:
0: aload_0
1: invokespecial #1
// Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: bipush
10
5: if_icmpge
22
8: getstatic
#2
// Field java/lang/System.out ...
11: ldc
#3
// String Hello World
13: invokevirtual #4
// Method java/io/PrintStream.println ...
16: iinc
1, 1
19: goto
2
22: return
}
这个布局描述了文件HelloWorld.class的字节码。对于更详细的信息,javap还有一个-v选项,提供完整的类文件头信息和常量池详细信息。该类文件包含两个方法,尽管源文件中只提供了一个main()方法;这是javac自动向类中添加默认构造函数的结果。构造函数中执行的第一个指令是aload_0,它将此引用放在堆栈的第一个位置。然后调用invokspecial命令,该命令调用一个实例方法,该方法对调用超类构造函数和创建对象具有特定的处理。在默认构造函数中,调用与Object的默认构造函数匹配,因为未提供覆盖。NOTE:JVM中的操作码【Opcodes 】非常简洁,表示类型、操作以及本地变量、常量池和堆栈之间的交互。回到main()方法,iconst_0将整数常量0推入计算堆栈。istore_1以偏移量1(在循环中表示为i)将这个常量值存储到局部变量中。局部变量偏移量从0开始,但是对于实例方法,第0项总是这样的。然后将偏移量为1的变量加载回堆栈,并使用if_icmpge(“if integer compare greater or equal”)与压入的常量10进行比较。只有当当前整数为>= 10时,该测试才会成功。对于最初的几个迭代,这个比较测试均是false,因此我们继续指令8。这里是来自System.out的静态方法,然后从常量池加载“Hello World”字符串。下一个调用是invokevirtual,调用基于类的实例方法。然后对整数进行递增,并调用goto返回到指令2。这个过程一直持续到if_icmpge比较最终返回true(循环变量为>= 10);在循环的那个迭代中,控制传递给指令22,方法返回。2.3 HotSpot简介1999年4月,Sun引入了Java在性能方面的最大变化之一。HotSpot虚拟机是Java的一个关键特性,它的性能可以与C和c++等语言相媲美(或者更好)(参见图2-3)。为了解释这是如何实现的,让我们深入研究用于应用程序开发的语言设计。图2-3 The HotSpot JVM语言和平台设计常常涉及在所需功能之间做出决策和权衡。C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.
零开销原则在理论上听起来很棒,但它要求所有语言的用户都要处理操作系统和计算机实际工作的底层现实。对于那些不把原始性能作为主要目标的开发人员来说,这是一个巨大的额外认知负担。不仅如此,它还要求在构建时将源代码编译为特定于平台的机器码——通常称为提前编译【Ahead-of-Time】(AOT)。这是因为其他执行模型,如解释器、虚拟机和可移植层,几乎都不是零开销的。该原则还在那句短语“what you do use, you couldn’t hand code any better”中隐藏了一个大麻烦。这需要很多条件,尤其是开发人员能够生成比自动化系统更好的代码。这根本不是一个安全的假设。很少有人想用汇编语言编写代码了,因此使用自动化系统(如编译器)来生成代码显然对大多数程序员有好处。Java从来没有采用过零开销的抽象哲学。相反,HotSpot虚拟机所采用的方法是分析程序的运行时行为,并智能地应用最有利于性能的优化。HotSpot VM的目标是让您能够编写您所习惯的Java代码并遵循良好的设计原则,而不是为了适应VM而扭曲您的程序。即时编译JIT【 Just-in-Time】简介Java程序在字节码解释器中开始执行,其中的指令在虚拟堆栈机器上执行。这种来自CPU的抽象提供了类文件可移植性的好处,但是要获得最大的性能,您的程序必须直接在CPU上执行,并利用其本机特性。HotSpot通过将程序单元从解释的字节码编译为本机代码来实现此目的。 HotSpot VM中的编译单元是方法和循环。 这称为即时(JIT)编译。JIT编译的工作原理是在应用程序以解释模式运行时监视它,并观察最经常执行的代码部分。在此分析过程中,将捕获程序化跟踪信息,以便进行更复杂的优化。一旦某个特定方法的执行通过了一个阈值,分析器就会寻找编译和优化该特定代码段的方法。JIT编译方法有许多优点,但其中最主要的优点之一是,它将编译器优化决策基于解释阶段收集的跟踪信息,从而使HotSpot能够进行更明智的优化。不仅如此,HotSpot还拥有数百年(或更长时间)的工程开发历史,几乎每个新版本都添加了新的优化和好处。这意味着任何运行在HotSpot新版本之上的Java应用程序都可以利用VM中提供的新性能优化,甚至不需要重新编译。TIP:在将Java源代码转换为字节码并执行了(JIT)编译的另一个步骤之后,实际执行的代码与编写时的源代码相比发生了很大的变化。这是一个关键的见解,它将推动我们处理与性能相关的调查的方法。在JVM上执行的JIT编译的代码可能与原始Java源代码完全不同.一般的情况是,像c++这样的语言(以及Rust)往往具有更可预测的性能,但代价是将大量低层次的复杂性强加给用户。请注意,“更可预测”并不一定意味着“更好”。AOT编译器生成的代码可能可能需要运行很多种处理器,并且可能无法假定特定的处理器特性是可用的。使用配置文件引导优化(profile-guided optimization, PGO)的环境,如Java,有可能以大多数AOT平台根本不可能的方式使用运行时信息。这可以提高性能,比如动态内联【dynamic inlining】和优化远程虚拟调用【optimizing away virtual calls】。HotSpot甚至可以在VM启动时检测它所运行的CPU类型,并且可以使用这些信息来支持针对特定处理器特性的优化(如果有的话)。TIP:检测精确的处理器的功能的技术称为JVM内嵌原语【JVM intrinsics】,不要与synchronized关键字引入的内部锁混淆。关于PGO和JIT编译的完整讨论可以在第9章和第10章中找到。HotSpot采用的复杂方法对大多数普通开发人员来说是一个很大的好处,但是这种折衷(放弃零开销抽象)意味着在高性能Java应用程序的特定情况下,开发人员必须非常小心地避免对Java应用程序实际执行方式的“常识”推理和过于简单的心理模型。NOTE:分析一小部分Java代码(微基准测试)的性能通常比分析整个应用程序更困难,而且是大多数开发人员不应该承担的一项非常专门化的任务。我们将在第五章讨论这个问题。HotSpot的编译子系统【compilation subsystem】是虚拟机提供的两个最重要的子系统之一。 另一种是自动内存管理【automatic memory management】,这是Java早期的主要卖点之一。2.4 JVM Memory Management:JVM内存管理在C、c++和Objective-C等语言中,程序员负责管理内存的分配和释放。管理内存和对象生命周期的好处是更具确定性的性能,并且可以将资源生命周期与对象的创建和删除联系起来。 但是这些好处的代价也是巨大的 - 为了准确性,开发人员必须能够准确地计算内存。不幸的是,几十年的实践经验表明,许多开发人员对内存管理的习惯用法和模式理解不足。c++和Objective-C的后续版本使用标准库中的智能指针风格的用法改进了这一点。然而,在创建Java时,糟糕的内存管理是应用程序错误的主要原因。这导致开发人员和管理人员担心花在处理语言特性而不是为业务交付价值上的时间。Java希望通过使用称为垃圾收集(GC)的进程引入自动管理的堆内存来帮助解决这个问题。简单地说,垃圾收集是一个非确定性的处理,当JVM需要更多内存进行分配时,它会被触发以恢复和重用不再需要的内存。然而,GC背后的故事并不是那么简单,在Java的历史进程中已经开发并应用了各种垃圾收集算法。GC是有代价的:当它运行时,它通常会停止整个世界【stops the world】,这意味着在GC进行时应用程序会暂停。通常,这些暂停时间被设计得非常小,但是当应用程序受到压力时,这个时间会增加。垃圾收集是Java性能优化中的一个主要主题,因此我们将在第6、7和8章详细介绍Java GC。2.5 Threading and the Java Memory Model:线程和Java内存模型Java在其第一个版本中带来的主要进步之一是内置了对多线程编程的支持。Java平台允许开发人员创建新的执行线程。例如,在Java 8语法中:Thread t = new Thread(() -> {System.out.println("Hello World!");});
t.start();
不仅如此,Java环境本身就是多线程的,JVM也是。这在Java程序的行为中产生了额外的、不可减少的复杂性,并且使性能分析人员的工作更加困难。在大多数主流JVM实现中,每个Java应用程序线程都精确地对应于一个专用的操作系统线程。另一种方法是使用共享线程池来执行所有Java应用程序线程(该方法也被称为green threads),事实证明这种方法不能提供可接受的性能配置文件,并且增加了不必要的复杂性。NOTE:可以安全地假设每个JVM应用程序线程都由一个惟一的OS线程支持,这个OS线程是在相应的线程对象上调用start()方法时创建的。Java的多线程方法可以追溯到20世纪90年代末,它具有以下基本的设计原则:Java进程中的所有线程共享一个公共的被垃圾收集的堆。一个线程创建的任何对象都可以被具有该对象引用的任何其他线程访问。对象在默认情况下是可变的;也就是说,对象字段中保存的值可以更改,除非程序员显式地使用final关键字将其标记为不可变。Java内存模型(JMM)是一个正式的内存模型,它解释了不同的执行线程如何看到对象中保存的值的变化。也就是说,如果线程A和B都有对象obj的引用,并且线程A更改了它,那么线程B中观察到的值会发生什么变化?这个看似简单的问题实际上比它看起来更复杂,因为操作系统的调度器(我们将在第3章中讨论)可以强制从CPU核【CPU cores】中清除线程。这可能导致另一个线程在原始线程完成处理之前开始执行和访问对象,并可能看到该对象处于损坏或无效的状态。在并发代码执行期间,Java核心提供的针对这种潜在对象损坏的惟一防御方式是互斥锁,在实际应用程序中使用这种锁可能非常复杂。第12章详细介绍了JMM的工作原理,以及使用线程和锁的实用性。2.6 Meet the JVMs许多开发人员可能只熟悉Oracle生成的Java实现。我们已经遇到了来自Oracle实现的虚拟机HotSpot。然而,我们将在本书中以不同深度讨论其他几种实现:OpenJDK:OpenJDK是一个有趣的特例。它是一个开源(GPL)项目,提供了Java的参考实现。该项目由Oracle领导和支持,并为其Java版本提供了基础。Oracle:Oracle的Java是最广为人知的实现。它基于OpenJDK,但在Oracle的专有许可下进行了重新授权。对Oracle Java的几乎所有更改都是从提交到OpenJDK公共存储库开始的(尚未公开的安全修复除外)。Zulu:Zulu是一个免费的(gpl许可的)OpenJDK实现,完全通过java认证,由Azul Systems提供。它不受专有许可证的限制,可以自由地重新分发。Azul是为数不多的为OpenJDK提供付费支持的供应商之一。IcedTea:Red Hat是第一个基于OpenJDK生成完全经过认证的Java实现的非oracle供应商。IcedTea是完全认证的,可以重新分销。Zing:Zing是一种高性能专有JVM。它是经过完全认证的Java实现,由Azul Systems生产。它只支持64位Linux,并且是为具有大堆(10s的100 GB)和大量CPU的服务器类系统设计的。J9:IBM的J9最初是作为专有JVM出现的,但在其生命的中途是开源的(就像HotSpot一样)。它现在构建在Eclipse开放运行时项目(OMR)的基础上,并形成了IBM专有产品的基础。它完全符合Java认证。Avian:就认证而言,Avian实现并不是100%符合Java。之所以将其包含在这个列表中,是因为它是一个有趣的开源项目,并且对于那些希望了解JVM如何工作的细节(而不是作为一个100%可生产的解决方案)的开发人员来说,它是一个很好的学习工具。Android:谷歌的Android项目有时被认为是“基于Java的”。然而,实际情况要复杂一些。Android最初使用不同的Java类库实现(来自clean-room Harmony项目)和交叉编译器来为非jvm虚拟机转换不同的(.dex)文件格式。在这些实现中,本书的大部分重点放在HotSpot上。这些内容同样适用于Oracle Java、Azul Zulu、Red Hat IcedTea和所有其他openjdk派生的jvm。NOTE:在比较类似的版本时,各种基于HotSpot的实现之间基本上没有性能相关的差异。我们还包括一些与IBM J9和Azul Zing相关的材料。 这旨在提供对这些替代方案的认识,而不是明确的指南。 有些读者可能希望更深入地探索这些技术,并鼓励他们以通常的方式设定绩效目标,然后进行衡量和比较。Android正在转向使用OpenJDK 8类库,并在Android运行时直接支持。 由于这个技术堆栈与其他示例相差甚远,因此我们不会在本书中进一步考虑Android。关于许可的说明我们将讨论的几乎所有JVM都是开源的,事实上,其中大多数都来自GPL许可的HotSpot。 例外的是IBM的Open J9,它是Eclipse许可的,而Azul Zing是商业的(尽管Azul的Zulu产品是GPL)。Oracle Java(从Java 9开始)的情况稍微复杂一些。 尽管源自OpenJDK代码库,但它是专有的,并非开源软件。 Oracle通过让OpenJDK的所有贡献者签署许可协议来实现这一目标,该许可协议允许双重许可他们对OpenJDK的GPL和Oracle的专有许可的贡献。Oracle Java的每个更新版本都被视为OpenJDK主线的一个分支,然后在未来的版本中不会在分支上进行修补。 这可以防止Oracle和OpenJDK的分歧,并且可以解释Oracle JDK和基于相同源的OpenJDK二进制文件之间缺乏有意义的差异。这意味着Oracle JDK和OpenJDK之间唯一真正的区别就是许可证。 这看起来似乎无关紧要,但Oracle许可证包含一些开发人员应该注意的条款:Oracle不授予在您自己的组织之外重新分发其二进制文件的权利(例如,作为Docker映像)。在没有得到Oracle二进制文件的同意(通常是指支持合同)的情况下,不允许对其应用二进制补丁。Oracle还提供了其他一些商业特性和工具,这些特性和工具只适用于Oracle的JDK,并且在其许可的范围内。但是,随着将来Oracle发布Java版本,这种情况将会发生变化,我们将在第15章中对此进行讨论。在计划新的绿色部署时,开发人员和架构师应该仔细考虑他们对JVM供应商的选择。一些大型组织,特别是Twitter和阿里巴巴,甚至维护他们自己的OpenJDK私有构建,尽管许多公司无法完成所需的工程工作。2.7 Monitoring and Tooling for the JVMJVM是一个成熟的执行平台,它为运行中的应用程序的检测、监视和可观察性提供了许多技术选择。这些类型的JVM应用程序工具可用的主要技术有:Java Management Extensions (JMX)Java agentsThe JVM Tool Interface (JVMTI)The Serviceability Agent (SA)JMX是一种功能强大的通用技术,用于控制和监视jvm及其上运行的应用程序。它提供了从客户机应用程序以一般方式更改参数和调用方法的能力。不幸的是,完整的论述超出了本书的范围。然而,JMX(及其相关的网络传输,RMI)是JVM管理功能的一个基本方面。Java代理是用Java编写的工具组件(因此得名),它使用Java .lang中的接口。用来修改字节码的方法。要安装代理,请向JVM提供一个启动标志:-javaagent:<path-to-agent-jar>=<options>
该代理JAR必须包含一个清单并包含Premain-Class属性。此属性包含代理类的名称,代理类必须实现充当Java代理注册钩子的公共静态premain()方法。如果Java插装的API不够,则可以使用JVMTI。 这是JVM的本机接口,因此使用它的代理必须使用本机编译语言(本质上是C或C ++)编写。 它可以被认为是一种通信接口,允许本机代理监视JVM并通知事件。 要安装本机代理,请提供稍微不同的标志:-agentlib:<agent-lib-name>=<options>
或者-agentpath:<path-to-agent>=<options>
JVMTI代理必须用本机代码编写,这意味着编写可能破坏正在运行的应用程序甚至使JVM崩溃的代码要容易得多。在可能的情况下,通常最好在JVMTI代码上编写Java代理。代理的编写要简单得多,但是一些信息不能通过Java API获得,访问这些数据JVMTI可能是惟一的可能。最后一种方法是可服务性代理。这是一组api和工具,可以公开Java对象和HotSpot数据结构。SA不需要在目标VM中运行任何代码。HotSpot SA使用符号查找和进程内存读取等原语来实现调试功能。SA能够调试实时Java进程和核心文件(也称为崩溃转储文件)。VisualVMJDK附带了许多有用的附加工具以及著名的二进制文件,如javac和java。一个经常被忽略的工具是VisualVM,它是一个基于NetBeans平台的图形化工具。TIP:jvisualvm是早期Java版本中现已过时的jconsole工具的替代品。如果您仍然使用jconsole,那么您应该迁移到VisualVM(有一个兼容性插件允许jconsole插件在VisualVM中运行)。Java的最新版本已经提供了VisualVM的稳定版本,JDK中提供的版本现在通常已经足够了。但是,如果需要使用最新版本,可以从http://visualvm.java.net/下载最新版本。下载之后,您必须确保visualvm二进制文件被添加到您的路径中,否则您将获得JRE默认二进制文件。TIP:从Java 9开始,VisualVM将从主发行版中删除,因此开发人员必须单独下载二进制文件。当VisualVM第一次启动时,它将对正在运行的机器进行校准,因此不应该有其他应用程序在运行,这可能会影响性能校准。校准后,VisualVM将完成启动,并显示一个闪屏。VisualVM最熟悉的视图是Monitor屏幕,类似于图2-4所示。图2 - 4 VisualVM显示屏VisualVM用于对正在运行的进程进行实时监视,它使用JVM的附加机制。 根据进程是本地进程还是远程进程,这种方式略有不同。本地流程相当简单。 VisualVM将它们列在屏幕的左侧。 双击其中一个会使其在右侧窗格中显示为新选项卡。要连接到远程进程,远程端必须接受入站连接(通过JMX)。 对于标准Java进程,这意味着jstatd必须在远程主机上运行(有关更多详细信息,请参阅jstatd的手册页)。NOTE:许多应用服务器和执行容器直接在服务器中提供与jstatd等效的功能。这样的进程不需要单独的jstatd进程。要连接到远程进程,请输入选项卡上使用的主机名和显示名。要连接的默认端口是1099,但这可以很容易地更改。VisualVM为用户提供了五个选项卡:Overview:提供有关Java进程的信息摘要。这包括传入的完整标志和所有系统属性。它还显示正在执行的Java版本。Monitor:这是与遗留JConsole视图最相似的选项卡。它展示了JVM的高级遥测,包括CPU和堆的使用情况。它还显示了加载和卸载的类的数量,以及运行的线程数量的概述。Threads:正在运行的应用程序中的每个线程都显示一个时间轴。这包括应用程序线程和VM线程。可以通过少量历史记录看到每个线程的状态。如果需要,还可以生成线程转储。Sampler and Profiler:在这些视图中,可以访问简化的CPU和内存利用率抽样。这将在第13章中详细讨论。VisualVM的插件体系结构允许将其他工具轻松添加到核心平台,以增强核心功能。其中包括允许与JMX控制台交互并桥接到遗留JConsole的插件,以及非常有用的垃圾收集插件VisualGC。2.8 Summary在这一章中,我们快速浏览了JVM的总体结构。它只可能触及到一些最重要的主题,实际上这里提到的每个主题背后都有一个丰富、完整的故事,值得进一步研究。在第3章中,我们将讨论操作系统和硬件如何工作的一些细节。这为Java性能分析人员理解观察到的结果提供了必要的背景。我们还将更详细地查看时序子系统,作为VM和本机子系统如何交互的完整示例。}

我要回帖

更多关于 java 超时机制实现 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信