JVM运行时数据区与内存模型
概述
Java虚拟机使Java成为了一种跨平台的语言,Java不直接与操作系统接触,而是通过虚拟机这个中间桥梁,通过JVM与底层接触。不同的系统有不同的JVM,但是所有的这些JVM都完美的支持Java语法,这就使得Write Once,Run EveryWhere成为可能。
除此之外,JVM的内存管理机制使得不需要再为每一个new操作去delete/free代码,由机器代替程序员这样就不容易出现内存泄露和内存溢出的问题了,但是一旦出现了这种问题如果不了解JVM是怎样使用内存的,那么排查错误将会非常困难。
JVM运行时数据区域
JVM分为几个区域,如下图。有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户进程的启动和结束而创建和销毁。
程序计数器
由于Java多线程是通过线程轮流切换完成的,一个线程没有执行完时就需要一个东西记录它执行到哪了,下次抢占到了CPU资源时再从这开始,这个东西就是程序计数器,正是因为这样,所以它也是“线程私有”的内存。
如果一个线程执行一个main方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器的值则为空,此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
解释:【指向当前线程所执行的字节码的行号】。字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支,循环,跳转,异常处理,线程回复等都需要依赖这个计数器来完成。
Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(一个栈帧意味着一个方法的开始和结束)用于存储局部变量表,对象引用,操作数栈,动态链接,方法出口等信息,下图为栈桢结构图:
通常所说的JVM里的堆和栈里这个栈就是Java虚拟机栈,或者说是Java虚拟机栈中的局部变量表部分。
局部变量表存放了编译期可知的各种基本数据类型,对象引用(仅限局部变量的,不包含成员变量的)。其中每个局部变量空间(Slot)有32位,所以long和double类型的数据会占用两个局部变量空间,其他类型包括对象引用占用一个。对象引用调用的是存在堆中的对象,这个引用可以是对象的起始地址或者是指向对象的句柄(在对象访问定位会介绍)。局部变量表所需的内存在编译期就已经确定了也就是进入这个方法时就已经确定了,运行期间不会更改。
操作数栈则存储方法内一些进行了运算操作后的结果
动态链接,在方法内调用接口,通过字面量链接到具体的实现类,实现Java的动态特性。
方法出口(返回地址),return或者发生Exception等。
如果方法methodOne方法调用了methodTwo,那么methodOne就会先入栈创建一个栈桢,接着methodTwo再入栈成为栈顶(假设没有其他的方法执行),methodTwo执行完先出栈,接着methodOne执行完出栈。
在使用递归的情况下,如果线程请求的栈的深度超过虚拟机所允许栈的深度就会抛出StackOverflowError;但是大部分虚拟机栈的深度都可以动态扩展,HotSpot中使用XSS可以设置栈的深度,如果扩展时无法请求到足够的内存就会抛出OutOfMemoryError。
本地方法栈
本地方法栈和虚拟机栈相似,区别就是虚拟机为虚拟机栈执行Java服务(字节码服务),而本地方法栈为虚拟机使用到的Native方法服务。本地方法栈中使用的语言,使用方式,数据结构没有强制要求。
Java堆
堆是JVM里最大的一块内存区域,被所有线程共享,在虚拟机启动时创建,此区域的目的就是存放对象实例和数组,几乎所有的对象实例都在这分配(随着JIT的发展已经不是那么绝对了)。Java堆是垃圾收集管理的主要区域,由于现在收集器基本都采用分代收集方法,所以Java堆中还可以分为新生代,老年代,永久代。1.8之后取消了永久代;其中新生代又划分为Eden空间,From Survivor空间,To Survivor空间。无论怎么划分都是为了更好的回收,分配,利用内存。下图为1.8后的内存模型
根据Java虚拟机规范,Java堆可以处于物理不连续的空间中,只要逻辑连续即可。在实现时,既可以实现成固定大小的也可以是可扩展的(通过-Xmx和-Xms控制),如果堆中没有足够的内存完成实例分配,并且堆也无法得到扩展时,将会抛出OutOfMemoryError异常。
常用参数:
- -Xmx 允许的最大堆内存
- -Xms 初始化时的堆内存
方法区(Metaspace)
方法区也是一个线程共享的区域,存储已被虚拟机加载的类信息,常量(final),静态变量(static),JIT(即时编译器)编译后的代码等数据。虚拟机对方法区规范非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小意外,还可以选择不实现垃圾回收。垃圾回收行为在这个区域比较少见但还是有必要的,主要是针对常量池回收和类型的卸载。Java虚拟机规范把方法区描述为堆的一个逻辑部分,其实堆和方法区可以看成数据部分;Java虚拟机栈和程序计数器可以看成指令部分;方法区存储一些不会变更的数据,之前HotSpot上使用GC分代收集管理方法区,所以方法区也被称为永久代,但是在Java8中已经被废除,引入了元空间,使用的是本地内存,而不是堆内存的一部分,所以元空间不受堆内存的影响,受本地内存的影响。可以通过以下参数进行控制:
- -XX:MetaspaceSize=N 初始化Metaspace大小(如果不设置,随着GC的到来,虚拟机会根据实际情况调控Metaspace的大小)
- -XX:MaxMetaspaceSize=N 允许Metaspace增长到的最大值
- -XX:MinMetaspaceFreeRatio=N 进行GC之后会计算当前Metaspace的空闲空间比(默认为40,即40%),当空闲空间小于这个值则增长Metaspace空间,它的值越大说明Metaspace增长的块,反之则慢
- -XX:MaxMetasaceFreeRatio=N 进行GC之后会计算当前Metaspace的空闲空间比(默认为70,即70%),当空闲空间大于这个值则释放Metaspace空间
- -XX:MaxMetaspaceExpansion=N 每次触发空间增长的最大增长幅度,默认为5MB
- -XX:MinMetaspaceExpansion=N 每次触发空间增长的最小增长幅度,默认为330KB
运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,运行时常量池相对于Class常量池另外一个特性就是具备动态性,运行期间可能将新的常量放入池中
类加载机制
加载
- 通过类的全限定名来获取其定义的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成一个代表这个类的class对象作为对方法区这些数据的访问入口
验证
确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全
准备
准备阶段是正式为类变量(即静态变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。注意final修饰的变量必须设置初始值,否则编译不通过,jvm不会为它设置初始值
解析
将常量池中的符号引用转化为直接引用的过程
初始化
使用
卸载
OutOfMemoryError异常
Java堆溢出
产生原因
- 在堆中创建的对象过多,导致对象占用的内存达到了堆内存的最大值,此称为内存溢出。
- 在堆中存在大量的“垃圾对象”(这类对象是否存活对程序运行没有任何影响),但是这类垃圾对象无法被垃圾回收线程给回收,此称为内存泄漏。
解决办法
- 可以通过内存分析映射分析工具对dump(转储)出来的堆转储快照进行分析,重点是确认内存中的对象是否必要,也就是首先分析清楚到底是内存溢出还是泄漏。通过工具查看是否存在泄漏对象,如果存在查看它到GC Root的引用链。通过这样就能查看到泄漏对象是通过怎样的路径与GC Root相关联,导致垃圾对象无法被垃圾回收器回收,从而定位到相关代码以解决问题。
- 如果不存在内存泄漏,那就是内存溢出,那就可以通过调大虚拟机最大分配内存,同时可以从代码上查看是否存在某些对象生命周期过长、持有状态时间过长等情况以减少程序在运行期间的内存消耗。
虚拟机栈和本地方法栈溢出
产生原因
- 在单个线程时,当栈帧太大(即方法调用次数太多,达到栈帧的最大深度)或者虚拟机栈容量太小,当内存无法分配时,都将抛出StackOverflowError异常
- 在多个线程时,如果线程过多导致或者每个线程的虚拟机栈分配的内存过大都可能导致内存溢出的情况。因为操作系统给每个进程分配的内存是有最大值的(一个进程下存在1-n个线程,进程内存=堆内存+方法区内存+java虚拟机内存+本地方法栈内存,忽略程序计数器内存和虚拟机进程等所消耗的内存),所以说当线程数越多和线程分配的栈内存越大则越容易出现内存溢出的情况。
解决办法
- 单个线程时通过增大每个虚拟机栈的最大容量或者改进代码实现逻辑
- 多个线程时在不能减少线程数量时可通过减少每个线程分配的栈内存、减少对堆内存的分配数额或者更换64位的操作系统来解决内存溢出问题。
故障排查参数
- java命令导出内存溢出(HeapDumpOnOutOfMemoryError)的堆信息(hprof文件):
-XX:+HeapDumpOnOutOfMemoryError
- -XX:+PrintGC 输出GC日志
- -XX:+PrintGCDetails 输出GC的详细日志
- -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
- -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
- -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
- -Xloggc:../logs/gc.log 日志文件的输出路径
参考博客:
Metaspace:
Metaspace:
Metaspace:
参数设置:
JVM调优总结:
类加载机制: