一、前言

java内存模型是java重要的知识,可以分析解决在生产环境中所遇到的各种“棘手”的问题。

  • jvm内存模型:class文件在java进程中内存分布的情况。
  • 运行时数据区(jvm组成):一个class文件,在jvm中运行时的数据存储以及数据状态,是一个动态的过程。

二、JVM组成

java jmm模型 java模型类_方法区

  1. 类加载器(classLoader)
  2. 运行时数据区(Runtime Data Area)
  3. 执行引擎(Execution Engine)
  4. 本地库接口(Navite Interface)

各个组成部分的用途:

程序在运行之前会把.java文件转成.class的字节码文件,jvm首先通过类加载器(classLoader)把字节码文件加载到内存——运行时数据区(Runtime Data Area),而字节码文件是jvm的一套指令集规范,底层系统并不能识别,需要调用执行引擎(Execution Engine),把字节码文件翻译成底层系统可以识别的语言,这个过程需要使用本地库接口(Navite Interface),本地库接口主要是c语言。

三、运行时数据区组成

  1. 方法区(Method Area)
  2. java堆(java Heap)
  3. java虚拟机栈(Java Virtual Mechine Stacks)
  4. 本地方法栈(Navite Method Stacks)
  5. 程序计数器(Program Counter Register)

内存问题都是在运行时数据区。

java jmm模型 java模型类_JVM_02

如上图所示:

  • 方法区和堆是数据区,是jvm共享的。其中方法区主要是存储静态数据(类信息,静态常量等),堆存储动态数据(对象)。
  • java虚拟机栈、本地方法栈、程序计数器,是运算区,是线程私有的。

3.1 方法区

最重要的内存区域,保存了类的信息(名称、成员、接口、父类)、静态常量、变量,域信息(字段,类型等)、方法信息和即时编译器(JIT)编译后的代码,反射机制是重要的组成部分,动态进行类操作的实现;

方法区介绍:

  • 方法区的生命周期与 JVM 进程一致
  • 存储已被虚拟机加载的类型信息,方法信息,域信息,运行时常量,静态变量(不同版本不一样),即时编译器( JIT )编译后的代码
  • 运行时常量池属于 Method Area 中的一部分方法区
  • 从逻辑上来理解其本身也属于 H eap 的一部分。但是为了区分和更好的内存对象的垃圾回收,我们将 Method Area 又称之为 Non 一 Heap 将之与 Heap 进行区分理解
     ( JDKS 之前的 Method Area 实现叫 Perm Space . JDKS 及之后的 Method Area 实现叫 Meta Space ) 
  • 方法区内存不足时,将抛出 outofMemoryError

特性:线程共享

异常规定:OutOfMemoryError
当方法无法满足内存分配需求时会抛出OutOfMemoryError异常。

误区:方法区不等于永生代
很多人原因把方法区称作“永久代”(Permanent Generation),本质上两者并不等价,只是HotSpot虚拟机垃圾回收器团队把GC分代收集扩展到了方法区,或者说是用来永久代来实现方法区而已,这样能省去专门为方法区编写内存管理的代码,但是在Jdk8也移除了“永久代”,使用Native Memory来实现方法区。

  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
  • 移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代
  • 移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是Native Heap。譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap

3.1.1 静态常量和字符串常量池是否总是在方法区

答案是否,jdk1.6中静态常量和字符串常量池是在方法区,jdk1.7以后移到了堆中。
见下图,注意静态常量和字符串常量池所在的区域变化:

java jmm模型 java模型类_java jmm模型_03

java jmm模型 java模型类_JVM_04

java jmm模型 java模型类_java jmm模型_05

3.1.2 jdk1.8模拟方法区内存不足异常 

java jmm模型 java模型类_java_06

java jmm模型 java模型类_java_07

java jmm模型 java模型类_方法区_08

模拟过程如上图: 

  1. 调小元数据空间参数: -XX MetaspaceSize=30M -XX MaxMetaspaceSize 30M
  2. 运行程序,报错:OutOfMemoryError:Metaspace

3.2 java堆 

保存了对象的信息,该内存牵扯到释放问题(GC);
生命周期也与jvm一致。

