JVM从入门到精通

JVM 原理及调优

Posted by leone on 2018-11-18

JVM

jvm 简介

什么是jvm

Java虚拟机概貌
Java虚拟机(英语:Java Virtual Machine,缩写为JVM),一种能够运行Java bytecode的虚拟机,以堆栈结构机器来进行实做。最早由太阳微系统所研发并实现第一个实现版本,是Java平台的一部分,能够运行以Java语言写作的软件程序。

Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。通过对中央处理器(CPU)所执行的软件实现,实现能执行编译过的Java程序码(Applet与应用程序)。

作为一种编程语言的虚拟机,实际上不只是专用于Java语言,只要生成的编译文件匹配JVM对加载编译文件格式要求,任何语言都可以由JVM编译运行。此外,除了甲骨文,也有其他开源或闭源的实现。

jvm 种类

HotSpot VM

HotSpot VM是绝对的主流。大家用它的时候很可能就没想过还有别的选择,或者是为了迁就依赖了Oracle/Sun JDK某些具体实现的烂代码而选择用HotSpot VM省点心。

Oracle / Sun JDK、OpenJDK的各种变种(例如IcedTea、Zulu),用的都是相同核心的HotSpot VM。
从Java SE 7开始,HotSpot VM就是Java规范的“参考实现”(RI,Reference Implementation)。把它叫做“标准JVM”完全不为过。

当大家说起“Java性能如何如何”、“Java有多少种GC”、“JVM如何调优”等等,经常默认说的就是特指HotSpot VM。可见其“主流性”。
(其实这不是件好事;具体到JVM实现才可以讨论的问题还是应该指明讨论是基于哪个实现)

JDK8的HotSpot VM已经是以前的HotSpot VM与JRockit VM的合并版,也就是传说中的“HotRockit”,只是产品里名字还是叫HotSpot VM。
这个合并并不是要把JRockit的部分代码插进HotSpot里,而是把前者一些有价值的功能在后者里重新实现一遍。移除PermGen、Java Flight Recorder、jcmd等都属于合并项目的一部分。

不过要留意的是,这里我说的HotSpot VM特指“正常配置”版,而不包括“Zero / Shark”版。Wikipedia那个页面上把后者称为“Zero Port”。用这个版本的人应该相当少,很多时候它的release版都build不成功…

J9 VM

J9是IBM开发的一个高度模块化的JVM。

在许多平台上,IBM J9 VM都只能跟IBM产品一起使用。这不是技术限制,而是许可证限制。
例如说在Windows上IBM JDK不是免费公开的,而是要跟IBM其它产品一起捆绑发布的;使用IBM Rational、IBM WebSphere的话都有机会用到J9 VM(也可以自己选择配置使用别的Java SE JVM)。
根据许可证,这种捆绑在产品里的J9 VM不应该用于运行别的Java程序,但是大家自己“偷偷的”拿来跑别的程序,IBM也没力气管。而在一些IBM的硬件平台上,很少客户是只买硬件不买配套软件的,IBM给一整套解决方案,里面可能就包括了IBM JDK。这样自然而然就用上了J9 VM。所以J9 VM得算在主流里,虽然很少是大家主动选择的首选。

J9 VM的性能水平大致跟HotSpot VM是一个档次的。有时HotSpot快些,有时J9快些。不过J9 VM有一些HotSpot VM在JDK8还不支持的功能,最显著的一个就是J9支持AOT编译和更强大的class data sharing。

JRockit

以前Java SE的主流JVM中还有JRockit,跟HotSpot与J9一起并称三大主流JVM。这三家的性能水平基本都在一个水平上,竞争很激烈。
自从Oracle把BEA和Sun都收购了之后,Java SE JVM只能二选一,JRockit就炮灰了。JRockit最后发布的大版本是R28,只到JDK6;原本在开发中的R29及JDK7的对应功能都没来得及完成项目就被终止了。

想在.NET上使用某些流行的Java库,或者干脆在.NET上运行完整的Java程序不?那http://IKVM.NET是不二之选。

主流就说这么几个吧。其它支持Java SE的JVM还有好多,但是难说是主流。

要比惨的话,Apache Harmony里的DRLVM可能算是最惨之一。背后有IBM和Intel的强力支持,本来有望成为F/OSS系的主流JVM,奈何被Sun狠狠的捅了一刀,不让它跑JCK;使它不能名正言顺的使用Java™,更不能说自己是符合规范的JVM。然后Sun自己赶紧开源了自己的JDK,一直拖到硬把Harmony项目逼死了。

Java SE Embedded

这是Oracle造出来的比较新的概念。硬件发展得很快,现在很多所谓“嵌入式”场景用的机器其实跟普通台式机的配置没差多少,完全足以运行Java SE,侵蚀了以前高端Java ME(例如Java ME CDC Profile)的地盘。

