一、前言
java内存模型是java重要的知识,可以分析解决在生产环境中所遇到的各种“棘手”的问题。
- jvm内存模型:class文件在java进程中内存分布的情况。
- 运行时数据区(jvm组成):一个class文件,在jvm中运行时的数据存储以及数据状态,是一个动态的过程。
二、JVM组成
- 类加载器(classLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Navite Interface)
各个组成部分的用途:
程序在运行之前会把.java文件转成.class的字节码文件,jvm首先通过类加载器(classLoader)把字节码文件加载到内存——运行时数据区(Runtime Data Area),而字节码文件是jvm的一套指令集规范,底层系统并不能识别,需要调用执行引擎(Execution Engine),把字节码文件翻译成底层系统可以识别的语言,这个过程需要使用本地库接口(Navite Interface),本地库接口主要是c语言。
三、运行时数据区组成
- 方法区(Method Area)
- java堆(java Heap)
- java虚拟机栈(Java Virtual Mechine Stacks)
- 本地方法栈(Navite Method Stacks)
- 程序计数器(Program Counter Register)
内存问题都是在运行时数据区。
如上图所示:
- 方法区和堆是数据区,是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以后移到了堆中。
见下图,注意静态常量和字符串常量池所在的区域变化:
3.1.2 jdk1.8模拟方法区内存不足异常
模拟过程如上图:
- 调小元数据空间参数: -XX MetaspaceSize=30M -XX MaxMetaspaceSize 30M
- 运行程序,报错:OutOfMemoryError:Metaspace
3.2 java堆
保存了对象的信息,该内存牵扯到释放问题(GC);
生命周期也与jvm一致。
堆介绍:
- 堆的生命周期与 JVM 进程一致
- 堆是 Java 虚拟机运行时数据区共享数据区最大的区域
- “几乎”所有的对象和数组都在堆中进行分配
- 堆是 JVM GC 工作重点区域
- 堆内存不足时,将抛出 OutofMemoryError
特性:线程共享
异常规定:OutOfMemoryError
如果在堆中没有内存完成实例分配,并且堆不可以再扩展时,将会抛出OutOfMemoryError。
Java虚拟机规范规定,Java堆可以处在物理上不连续的内存空间中,只要逻辑上连续即可,就像我们的磁盘空间一样。在实现上也可以是固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是可扩展的,通过-Xmx和-Xms控制。
3.2.1 模拟堆内存溢出
模拟堆内存溢出过程:
- 调小堆内存参数:-Xms30M -Xmx30M
- 启动程序,报错OutOfMemoryError:GC overhead limit exceeded
这个错误是由于
JVM
花费太长时间执行GC
且只能回收很少的堆内存时抛出的。根据Oracle
官方文档,默认情况下,如果Java
进程花费98%
以上的时间执行GC
,并且每次只有不到2%
的堆被恢复,则JVM
抛出此错误。换句话说,这意味着我们的应用程序几乎耗尽了所有可用内存,垃圾收集器花了太长时间试图清理它,并多次失败。
扩展链接:OutOfMemoryError系列(2): GC overhead limit exceeded
3.3 java虚拟机栈
线程的私有空间,在每一次进行方法调用的时候都会存在有栈帧,采用先进后出的设计原则;
- 本地变量表:局部变量或形参——volatile关键字问题;
- 操作数栈:执行所有的方法计算操作;
- 常量池引用:String类实例、Integer类示例
- 返回地址:方法执行完毕后的恢复执行点;
特性:线程私有
栈介绍:
- 虚拟机栈的生命周期与执行的线程一致
- 虚拟机栈是当前执行线程独占空间.以栈的数据结构形式存在
- 虚拟机栈是线程运算执行的区域,它保存着一个线程调用方法的顺序和过程
- 被线程执行的方法都是以栈帧( frame )的结构压入虚拟机栈
- 当虚拟机栈的空间不够使用时,将报出 stackoverflowExecption
3.3.1 java虚拟机栈运行示意图
由上图,结合我们的程序运行顺序:main()——>calc()——>xxx()——>hashCode()——>xxx()——>calc()——>main()
所以先进后出,符合程序运行过程
异常规定:StackOverflowError、OutOfMemoryError
- 如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出StackOverflowError异常。
- 如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出OutOfMemoryError异常。
3.3.2 Stacks-方法的调用过程
3.3.3 模拟方法深度太深,导致栈溢出
示例:方法中调用其他方法深度太深,会导致StackOverflowError异常。
性能优化的点,递归或for循环深度太深可能会导致栈溢出。优化的点:调整jvm栈的大小
解决方法:
- 一般从代码层面解决,某个方法调用深度太深,看看能否避免
- 调整单个栈大小参数:-Xss30M -Xsx30M
3.4 本地方法栈
与java虚拟机栈功能类似,区别在于是为本地方法服务的;
特性:同虚拟机栈。
3.5 程序计数器
执行指令的一个顺序编码,该区域所占比率几乎可以忽略;
作用:记录当前线程执行的字节码行号,当线程再次抢到执行权后可以在上次的行号上继续执行。
程序计数器介绍:
一个很小内存区域用于保存当前线程所执行的字节码的行号(内存地址)
特点:
- 每一个线程都有自己私有的程序计数器
- 唯一一个没有 OOM 的区域 ·
- 生命周期与线程一致生命周期随着线程.线程启动而产生,线程结束而消亡
四、堆内存模型
jdk1.8及以后堆内存模型:
jdk1.8以前的堆内存模型:
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流程图
五 、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参数配置
六、垃圾回收算法(后续会另写一篇专门讲)
【年轻代】串行GC
【年轻代】并行回收GC
【年轻代】并行GC
【老年代】串行GC
【老年代】并行GC
【老年代】CMS:STW(Stop-The-World)设计问题,暂时挂起所有的程序的执行线程,进行无用的对象标记。
没有任何一项合适的Gc回收操作。从JDK 1.8开始提供有G1收集器,在JDK 11之后提供有了ZGC。
-XX:+UseG1GC JDK 11之后默认就是G1回收器,对于其他的回收算法实际上就可以忽略掉了。
如果让你设计垃圾回收器,你会关注哪些指标?
- 垃圾收集器的耗时-低耗时
- 吞吐量-高
- 垃圾回收的次数-少