Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构是基于寄存器的指令集架构。
基于栈式架构的特点
基于寄存器架构的特点
类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
通过一个类的全限定名获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
为类变量分配内存并且设置该类变量的默认初始值,即零值。
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化(TO DO)
将常量池内的符号引用转换为直接引用的过程。
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT FieldRef info、CONSTANT MethodRef info
等
()
的过程()
不同于子类的构造器()
执行前,父类的()
已经执行完毕(clint执行的是类中的实例属性以及静态代码块部分)()
方法在多线程下被同步加锁(即Class文件只会被加载一次)启动类加载器(引导类加载器,Bootstrap ClassLoader)
JAVA_HOME/jre/lib/rt.jar
、resources.jar
等,用于提供JVM自身需要的类扩展类加载器(Extension ClassLoader)
应用程序类加载器(系统类加载器 AppClassLoader)
Java虚拟机对class文件采用的是按需加载对方式,也就是说需要使用该类时才会将它的class文件加载到内存中生成class对象,而且加载某一个类的class文件时,Java虚拟机采用的双亲委派机制,即把请求交由父类处理。
当加载jdbc.jar
用于实现数据库连接的时候,首先jdbc.jar
是基于SPI
接口实现的,所以在加载的时候,会进行双亲委派机制,最后从根加载器加载SPI
核心类,然后在加载SPI
接口类,但是根加载器无法加载各个厂商的实现类,只能通过线程上下文类加载器来获取系统类加载器来进行实现类的加载。
JVM必须知道一个类是由启动类加载器加载还是用户类加载器加载,如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
一个JVM只有一个RunTime的实例
JVM允许一个应用有多个线程并行的执行,在Hotspot JVM里,每一个线程都对应着操作系统的本地线程(当一个Java线程准备好执行之后,此时一个操作系统的本地线程也同时创建,Java线程执行终止,本地线程也会回收)。操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()
方法。
并非是广义上所指的物理寄存器,是一块很小的内存空间,是运行速度最快的存储区域。
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
每一个线程在创建时都会创建一个虚拟机栈,其内部保存着一个个栈帧,对应着Java方法的调用**(是线程私有的)**
是一种快速有效的分配存储方式,访问速度仅此于程序计数器,栈不存在垃圾回收的问题,栈会出现溢出的情况。
Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError
异常。
如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 outofMemoryError
异常。
我们可以使用参数 -Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
-Xss1m
-Xss1k
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
栈帧的大小主要由局部变量表 和 操作数栈决定的
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用reference
,以及returnAddress
类型。
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量。
this对象是0索引位置
int float reference32位
long double 64位
Slot的重复利用
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
例:
public class SlotTest() {
public void localVarl() {
int a = 0;
System.out.println(a);
int b = 0;
}
public void localVal2() {
{
int a = 0;
System.out.println(a);
}
// 此时b就会复用a的槽位
b = 0;
}
}
变量的分类:
在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出的操作数栈。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的max_stack
中
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
每一个栈帧内部都包含一个指向运行时运行常量池中该栈帧所属方法的引用包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)
动态链接的作用就是将这些符号引用转换为调用方法的直接引用。
在Java源文件被编译到字节码文件中时,所有的变量和方法的引用都作为符号引用保存在class文件的常量池中。
因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间
常量池的作用:就是为了提供一些符号和常量,便于指令的识别
JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
子类对象的多态的使用前提
普通调用指令
invokestatic
:调用静态方法,解析阶段确定唯一方法版本invokespecial
:调用方法、私有及父类方法,解析阶段确定唯一方法版本invokevirtual
:调用所有虚方法invokeinterface
:调用接口方法动态调用指令
invokedynamic
:动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic
指令则支持由用户确定方法版本。其中invokestatic
指令和invokespecial
指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
一个Native Method是一个Java调用非Java代码的接口,在定义一个native method时,并不提供实现体,因为实现体是由非Java语言在外面实现的。
标识符native可以与其他java的标识符连用,但是abstract除外
Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者对于效率要求比较高时,就需要使用Native Method
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。本地方法栈,也是线程私有的。允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方法是相同的)
当某一个线程调用本地方法时,就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含了多个线程,共享同一个堆空间。
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域(堆内存大小是可以调节的)。Java堆区在JVM启动的时候被创建,其空间大小也就确定了。
堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。
-Xms10m: 最小堆内存
-Xmx10m: 最大堆内存
“几乎”所有的对象实例都在这里分配内存。一从实际使用角度看的(因为还有一些对象是在栈上分配的)
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会删除。
Java7 及之前堆内存逻辑上分为三部分:新生区 + 养老区 + 永久区
Java8 及之后堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间
Java堆区用于存储Java对象的实例
-Xms : 用于表示堆区的起始内存,等价于-XX:InitialHeapSize
-Xmx : 用于表示堆区的最大内存,等价于-XX:MaxHeapSize
一旦堆区中的内存大小超过“-Xmx所指定的最大内存时,将会跑出OOM异常”
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况下
如何查看堆内存的内存分配情况
jps -> jstat -gc 进程id
-XX:+PrintGCDetails
JVM在进行GC的时候,并非每次都对上面的三个内存区域一起回收,大部分回收都是指新生代。针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)
当年轻代空间不足时,就会触发MinorGC,这里的年轻代满是指Eden代满,Survivor满不会引发GC
MinorGC会引发STW,暂停其他用户的线程,等待垃圾回收结束,用户线程才能恢复运行
指发生在老年代的GC,对象从老年代消失时,我们说 “Major Gc” 或 “Full GC” 发生了
也就是在老年代空间不足时,会先尝试触发MinorGc。如果之后空间还不足,则触发Major GC
Major GC的速度一般会比MinorGc慢10倍以上,STW的时间更长,如果Major GC后,内存还不足,就报OOM了
触发Full GC执行的5种情况
TLAB:Thread Local Allocation Buffer,也就是为每个线程单独分配一个缓冲区
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
在程序中,开发人员可以通过选项-Xx:UseTLAB
设置是否开启TLAB空间。
默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1,当然我们可以通过选项-Xx:TLABWasteTargetPercent
设置TLAB空间所占用Eden空间的百分比大小。
一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
对象首先是通过TLAB开辟空间,如果不能放入,那么需要通过Eden来进行分配
-XX: +PrintFlagsInitial: 查看所有的参数的默认初始值
-XX: +PrintFlagsFinal: 查看所有的参数的最终值
-Xms: 初始堆空间内存(默认为物理内存的1/64)
-Xmx: 最大堆空间内存(默认为物理内存的1/4)
-Xmn: 设置新生代的大小(初始值及最大值)
-XX: NewRatio: 配置新生代与老年代在堆结构的占比
-XX: SurvivorRatio: 设置新生代中Eden和S0/S1空间的比例
-XX: MaxTenuringThreshold: 设置新生代垃圾的最大年龄
-XX: +PrintGCDetails: 输出详细的GC处理日志
-XX:HandlePromotionFalilure: 是否设置空间分配担保
在发生MinorGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
方法区看作是一块独立于Java堆的内存空间
方法区主要存放的是Class,而堆中主要存放的是实例化对象
在JDK7及以前,习惯上把方法区,称为永久代。JDK8开始,使用元空间取代了永久代。
JDK1.8后,元空间存放在堆外内存中
而到了JDK8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替
根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常
方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。
JDK7及以前
-xx:Permsize
来设置永久代初始分配空间。默认值是20.75M-XX:MaxPermsize
来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82MOutOfMemoryError: PermGen space
。JDK8以后
元数据区大小可以使用参数-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
指定
默认值依赖于平台。windows下,-XX:MetaspaceSize
是21M,-XX:MaxMetaspaceSize
的值是-1,即没有限制。
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError: Metaspace
-XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-xx:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,FullGC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值(动态改变MetaSpaceSize的大小)
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到FullGC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize
设置为一个相对较高的值。
常量池中有什么?
小结:常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等类型。
运行时常量池是方法区的一部分
常量池是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池
JVM为每一已加载的类型(类或接口)都维护一个常量池。池中的数据像数组项一样,是通过索引访问的
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换成了真实地址。
运行时常量池,相对于Class文件常量池的另一个重要特征:具备动态性(intern)
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OutoOfMemoryError异常。
JDK1.6及以前:有永久代,静态变量存储在永久代上
JDK1.7:有永久代,字符串常量池,静态变量移除,保存在堆上
JDK1.8:无永久代,字段,方法,常量保存在本地内存的元空间中,但字符串常量池、静态变量仍然在堆上
在某些场景下,如果动态加载类过多,容易产生Perm区的oom。比如某个实际Web工 程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。
JDK7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而fullgc是老年代的空间不足、永久代不足时才会触发。
这就导致stringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
静态引用对应的对象实体始终都存在堆空间中
JDK7及其以后版本的HotSpot虚拟机选择把静态变量与类型在Java语言一端的映射class对象存放在一起,存储于Java堆之中。
方法区的垃圾是必要的,但是回收的效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型
方法区常量池中主要存放的两大类常量:字面量和符号引用,包括下面三种常量:
判断一个常量是否废弃比较简单,而判断一个类型是否属于“不再被使用的类”的条件就比较苛刻了
在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及oSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
直接指针(hotspots)
直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据
DirectByteBuffer
操作Native内存由于直接内存在Java堆外,因此大小不会直接受限于-xmx指定的最大堆大小,但是系统内存是有限的
直接内存大小通过MaxDirectMemorySize
设置
如果不指定,默认与堆的最大值-xmx参数一致
执行引擎是Java虚拟机核心的组成部分之一,里面包括 解释器、即时编译器、垃圾回收器
“虚拟机”是一个相对于“物理机”,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。
执行引擎是将字节码指令解释/编译为对应平台上的本地机器指令
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
JIT(Just In Time Compiler)编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。
Java也发展出可以直接生成本地代码的编译器。现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。
翻译成本地代码后,就可以做一个缓存操作,存储在方法区中
JVM设计者们的初衷仅仅只是单纯地为了满足Java程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。(同样也是跨语言的,也就是任何可以编译为字节码文件都能运行在JVM上)
在Java的发展历史里,一共有两套解释执行器,即古老的字节码解释器、现在普遍使用的模板解释器。
JIT是just in time的缩写,也就是即使编译器。使用即使编译器技术,能够加速Java程序的执行速度。
在运行时 JIT 会把翻译过的机器码保存起来,以备下次使用,因此从理论上来说,采用该 JIT 技术可以接近以前纯编译技术。下面我们看看,JIT 的工作过程。
首先,如果这段代码本身在将来只会被执行一次,那么从本质上看,编译就是在浪费精力。因为将代码翻译成 java 字节码相对于编译这段代码并执行代码来说,要快很多。
如果一段代码被多次执行,那么编译就非常值得了。Hot Spot VM采用了JIT compile技术,将运行频率很高的字节码直接编译为机器指令执行以提高性能。运行时,部分代码可能由 JIT 翻译为目标机器指令(以 method 为翻译单位,还会保存起来,第二次执行就不用翻译了)直接执行。
当 JVM 执行某一方法或遍历循环的次数越多,就会更加了解代码结构,那么 JVM 在编译代码的时候就做出相应的优化。
第一种是将源代码编译成字节码文件,然后在运行时通过解释器将字节码文件转为机器码执行
第二种是编译执行(直接编译成机器码)。现代虚拟机为了提高执行效率,会使用即时编译技术(JIT,Just In Time)将方法编译成机器码后再执行
HotSpot VM是目前市面上高性能虚拟机的代表作之一。它采用解释器与即时编译器并存的架构。在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。
JRockit虚拟机是砍掉了解释器,也就是只采及时编译器。那是因为呢JRockit只部署在服务器上,一般已经有时间让他进行指令编译的过程了,对于响应来说要求不高,等及时编译器的编译完成后,就会提供更好的性能
当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。 编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码后,执行效率高。
当Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。
在生产环境发布过程中,以分批的方式进行发布,根据机器数量划分成多个批次,每个批次的机器数至多占到整个集群的1/8。曾经有这样的故障案例:某程序员在发布平台进行分批发布,在输入发布总批数时,误填写成分为两批发布。如果是热机状态,在正常情况下一半的机器可以勉强承载流量,但由于刚启动的JVM均是解释执行,还没有进行热点代码统计和JIT动态编译,导致机器启动之后,当前1/2发布成功的服务器马上全部宕机,此故障说明了JIT的存在。—阿里团队
解释执行:优点是启动效率快、占用内存少,缺点是整体的执行速度较慢、占用程序运行时间和运算资源。
即时编译:相比于解释器,即时编译将部分“热点代码”编译成本地代码,并进行优化,提高了执行效率。相比于提前编译,内存占用小(大部分应用仅20%的功能常用,80%可以不编译)、可优化及编译动态加载的Class文件、可进行激进预测性优化(失败后可退回到低级编译器甚至解释器上执行)、可获得热点代码集中优化和分配更好的资源。(JIT)
提前编译:改善启动时间,快速达到最高性能,缺点在即时编译对比中可看出。(AOT)
一个被多次调用的方法,一个方法题内部循环次数较多都可以被称为“热点代码”,因此被JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此被称之为栈上替换简称为OSR
一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才能达到这个标准呢?
采用基于计数器的热点探测,HotSpot V将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。
这个计数器就用于统计方法被调用的次数,它的默认阀值在Client模式下是1500次,在Server模式下是10000次。超过这个阈值,就会触发JIT编译。
当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阀值。如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。
如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)
它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译。
-Xint: 完全采用解释器模型运行程序
-Xcomp: 完全采用即时编译器模式执行程序,如果即时编译出现问题,解释器会介入执行
-Xmixed: 采用解释器+即时编译器的混合模式共同执行程序。
C1Client mode
编译器进行简单的优化,C2server mode
是耗时较长的优化,以及激进优化
C1的优化策略
方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程
去虚拟化:对唯一的实现樊进行内联
冗余消除:在运行期间把一些不会执行的代码折叠掉
C2的优化策略
标量替换:用标量值代替聚合对象的属性值
栈上分配:对于未逃逸的对象分配对象在栈而不是堆
同步消除:清除同步操作,通常指synchronized
private final char value[]; // 以前
private final byte[] value;
String不再使用char[]来存储了,改成了byte[]加上编码标记,节约了一些空间
字符串常量池是不会存储相同内容的字符串
String 的string Pool是一个固定大小的HashTable,默认值大小为1009。如果放进string Pool的string非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用string.intern时性能会大幅下降。
在JDK6中,StringTable可以设置的最小值为1009。
在JDK7中,StringTable的长度默认值是60013。
在JDK8中,StringTable可以设置的最小值为1009。
注意:
如果左右两边都是变量的话,就是需要new StringBuilder
进行拼接,但是如果是用final来进行修饰,则是从常量池中获取
@Test
public void test() {
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4); // true
}
通过StringBuilder的append()方式来添加字符串的效率,要远远高于String的字符串拼接方式
好处:
intern是一个native方式,调用的是底层C的方法
JDK1.6 中,将这个字符串对象尝试放入串池
JDK1.7中,将这个字符串对象尝试放入串池
Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象。
两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。