Skip to content

JVM介绍

write once,run everywhere。一次编译到处运行

一、JVM内存模型的概述

Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

简要言之,jmm是jvm的一种规范,定义了jvm的内存模型。它屏蔽了各种硬件和操作系统的访问差异,不像c那样直接访问硬件内存,相对安全很多,它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。

二、jvm内存区域-运行时数据区

JVM定义不同运行时数据区,他们是用来执应用程序的。某些区域随着JVM启动及销毁,另外一些区域的数据是线程性独立的,随着线程创建和销毁。

主要包含五个内容,以下是数据区分类:

  • 线程共享数据区:方法区(Method Area)、堆(Heap)

  • 线程隔离数据区:JVM虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、

    程序计数器(Program Counter Register)

运行时的数据区

JDK8及以后:

在这里插入图片描述

2.1 程序计数器(Program Counter Register)

为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不不影响,独立存储,我们称这类内存区域为“线程私有”的内存

  • 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器
  • 线程是一个独立的执行单元,是由CPU控制执行的
  • 字节码解释器⼯工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成

特点:内存区域中唯一个没有规定任何 OutOfMemoryError 情况的区域

2.2 JVM虚拟机栈(VM Stack)

每个方法在执行的同时都会创建一个栈帧(Stack Framel)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程

特点:局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)以及对象引用(reference 类型) 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常

2.3 本地方法栈(Native Method Stack)

与Java虚拟机栈相同,每个方法在执行的同时都会创建一个栈帧(Stack Framel)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

本地方法栈和虚拟机栈最大的区别,本地方法栈主要是存储native修饰的方法。而虚拟机栈主要是存储除native以外的方法,即我们常规使用的方法。

特点:Hotshot将Java虚拟机栈和本地方法栈合二为一(编译时分开,运行时结合)

2.4 堆(Heap)

java堆是Java内存区域中一块用来存放对象实例例的区域,【几乎所有的对象实例例都在这⾥分配内存】,此内存区域的唯一目的就是存放对象实例 Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块 Java 堆是被所有线程共享的一块内存区域

特点:Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(Garbage)

Java堆可以分成新生代和老年代 新生代可分为To Space、From Space、Eden

2.5 方法区(Method Area)/元空间(Metaspace)

方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。内存中存放类信息、静态变量等数据,属于线程共享的一块区域。

类信息包括:类版本号、方法、接口

Hotspot使用永久代来实现方法区 JRockit、IBM J9VM Java堆一样管理这部分内存。

特点:并非数据进入了方法区就如永久代的名字一样“永久”存在了。

注意:

JDK1.8hotspot永久代被元空间(Metaspace)取代, 字符串常量池位置还在堆中, 运行时常量池位置变成了元空间(Metaspace)

2.5.1 常量池

  • 类文件常量池:又称为静态常量池,存储区域在堆中,编译时产生对应的class文件,主要包含字面量和符号引用;
  • 运行时常量池:存在元数据(Meta Space)空间,JVM运行时,在类加载完成后,将每个class常量池中的符号引用转换为直接引用
  • 字符串常量池:存在堆内存中,类在加载、验证、准备完成后在堆中生成字符串对象实例,然后将该字符串对象实例的引用只存储到String Pool中

运行时常量池是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

对常量池的回收和对类型的卸载方法区也会抛出OutofMemoryError,当它无法满足内存分配需求时。运行时常量池是方法区的一部分,受到方法区内存的限制,当常量池再申请到内存时会抛出OutOfMemoryError异常

三、堆内存分配

对象分配的规则有哪些

  • 对象主要分配在新生代的 Eden 区上
  • 如果启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配
  • 少数情况下也可能会直接分配在老年代中,如大对象,即对象的大小超过了jvm设置新生代的阈值就会分配至老年代

GC参数指定垃圾回收

  • -Xms20M、-Xmx20M、-Xmn10M 这 3 个参数限制了 Java 堆大小为 20 MB,不可扩展,其中 10 MB 分配给新生代,剩下的 10 MB 分配给老年代。-Xx: SurvivorRatio= 8 决定了新生代中 Eden 区与两个 Survivor 区的空间比例是 8:1

    -verbose:gc -XX:+PrintGCDetails 表示开启GC日志打印

  • 新生代与老年代

    新生代 GC (Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度老年代 GC (Major GC/ Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。Major GC 的速度一般会比 Minor GC 慢 10 倍以上。

3.1 分配规则讲解

逃逸分析

逃逸分析的基本行为就是分析对象动态作用域

  • 方法逃逸:当一个对象在方法中被定义后,它可能被外部方法所引用,称为方法逃逸。
  • 线程逃逸:如果还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸

栈上分配

栈上分配就是把方法中的变量和对象分配到栈上,方法执行完后自动销毁,而不需要垃圾回收的介入,从而提高系统性能

-XX:+DoEscapeAnalysis 开启逃逸分析(jdk1.8默认开启)
-XX:-DoEscapeAnalysis 关闭逃逸分析

四、java的内存交互

java内存交互的流程间的操作如下:

  1. lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
  2. read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  3. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  4. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  5. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
  6. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
  7. write(写入):作用于主内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
  8. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

上面8中操作必须满足以下规则

  1. 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
  2. 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  3. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存。
  4. 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
  5. 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
  6. 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  7. 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
  8. 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

五、指令重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

**数据依赖性:**编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。(仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。)

**最终结果:**不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变