堆介绍:

  • 堆的生命周期与 JVM 进程一致
  • 堆是 Java 虚拟机运行时数据区共享数据区最大的区域
  • “几乎”所有的对象和数组都在堆中进行分配
  • 堆是 JVM GC 工作重点区域
  • 堆内存不足时,将抛出 OutofMemoryError

特性:线程共享

异常规定:OutOfMemoryError

如果在堆中没有内存完成实例分配,并且堆不可以再扩展时,将会抛出OutOfMemoryError。

Java虚拟机规范规定,Java堆可以处在物理上不连续的内存空间中,只要逻辑上连续即可,就像我们的磁盘空间一样。在实现上也可以是固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是可扩展的,通过-Xmx和-Xms控制。

3.2.1 模拟堆内存溢出

java jmm模型 java模型类_java jmm模型_09

java jmm模型 java模型类_java jmm模型_10

 模拟堆内存溢出过程:

  1. 调小堆内存参数:-Xms30M -Xmx30M
  2. 启动程序,报错OutOfMemoryError:GC overhead limit exceeded

 这个错误是由于JVM花费太长时间执行GC且只能回收很少的堆内存时抛出的。根据Oracle官方文档,默认情况下,如果Java进程花费98%以上的时间执行GC,并且每次只有不到2%的堆被恢复,则JVM抛出此错误。换句话说,这意味着我们的应用程序几乎耗尽了所有可用内存,垃圾收集器花了太长时间试图清理它,并多次失败。

3.3 java虚拟机栈

线程的私有空间,在每一次进行方法调用的时候都会存在有栈帧,采用先进后出的设计原则;

  • 本地变量表:局部变量或形参——volatile关键字问题;
  • 操作数栈:执行所有的方法计算操作;
  • 常量池引用:String类实例、Integer类示例
  • 返回地址:方法执行完毕后的恢复执行点;

特性:线程私有

栈介绍:

  • 虚拟机栈的生命周期与执行的线程一致
  • 虚拟机栈是当前执行线程独占空间.以栈的数据结构形式存在 
  • 虚拟机栈是线程运算执行的区域,它保存着一个线程调用方法的顺序和过程 
  • 被线程执行的方法都是以栈帧( frame )的结构压入虚拟机栈
  • 当虚拟机栈的空间不够使用时,将报出 stackoverflowExecption

3.3.1 java虚拟机栈运行示意图

java jmm模型 java模型类_java jmm模型_11

由上图,结合我们的程序运行顺序:main()——>calc()——>xxx()——>hashCode()——>xxx()——>calc()——>main()

所以先进后出,符合程序运行过程 

异常规定:StackOverflowError、OutOfMemoryError

  1. 如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出StackOverflowError异常。
  2. 如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出OutOfMemoryError异常。

3.3.2 Stacks-方法的调用过程

java jmm模型 java模型类_jvm_12

3.3.3 模拟方法深度太深,导致栈溢出

示例:方法中调用其他方法深度太深,会导致StackOverflowError异常。

java jmm模型 java模型类_方法区_13

 性能优化的点,递归或for循环深度太深可能会导致栈溢出。优化的点:调整jvm栈的大小

解决方法:

  1. 一般从代码层面解决,某个方法调用深度太深,看看能否避免
  2. 调整单个栈大小参数:-Xss30M -Xsx30M

3.4 本地方法栈

与java虚拟机栈功能类似,区别在于是为本地方法服务的;

特性:同虚拟机栈。

3.5 程序计数器

执行指令的一个顺序编码,该区域所占比率几乎可以忽略;

作用:记录当前线程执行的字节码行号,当线程再次抢到执行权后可以在上次的行号上继续执行。

程序计数器介绍:

一个很小内存区域用于保存当前线程所执行的字节码的行号(内存地址)

特点:

  • 每一个线程都有自己私有的程序计数器
  • 唯一一个没有 OOM 的区域 · 
  • 生命周期与线程一致生命周期随着线程.线程启动而产生,线程结束而消亡

四、堆内存模型

jdk1.8及以后堆内存模型:

java jmm模型 java模型类_java_14

jdk1.8以前的堆内存模型:

java jmm模型 java模型类_JVM_15

4.1 年轻代

