JVM必知必会

0.官方文档

JDK 版本更新较快,很多概念必须从官网才能找到准确描述。
下面是JavaSE8的JVM文档:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5

1. JAVA内存模型 和 JVM运行时数据区 区别

JAVA内存模型和JVM运行时数据区不仅仅是字面上的不同,这两个根本就不是一回事。java最大的一个特点就是一次编译到处使用,这是因为不同的虚拟机厂商根据《JAVA虚拟机规范》为不同的系统生产对应的java虚拟机.《JAVA虚拟机规范》是用来描述Java虚拟机这个模型是什么样的, 虚拟机厂商生产出来的虚拟机必须要遵循《JAVA虚拟机规范》。
不仅仅java语言可以在虚拟机上运行,JRuby、Scala、Groovy都可以在java虚拟机上运行,那么java虚拟机是根据什么来区分到底是哪种语言呢?这就是另一个规范,叫做语言规范,每种语言都有自己的语言规范,比如Java语言规范。语言规范就是用来描述该语言的特性。
从上面我们可以知道《JAVA虚拟机规范》和《JAVA语言规范》是不同的两个东西,而JVM运行时数据区是《JAVA虚拟机规范》提出来的,而《JAVA语言规范》里面是对JAVA内存模型的描述。

总结:JVM运行时数据区是一个规范,JAVA内存模型是一个实现。

2. JAVA内存模型(Java Memory Model,简称 JMM )

Java内存模型定义了线程和主内存之间的抽象关系,即定义了 JVM 在计算机内存(RAM)中的工作方式。
由于CPU 执行速度很快,而从内存读取数据和向内存写入数据的过程跟 CPU 执行指令的速度比起来要差几个数量级,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。为解决该矛盾,人们便在CPU和内存之间增加了高速缓存cache,如下图所示:

JVM必知必会
(1)将运算需要的数据从主存复制一份到CPU的高速缓存当中
(2)CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据
(3)运算结束之后,再将高速缓存中的数据刷新到主存当中。

事实上,CPU上可能同时开多个线程,多个线程对同一份数据进行访问,很容易出现数据访问不一致的问题,JMM中使用volatile关键字保证主内存和线程工作内存的数据一致性。

Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这里的工作内存是 JMM 的一个抽象概念,也叫本地内存,其存储了该线程以读 / 写共享变量的副本。就像每个处理器内核拥有私有的高速缓存,JMM 中每个线程拥有私有的本地内存。
不同线程之间无法直接访问对方工作内存中的变量,线程间的通信一般有两种方式进行,一是通过消息传递,二是共享内存。Java 线程间的通信采用的是共享内存方式,线程、主内存和工作内存的交互关系如下图所示:
JVM必知必会

3. JVM运行时数据区

JVM必知必会
运行时数据区主要包括5大部分:
1) The pc Register
2) Java Virtual Machine Stacks
3) Heap
4) Method Area / Metaspace
5) Native Method Stacks

其中线程独享的有:虚拟机栈、本地方法栈、程序计数器。
线程共享的有:堆、方法区。

下面简单介绍下各自的功能:
程序计数器
每个线程都有自己的程序计数器。占用一小块的内存,记录当前线程目前执行到你的代码所对应的哪一条字节码指令。

虚拟机栈
是一个先进后出的数据结构。存放方法里面的一些局部变量。
当一个线程被创建时,虚拟机栈也随之被创建,该区域为线程私有,无法被其他线程使用,因此不存在线程安全问题在该线程中,每执行一个方法,就会生成一个栈(Stack Frame),并被压入虚拟机栈,当该方法执行完毕之后,该栈帧会被弹出,也就是说方法调用和方法返回的过程对应着栈帧入站和出栈的操作。
可能抛出的异常:
*Error:没出口的递归调用,会抛出该异常。

本地方法栈
native修饰的方法,是调用本地操作系统里面的方法。
可能会抛出的异常:*Error/OutOfMemoryError

方法区
JDK的新版本中,也叫 Metaspace。
存放常量池, 字段、方法、构造器和 class文件加载进来放这里。
运行时常量池(Runtime Constant Pool)是方法区的一部分。
可能会抛出的异常:OutOfMemoryError


存放创建对象。
可能会抛出的异常:OutOfMemoryError

还有一个区域比较特殊,堆外内存,不属于JVM。如Spark的钨丝计划大量使用。

4. JVM可能抛出的异常

虚拟机栈:
java.lang.*Error
//没有出口的递归调用

堆:
java.lang.OutOfMemoryError: Java heap space
//new的对象过多
java.lang.OutOfMemoryError: GC overhead limit exceeded
//GC不过来,一般是代码问题

方法区:
java.lang.OutOfMemoryError: Metaspace
//类的相关信息太多

堆外:
java.lang.OutOfMemoryError: Direct buffer memory
//用户分配的内存超过了能用的内存。

java.lang.OutOfMemoryError: unable to create new native thread
//OS不能创建新的线程了

以上只列出了部分异常,其中OOM的异常种类较多,注意分辨以便排查问题。

5. JVM参数

JVM参数分为三类:

  1. 标准参数
    各JDK版本很少变化。如:java -version
  2. X参数
    变化也很小。
    如:java -Xint -version
    java -Xcomp -version
  3. XX参数
    1)Boolean型
    公式:-XX:[+/-]
    如:> jinfo -flag PrintGCDetails 1271
    可在IDE的VM option中加入:-XX:-PrintGCDetails
    2)非Boolean型
    公式:-XX:<key>=<value>
    如:> jinfo -flag MetaspaceSize 1271
    可在IDE的VM option中加入:-XX:MetaspaceSize=128m
    > jinfo -flag MaxTenuringThreshold 1271
    MaxTenuringThreshold表示需要GC对象的年龄,范围是0-15

常用参数:
-Xms heap初始化值 等价于 -XX:InitialHeapSize
-Xmx heap的最大值 等价于 -XX:MaxHeapSize
-Xss 等价于 -XX:ThreadStackSize 单位kb

-XX:+PrintFlagsInitial 参看初始默认值
-XX:+PrintFlagsFinal 参看修改后的
-XX:+PrintCommandLineFlags 打印命令行参数

查看JVM各项参数默认值:java -XX:+PrintFlagsFinal -version >> flags.txt
其中 := 表示修改过的

查看指定进程号的指定参数的参数值: jinfo -flag MaxHeapSize 10254

allocate 操作的是JVM
allocateDirect 直接操作OS,不属于GC管理