HotSpot VM
谈到Java的性能,runtime的两个方面很关键:JIT和GC。JIT的作用使尽可能快地执行代码;GC的作用是(在管理存储的同时)从代码的执行中抽取尽可能少的时间。因而Java的性能是让JIT(在更多存储器的帮助下)产生更多理想代码,并减少GC用以管理存储的时间(指针越大这越困难)
随着64位的HotSoptVM的出现,虽然64位CPU拥有更宽的数据总线,但是这却使得java中对象的指针(oop)消耗了更多的空间,这使得更少的oop可以保存在CPU的缓存中。所以这里要使用compressed oops(-XX:+UseCompressedOops)。
HotSpot的命令行分为三种:标准命令行,非标准命令行(-x前缀),开发命令行(-xx前缀)
VM的生命周期:
开启HotSpot VM的元素叫做launcher。最常见的launcher是java和javaw命令。嵌入式的jvm是JNI_CreateJavaVM。 javaws是用于web浏览器启动applets的。
启动器会执行下面几个步骤:
1.解析命令行参数。如-client,-server
2.创建java堆和JIT编译器的类型(client Server)
3.创建环境变量(CLASSPATH)
4.如果Main Class没有指定,则会分析JAR的manifest文件来找到Main-Class
5.使用java的Native接口方法JNI_CreateJavaVM在一个新创建的非原生的线程中创建HotSpot VM
原生的线程是当HotSpot VM启动时,操作系统分配的。创建HotSpot VM的线程之所以是非原生的,是为了可以定制 如改变栈的大小等。
6.当HotSpot VM创建和初始化完成之后,java的Main-Class就被加载了。
7.HotSpot VM通过调用java的native接口方法CallStaticVoidMethod来执行main方法
这里需要读者去了解VM对类的加载机制,类文件的验证机制。
这里我要说说VM的同步机制:
大家可能认为java的synchronization耗费很多的资源,其实事实不全是这样。
java中的synchronization分为两种 竞争同步和非竞争同步
而非竞争同步是构成了应用程序很大的一部分。HotSpot使用一个叫fast-path code的方法来显示非竞争同步,这个开销是很小的,只有20到250个时钟周期。然后,当需要实现阻塞或唤醒线程时,fast-path会调用slow-path来实现。slow-path是用C++编写的,fast-path是JIT编译器生成的依赖于机器的语言实现的。
现代的JVM可以解除确证不存在的锁。如果经过分析一个锁对象只能由一个线程访问,那么jvm就会去除这个锁请求。
这里介绍一个参数:-XX:+UseBiasedLocking
Enables a technique for improving the performance of uncontended synchronization. An object is "biased" toward the thread which first acquires its monitor via a monitorenter bytecode or synchronized method invocation; subsequent monitor-related operations performed by that thread are relatively much faster on multiprocessor machines. Some applications with significant amounts of uncontended synchronization may attain significant speedups with this flag enabled; some applications with certain patterns of locking may see slowdowns, though attempts have been made to minimize the negative impact.
实际上,Sun采用的UseBiasedLocking是Initail Locker的方式,即第一个获取锁的线程,JVM会为它保留锁(不需要原子性操作),从而,在其后,该线程获取锁等同于uncontended synchronization的效果。延迟锁(或者保留锁)都是忌讳频繁的多线程竞争锁的情形。Java锁一般都符合保留锁的条件,即大部分情况下,在某个时间片内,都是锁都是被某个线程独占。
java使用自适应自旋锁来改善竞争同步的吞吐量。
The terms contended and uncontended refer to how many threads are operating on a particular lock. A lock that is not held by anythread is an uncontended lock: the first thread that attempts toacquire it immediately succeeds.
When a thread attempts to acquire a lock that is already held by another thread, the lock becomes a contended lock. A contended lock has at least one thread waiting for it; it may have many more. Note that a contended lock becomes an uncontended one when threads are no longer waiting to acquire it.
VM垃圾回收器
两个假设:
1 大多数新生成的对象会很快的需要回收
2. 很少有老年对象会指向新生对象
所以jvm使用分代来管理java堆:新生代和老年代。永久代属于方法区。
新生代的回收是频繁的和高效的。老年代的回收是不频繁的和低效的。分代是为了针对每个区的特性 更好地设计垃圾回收算法。
为了是minor垃圾回收更高效,垃圾回收期使用一个叫card table的数据结构。
卡表的机制是将“老生代”以512字节为单位进行划分,划分得到的每个区域叫做一个卡。每个卡在卡表中有占用一个一个字节的标识。java代码在执行的过程中JVM一旦发现“老生代”的对象引用了或者释放了“新生代”中的对象,那么JVM就要将与之对应的卡表中的状态置为相应的值。这样在次收集的时候只遍历被标记为“脏”的卡,以便知道哪些“新生代”的对象被引用中,是不可以进行回收的。
空的eden使得VM内存的分配更加有效,它使用bump-the-pointer技术,最后一个被分配的对象的内存地址总是可以被记录。当要新分配一个对象时,VM只要检查剩余的空间是否够分配。
很多java程序是多线程的,它们的内存分配操作需要是线程安全的,如果仅仅是加一个全局锁的话。那么分配内存的锁很快会成为一个瓶颈。这里java使用TLABs(Thread-Local Allocation Buffers),他给每个线程一个缓存(eden区的一小段空间)来分配内存,这样每个线程的分配内存操作可以很快进行。
JIT编译器
Java HotSpot虚拟机可以运行在两种模式下:client或者server。你可以在JVM启动时通过配置-client或者-server选项来选择其中一种。两种模式都有各自的适用场景。
两种模式最主要的区别是server模式下会进行更激进的优化 —— 这些优化是建立在一些并不永远为真的假设之上。一个简单的保护条件(guard condition)会验证这些假设是否成立,以确保优化总是正确的。如果假设不成立,Java HotSpot虚拟机将会撤销所做的优化并退回到解释模式。也就是说Java HotSpot虚拟机总是会先检查优化是否仍然有效,不会因为假设不再成立而表现出错误的行为。
在server模式下,Java HotSpot虚拟机会默认在解释模式下运行方法10000次才会触发JIT编译。可以通过虚拟机参数-XX:CompileThreshold来调整这个值。比如-XX:CompileThreshold=5000会让触发JIT编译的方法运行次数减少一半。(译者注:有关JIT触发条件可参考《深入理解Java虚拟机》第十一章以及《Java Performance》第三章HotSpot VM JIT Compilers小节)
这可能会诱使新手将编译阈值调整到一个非常低的值。但要抵挡住这个诱惑,因为这样可能会降低虚拟机性能,优化后减少的方法执行时间还不足以抵消花在JIT编译上的时间。
当Java HotSpot虚拟机能为JIT编译收集到足够多的统计信息时,性能会最好。当你降低编译阈值时,Java HotSpot虚拟机可能会在非热点代码的编译中花费较多时间。有些优化只有在收集到足够多的统计信息时才会进行,所以降低编译阈值可能导致优化效果不佳。
HOTSPOT有两个计数器:方法调用计数器和回边计数器
方法调用计数器client默认1500次,server默认10000次,可以通过参数-XX:CompileThreshold来设定。调用方法时,会先判断是否存在编译过的版本,如果有则调用该版本,否则计数器加1,然后看方法调用计数器和回边计数器之和是否超过方法调用计数器的阈值。超过,则提交编译请求
方法调用计数器并不是统计方法调用绝对次数,而是一个相对执行频率,超过一定时间,如果方法调用次数不足以让它提交给编译器,则计数器就会被减少一半,这种现象称为热度衰减(Counter Decay),进行热度衰减的动作是在垃圾回收时顺便进行的,而这段时间就被称为半衰周期(Counter Half Life Time)可用-XX:-UseCounterDecay来关闭热度衰减,用-XX:CounterHalfLifeTime来设置半衰时间。
回边计数器用于统计方法中循环体的执行次数。字节码遇到控制流向后跳转 的指令成为回边。建立回边计数器统计的目的就是为了触发OSR编译。回边的控制参数有:
-XX:BackEdgeThreshold,-XX:OnStackReplacePercentage。
1.在Client模式下,回边计数器阀值计算公式:方法调用计数器阀值乘以OSR比率,然后除以100.
2.在server模式下,回边计数器阀值计算公式:方法调用计数器阀值乘以(OSR比率,然后减去解释器监控比率的差值)除以100。
与方法计数器不同,回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。