Oracle Java SE Embedded里带的JVM自然还是HotSpot VM,不过是Java SE Embedded定制版:简化了JVM内的某些部件,尽可能在支持完整的Java SE功能的前提下向着减少内存消耗的方向优化;只留下了Client Compiler(C1)而去掉了Server Compiler(C2);GC以前好像是只留下了Serial GC但后来有没有支持更多GC种类。

IBM在这个领域照样可以用J9 VM应对。

其它还算主流Java SE Embedded JVM的话,可能JamVM可以算进来吧。它是一个小巧的、能支持完全OpenJDK类库和Java SE规范的JVM。

jvm 的组成

栈 Stack(线程独占)

Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

堆 Heap(线程共享)

java堆是和应用程序关系最为密切的内存空间,几乎所有的对象都存放在堆上。并且java堆是完全自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理,而不需要显示的释放。
根据java回收机制的不同,java堆有可能拥有不同的结构。最为常见的一种构成是将整个java堆分为新生代和老年代。其中新生代存放新生对象或者年龄不大的对象,老年代则存放老年对象。新生代有可能分为eden区、s0区、s1区,s0区和s1区也被称为from和to区,他们是两块大小相同、可以互换角色的内存空间。

方法区 Method Area(线程共享)

主要用于存储Class Data和Meta Data,简单地说,Java类加载器将Java类加载到JVM中后,类的信息会保存在方法区,比如类的静态成员,构造器,方法等。一般情况下,java.lang.OutOfMemoryError: PermGen space异常就是由于该区空间耗尽而引起的,方法区由多个线程共享。

程序计数器 Program Counter Register(线程独享)

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。

本地方法栈 Native Method Stacks(线程独享)

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

GC(垃圾回收器)

Java的内存分配与回收全部由JVM垃圾回收进程自动完成。与C语言不同,Java开发者不需要自己编写代码实现垃圾回收。这是Java深受大家欢迎的众多特性之一,能够帮助程序员更好地编写Java程序。

常见的java垃圾回收器

  • 串行垃圾回收器(Serial Garbage Collector)

串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。d通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。

  • 并行垃圾回收器(Parallel Garbage Collector)

并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,它也会冻结所有的应用程序线程当执行垃圾回收的时候

  • 并发标记扫描垃圾回收器(CMS Garbage Collector)

并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
1.当标记的引用对象在tenured区域;
2.在进行垃圾回收的时候,堆内存的数据被并发的改变。
相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。
通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。

  • G1垃圾回收器(G1 Garbage Collector)

G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域
通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器

Java 8 的新特性

在使用G1垃圾回收器的时候,通过 JVM参数 -XX:+UseStringDeduplication 。 我们可以通过删除重复的字符串,只保留一个char[]来优化堆内存。这个选择在Java 8 u 20被引入。

我们给出了全部的四种Java垃圾回收器,需要根据应用场景,硬件性能和吞吐量需求来决定使用哪一种。

垃圾回收的JVM配置

配置 描述
-XX:+UseSerialGC 串行垃圾回收器
-XX:+UseParallelGC 并行垃圾回收器
-XX:+UseConcMarkSweepGC 并发标记扫描垃圾回收器
-XX:ParallelCMSThreads 并发标记扫描垃圾回收器 =为使用的线程数量
-XX:+UseG1GC G1垃圾回收器

jvm常见参数调优

最大JVM可用内存

-Xmx1024m

最小JVM可用内存

-Xms2g

年轻代内存大小

-Xmn512m

设置新生代最小空间大小。

-XX:NewSize

设置新生代最大空间大小。

-XX:MaxNewSize

PermSize:永久代内存大小,该值太大会导致fullGC时间过长,太小将增加fullGC频率

-XX:PermSize=128m

-XX:MaxPermSize设置永久代最大空间大小。

线程栈大小,太大将导致JVM可建的线程数量减少,例:-Xss256k

-Xss256k

禁止手动fullGC,如果配置,则System.gc()将无效,比如在为DirectByteBuffer分配空间过程中发现直接内存不足时会显式调用System.gc()
-XX:+DisableExplicitGC

一般PermGen是不会被GC,如果希望PermGen永久代也能被GC,则需要配置该参数

-XX:+UseConcMarkSweepGC

GC进行时标记可回收对象时可以并行remark-XX:+UseCMSCompactAtFullCollection 表示在fullGC之后进行压缩,CMS默认不压缩空间

-XX:+CMSParallelRemarkEnabled

为java堆内存设置内存页大小

-XX:LargePageSizeInBytes=128m

对原始类型进行快速优化

-XX:+UseFastAccessorMethods

关闭预期开始的晋升率的统计

-XX:+UseCMSInitiatingOccupancyOnly

使用cms作为垃圾回收,并设置GC百分比,例(使用70%后开始CMS收集)

-XX:CMSInitiatingOccupancyFraction=70

打印GC的详细信息

-XX:+PrintGintGCDateStamp

指定GC文件路径

-Xloggc