组成:一个eden(伊甸区)区,两个servivor(s区)区,一个virtrual(伸缩区);

年轻代使用复制算法:

1.新的对象都会在eden区开辟,当eden区内存空间不足,会进行GC,在GC开始的时候,对象只存在Eden区和名为"Form"区的Servivor区,名为"To"的Servivor区是空的;

2.进行GC,Eden区的还存活对象全部复制到“To”,"Form"的还存活对象会根据年龄阈值(可以通过-XX:MaxTenuringThreshold来设置)来决定去向,年龄达到一定值,移到年老代,没有达到阈值,复制到"To"区;

3.这次GC后,Eden区和"Form"区已经清空了,"Form"和"To"会交换角色。不管怎么样,"To"区一定是空的;

4.GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

另:如果新创建的对象的空间占用过大将被直接保存到老年代之中。

4.2 年老代

年老代主要保存时间周期长的对象,对象如果使用了application级别的缓存,缓存中的对象也会被移到年老代。

老年代回收算法:

“标记-清除”算法:先进行对象的第一次标记,在这段时间之内会暂停程序的执行(如果标记的时间长或者对象的内容过多),这个暂停的时间就会长; 就会产生串行标记、并行标记使用问题;

“标记-压缩”算法:基于“标记-清除”算法,将零散的内存空间进行整理重新集合再分配;

4.3 Perm永久代

上面说了,方法区不能等同于永久代。但是也说了1.7及1.7以前是用永久代来实现方法区的,主要存class,method,filed对象。

这部分空间一般不会溢出,除非一次性加载了很多类。不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,原因可能是重新部署后,类的class没有卸载掉,这样就造成了大量的class对象保存在了Perm,一般重启服务就可以解决。

在jdk1.8中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与1.7的永久代最大的区别所在
 

4.4 Virtual伸缩区

最大内存和初始内存的差值,就是Virtual区。

4.5 为什么移除永久代

官网给出了解释:http://openjdk.java.net/jeps/122 

This is part of the JRockit and Hotspot convergence effort. JRockitcustomers do not need to configure the permanent generation (since JRockitdoes not have a permanent generation) and are accustomed to not configuring the permanent generation.

移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。现实使用中,由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen。

基于此,将永久区废弃,而改用元空间,改为了使用本地内存空间

4.6 gc流程图

java jmm模型 java模型类_jvm_16

五 、jvm的允许参数以及参数设置。

5.1、jvm允许参数

 a.标准参数

        -help,-version

    b.-X参数(非标准参数)

        -Xint,-Xcomp

    c.-XX参数(非标准参数,使用率较高)

2、-Xms与Xmx:设置jvm的初始值和最大值

    -Xms等价于-XX:InitialHeapSize

    -Xmx等价于-XX:MaxHeapSize

3、jps和jinfo

    jps:查看进程

    jinfo:查看jvm参数

        例如: 查看pid为1001的进程jvm参数,在命令行敲:jinfo -flags 1001

4.jmap

jmap -head PID

5.简单的调优

  • Tomcat调优:JAVA_OPTS="-Xms4096m -Xmx4096m -Xss1024K -XX:+UseG1GC” 路径:tomcat/bin/catalina.sh
  • Spring可以通过系统的环境参数配置实现调优。

5.2 如何打印jvm参数配置

java jmm模型 java模型类_JVM_17

六、垃圾回收算法(后续会另写一篇专门讲)

【年轻代】串行GC
【年轻代】并行回收GC
【年轻代】并行GC
【老年代】串行GC
【老年代】并行GC
【老年代】CMS:STW(Stop-The-World)设计问题,暂时挂起所有的程序的执行线程,进行无用的对象标记。

没有任何一项合适的Gc回收操作。从JDK 1.8开始提供有G1收集器,在JDK 11之后提供有了ZGC。
-XX:+UseG1GC JDK 11之后默认就是G1回收器,对于其他的回收算法实际上就可以忽略掉了。

如果让你设计垃圾回收器,你会关注哪些指标?

  1. 垃圾收集器的耗时-低耗时
  2. 吞吐量-高
  3. 垃圾回收的次数-少

 七、扩展

java jmm模型 java模型类_jvm_18

八、 大局观

java jmm模型 java模型类_JVM_19