Java性能调优之JVM

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使用自适应自旋锁来改善竞争同步的吞吐量。

Contended and Uncontended Lock

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就要将与之对应的卡表中的状态置为相应的值。这样在次收集的时候只遍历被标记为“脏”的卡,以便知道哪些“新生代”的对象被引用中,是不可以进行回收的。

新生代的组成
        “新生代”又3个部分组成,见图3。一个Eden和两个生存区( Survivor Space ),
图3:新生代组成
其中:
l        Eden :绝大部分新创建的对象存放在此区域。为什么说绝大多数而不是所有的呢?原因是应用在创建一个非常大的对象的时候JVM会直接将其分配在老生代而非新生代。在每次完成次收集的时候Eden区域总是空的。
l        存活区:顾名思义在垃圾收集过程中没有被当作垃圾收集的对象将放在次区域中。也就是说这个区域中的对象至少经历了一次次收集。存放存活区的对象在被“提升”到老生代前还有机会被收集。存活区有一对,他们中的一个始终保持为空,另一个用于存放存活下来的对象。
  图4描述了次收集的收集过程,其中绿色部分是未被使用的对象
图4:一次次收集
(即垃圾)。从图中可以看到在Eden区的绿色部分将被收集而幸存下来的对象(白色部分)将被移到没有被使用的存活区2。在存活区1中的绿色部分是也是不被使用的对象,这些对象也将被收集。而位于存活区1中的蓝色部分是尚被引用但是还不够“老”的这些对象也将移到到存活区2。存活区1中剩余的部分就是被引用且已经够“老”的对象,他们将被移到老生代区。
        在完成了一次次收集后(见图5),两个存活区就会交互角色。即存活区2中将存放存活对象,存活区1将不被使用。Eden区将会变的空空如也。同时由于有新对象移到了老生代,老生代的空间将被更多的对象占据。
图5:完成一次次收集后
一个minor GC后 eden区是空的,只有一个suivivor区是被使用的,另一个是空的。

空的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。

       与方法计数器不同,回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。

相关文章