Java内存管理:Java内存区域 JVM运行时数据区
在前面的一些文章了解到javac编译的大体过程、Class文件结构、以及JVM字节码指令。
下面我们详细了解Java内存区域:先说明JVM规范定义的JVM运行时分配的数据区有哪些,然后分别介绍它们的特点,并指出给出一些HotSpot虚拟机实现的不同点和调整参数。
1、Java内存区域概述
1-2、C/C++与Java程序开发的内存管理
在内存管理领域,C/C++程序开发与Java程序开发有着完全不同的理念:
1、C/C++程序开发
自己管理内存是一项基础的工作;
自已分配内存,但也得自己来及时回收;
比较自由,但多了些工作量,且容易出现内存泄露和内存溢出等问题;
2、Java程序开发
JVM管理内存,不需要自己手动分配内存和释放内存;
不容易出现内存泄露和内存溢出;
一旦出现问题不容易排查,所以得了解JVM是怎么使用内存;
1-2、Java内存区域与JVM运行时数据区
如上图, Java虚拟机规范定义了字节码执行期间使用的各种运行时数据区,即JVM在执行Java程序的过程中,会把它管理的内存划分为若干个不同的数据区域,包括:
程序计数器、java虚拟机栈、本地方法栈、java堆、方法区、运行时常量池;
从线程共享角度来说,可以分为两类:
1、所有线程共享的数据区
方法区、运行时常量池、java堆;
这些数据区域是在Java虚拟机启动时创建的,只有当Java虚拟机退出时才会被销毁;
2、线程间隔离的数据区
程序计数器、java虚拟机栈、本地方法栈、
这些数据区域是每个线程的"私有"数据区,每个线程都有自己的,不与其他线程共享;
每个线程的数据区在创建线程时创建,并在线程退出时被销毁;
3、另外,还一种特殊的数据区
直接内存--使用Native函数库直接分配的堆外内存;
即Java内存区域 = JVM运行时数据区 +直接内存。
2、Java各内存区域说明
上面图片展示的是JVM规范定义的运行时数据概念模型,实际上JVM的实现可能有所差别,下面在介绍各内存数据区时会给出一些HotSpot虚拟机实现的不同点和调整参数。
2-1、程序计数器
程序计数器(Program Counter Register),简称PC计数器;
1、生存特点
每个线程都需要一个独立的PC计数器,生命周期与所属线程相同,各线程的计数器互不影响;
2、作用
JVM字节码解释器通过改变这个计数器的值来选取线程的下一条执行指令;
3、存储内容
JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的;
在任意时刻,一个线程只会执行一个方法的代码(称为该线程的当前方法(Current Method));
(A)、如果这个方法是Java方法,那PC计数器就保存JVM正在执行的字节码指令的地址;
(B)、如果该方法是native的,那PC计数器的值是空(undefined);
4、内存分配特点
PC计数器占用较小的内存空间;
容量至少应当能保存一个returnAddress类型的数据或者一个与平台相关的本地指针的值;
5、异常情况
唯一一个JVM规范中没有规定会抛出OutOfMemoryError情况的区域;
2-2、Java虚拟机栈
Java虚拟机栈(Java Virtual Machine Stack,JVM Stack),指常说的栈内存(Stack);
和Java堆指的堆内存(Heap),都是需要重点关注的内存区域;
1、生存特点
每个线程都有一个私有的,生命周期与所属线程相同;
2、作用
描述的是Java方法执行的内存模型,与传统语言中(如C/C++)的栈类似;
在方法调用和返回中也扮演了很重要的角色;
3、存储内容
用于保存方法的栈帧(Stack Frame);
每个方法从调用到执行结束,对应其栈帧在JVM栈上的入栈到出栈的过程;
栈帧:
每个方法执行时都会创建一个栈帧,随着方法调用而创建(入栈),随着方法结束而销毁(出栈);
栈帧是方法运行时的基础结构;
栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息;
(A)、局部变量表
局部变量表(Local Variables Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
这些都是在编译期可知的数据,所以一个方法调用时,在JVM栈中分配给该方法的局部变量空间是完全确定的,运行中不改变;
一个方法分配局部变量表的最大容量由Class文件中该方法的Code属性的max_locals数据项确定;
(B)、操作数栈
操作数栈(Operand Stack)简称操作栈,它是一个后进先出(Last-In-First-Out,LIFO)栈;
在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容(任意类型的值),也就是入栈/出栈操作;
在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果;
一个方法的操作数栈长度由Class文件中该方法的Code属性的max_stacks数据项确定;
(C)、动态链接
每一个栈帧内部都包含一个指向运行时常量池的引用,来支持当前方法的执行过程中实现动态链接 (Dynamic Linking);
在 Class 文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用(Symbolic Reference)来表示的;
动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用(除了在类加载阶段解析的一部分符号);
4、内存分配特点
因为除了栈帧的出栈和入栈之外,JVM栈从来不被直接操作,所以栈帧可以在堆中分配;
JVM栈所使用的内存不需要保证是连续的;
JVM规范允许JVM栈被实现成固定大小的或者是根据计算动态扩展和收缩的:
(A)、固定大小
如果JVM栈是固定大小的,则当创建新线程的栈时,可以独立地选择每个JVM栈的大小;
(B)、动态扩展或收缩
在动态扩展或收缩JVM栈的情况下,JVM实现应该提供调节JVM栈最大和最小内存空间的手段;
两种情况下,JVM实现都应当提供调节JVM栈初始内存空间大小的手段;
HotSpot VM通过"-Xss"参数设置JVM栈内存空间大小;
5、异常情况
JVM规范中对该区域,规定了两种可能的异常状况:
(A)、StackOverflowError
如果线程请求分配的栈深度超过JVM栈允许的最大深度时,JVM将会抛出一个StackOverflowError异常;
(B)、 OutOfMemoryError
如果JVM栈可以动态扩展,当然扩展的动作目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那JVM将会抛出一个OutOfMemoryError异常;
该区域与方法执行的JVM字节码指令密切相关,这里篇幅有限,以后有时间会分析方法的调用与执行过程,再来详细介绍该区域。
2-3、本地方法栈
本地方法栈(Native Method Stack)与 Java虚拟机栈类似;
1、与Java虚拟机栈的区别
Java虚拟机栈为JVM执行Java方法(也就是字节码)服务;
本地方法栈则为Native方法(指使用Java以外的其他语言编写的方法)服务;
2、HotSpot VM实现方式
JVM规范中没有规定本地方法栈中方法使用的语言、方式和数据结构,JVM可以自由实现;
HotSpot VM直接把本地方法栈和Java虚拟机栈合并为一个;
2-4、Java堆
Java堆(Java Heap)指常说的堆内存(Heap);
1、生存特点
所有线程共享;
生命周期与JVM相同;
2、作用
为"new"创建的实例对象提供存储空间;
里面存储的这些对象实例都是通过垃圾收集器(Garbage Collector)进行自动管理,所以Java堆也称"GC堆"(Garbage Collected Heap);
对GC堆以及GC的参数设置调整,就是JVM调优的主要内容;
3、存储内容
用于存放几乎所有对象实例;
(随JIT编译技术和逃逸分析技术发展,少量对象实例可能在栈上分配,详见后面介绍JIT编译的文章);
4、内存分配特点
(A)、Java堆划分
为更好回收内存,或更快分配内存,需要对Java堆进行划分:
(I)、从垃圾收集器的角度来看
JVM规范没有规定JVM如何实现垃圾收集器;
由于很多JVM采用分代收集算法,所以Java堆还可以细分为:新生代、老年代和永久代;
(II)、从内存分配角度来看
为解决分配内存线程不安全问题,需要同步处理;
Java堆可能划分出每个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),减少线程同步;
HotSpot VM通过"-XX:+/-UseTLAB"指定是否使用TLAB;
(B)、分配调整
和JVM栈一样,Java堆所使用的物理内存不需要保证是连续的,逻辑连续即可;
JVM规范允许Java堆被实现成固定大小的或者是根据计算动态扩展和收缩的:
两种情况下,JVM实现都应当提供调节JJava堆初始内存空间大小的手段;
在动态扩展或收缩的情况下,还应该提供调节最大和最小内存空间的手段;
(C)、HotSpot VM相关调整
目前主流的JVM都把Java堆实现成动态扩展的,如HotSpot VM:
(1)、初始空间大小
通过"-Xms"或"-XX:InitialHeapSize"参数指定Java堆初始空间大小;
默认为1/64的物理内存空间;
(2)、最大空间大小
通过"-Xmx"或"-XX:MaxHeapSize"参数指定ava堆内存分配池的最大空间大小;
默认为1/4的物理内存空间;
Parallel垃圾收集器默认的最大堆大小是当小于等于192MB物理内存时,为物理内存的一半,否则为物理内存的四分之一;
(3)、各年代内存的占用空间与可用空间的比例
通过"-XX:MinHeapFreeRatio"和"-XX:MaxHeapFreeRatio"参数设置堆中各年代内存的占用空间与可用空间的比例保持在特定范围内;
默认:
"-XX:MinHeapFreeRatio=40":即一个年代(新生代或老年代)内存空余小于40%时,JVM会从未分配的堆内存中分配给该年代,以保持该年代40%的空余内存,直到分配完"-Xmx"指定的堆内存最大限制;
"-XX:MaxHeapFreeRatio=70":即一个年代(新生代或老年代)内存空余大于70%时,JVM会缩减该年代内存,以保持该年代70%的空余内存,直到缩减到"-Xms"指定的堆内存最小限制;
这两个参数不适用于Parallel垃圾收集器(通过“-XX:YoungGenerationSizeIncrement”、“-XX:TenuredGenerationSizeIncrement ”能及“-XX:AdaptiveSizeDecrementScaleFactor”调节);
(4)、年轻代与老年代的大小比例
通过"-XX:NewRatio":控制年轻代与老年代的大小比例;
默认设置"-XX:NewRatio=2"表新生代和老年代之间的比例为1:2;
换句话说,eden和survivor空间组合的年轻代大小将是总堆大小的三分之一;
(5)、年轻代空间大小
通过"-Xmn"参数指定年轻代(nursery)的堆的初始和最大大小;
或通过"-XX:NewSize"和"-XX:MaxNewSize"限制年轻代的最小大小和最大大小;
(6)、定永久代空间大小
通过"-XX:MaxPermSize(JDK7)"或"-XX:MaxMetaspaceSize(JDK8)"参数指定永久代的最大内存大小;
通过"-XX:PermSize(JDK7)"或"-XX:MetaspaceSize(JDK8)"参数指定永久代的内存阈值--超过将触发垃圾回收;
注:JDK8中永久代已被删除,类元数据存储空间在本地内存中分配;
详情请参考:/javase/8/docs/technotes/guides/vm/gctuning/considerations.html#sthref62
(D)、调整策略
关于这些参数的调整需要垃圾收集的一些知识(以后文章会介绍),先来简单了解:
当使用某种并行垃圾收集器时,应该指定期望的具体行为而不是指定堆的大小;
让垃圾收集器自动地、动态的调整堆的大小来满足期望的行为;
调整的一般规则:
除非你的应用程序无法接受长时间的暂停,否则你可以将堆调的尽可能大一些;
除非你发现问题的原因在于老年代的垃圾收集或应用程序暂停次数过多,否则你应该将堆的较大部分分给年轻代;
关于HotSpot虚拟机堆内存分代说明以及空间大小说明请参考:
/javase/8/docs/technotes/guides/vm/gctuning/generations.html#sthref16
/javase/8/docs/technotes/guides/vm/gctuning/sizing.html#sizing_generations
5、异常情况
如果实际所需的堆超过了垃圾收集器能提供的最大容量,那Java虚拟机将会抛出一个OutOfMemoryError异常;
该部分的内存如何分配、垃圾如何收集,上面这些参数如何调整,将在以后的文章详细说明。
2-5、方法区
方法区(Method Area)是堆的逻辑组成部分,但有一个别名"Non-Heap"(非堆)用以区分;
1、生存特点
所有线程共享;
生命周期与JVM相同;
2、作用
为类加载器加载Class文件并解析后的类结构信息提供存储空间;
以及提供JVM运行时常量存储的空间;
3、存储内容
用于存储JVM加载的每一个类的结构信息,主要包括:
(A)、运行时常量池(Runtime Constant Pool)、字段和方法数据;
(B)、构造函数、普通方法的字节码内容以及JIT编译后的代码;
(C)、还包括一些在类、实例、接口初始化时用到的特殊方法;
4、内存分配特点
(A)、分配调整
和Java堆一样,所使用的物理内存不需要保证是连续的;
或以实现成固定大小的或者是根据计算动态扩展和收缩的;
(B)、方法区的实现与垃圾回收
JVM规范规定:
虽然方法区是堆的逻辑组成部分,但不限定实现方法区的内存位置;
甚至简单的虚拟机实现可以选择在这个区域不实现垃圾收集;
因为垃圾收集主要针对常量池和类型卸载,效果不佳;
但方法区实现垃圾回收是必要的,否则容易引起内存溢出问题;
(C)、HotSpot VM相关调整
(I)、在JDK7中
使用永久代(Permanent Generation)实现方法区,这样就可以不用专门实现方法区的内存管理,但这容易引起内存溢出问题;
有规划放弃永久代而改用Native Memory来实现方法区;
不再在Java堆的永久代中生成中分配字符串常量池,而是在Java堆其他的主要部分(年轻代和老年代)中分配;
更多请参考:/javase/8/docs/technotes/guides/vm/enhancements-7.html
(II)、在JDK8中
永久代已被删除,类元数据(Class Metadata)存储空间在本地内存中分配,并用显式管理元数据的空间:
从OS请求空间,然后分成块;
类加载器从它的块中分配元数据的空间(一个块被绑定到一个特定的类加载器);
当为类加载器卸载类时,它的块被回收再使用或返回到操作系统;
元数据使用由mmap分配的空间,而不是由malloc分配的空间;
通过"-XX:MaxMetaspaceSize" (JDK8)参数指定类元数据区的最大内存大小;
通过"-XX:MetaspaceSize" (JDK8)参数指定类元数据区的内存阈值--超过将触发垃圾回收;
详情请参考:/javase/8/docs/technotes/guides/vm/gctuning/considerations.html#sthref62
5、异常情况
如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常;
2-6、运行常量池
运行常量池(Runtime Constant Pool)是方法区的一部分;
1、存储内容
是每一个类或接口的常量池(Constant_Pool)的运行时表示形式;
包括了若干种不同的常量:
(A)、从编译期可知的字面量和符号引用,也即Class文件结构中的常量池;
(B)、必须运行期解析后才能获得的方法或字段的直接引用;
(C)、还包括运行时可能创建的新常量(如JDK1.6中的String类intern()方法)
2-7、直接内存
直接内存(Direct Memory)不是JVM运行时数据区,也不是JVM规范中定义的内存区域;
1、特点
是使用Native函数库直接分配的堆外内存;
被频繁使用,且容易出现OutOfMemoryError异常;
2、作用
因为避免了在Java堆中来回复制数据,能在一些场景中显著提高性能;
3、实现方式
JDK1.4中新加入NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式;
它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java椎中的DirectByteBuffer对象作为这块内存的引用进行操作;
4、HotSpot VM相关调整
可以通过"-XX:MaxDirectMemorySize"参数指定直接内存最大空间;
不会受到Java堆大小的限制,即"-Xmx"参数限制的空间不包括直接内存;
这容易导致各个内存区域总和大于物理内存限制,出现OutOfMemoryError异常;
到这里,我们大体了解Java各内存区域是什么,有些什么特点了,但方法执行的JVM字节码指令如何在Java虚拟栈中运作的,以及Java堆内存如何分配、垃圾如何收集,如何进行JVM调优,将在以后的文章详细说明。
后面我们将分别去了解:方法的调用与执行、JIT编译--在运行时把Class文件字节码编译成本地机器码的过程、以及JVM垃圾收集相关内容……
【参考资料】
1、《The Java Virtual Machine Specification》Java SE 8 Edition:/javase/specs/jvms/se8/html/index.html
2、《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》:/javase/8/docs/technotes/guides/vm/gctuning/index.html
3、《Memory Management in the Java HotSpot™ Virtual Machine》:/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf
4、HotSpot虚拟机参数官方说明:/javase/8/docs/technotes/tools/unix/java.html
5、《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版 第2章
6、Java Class文件结构解析 及 实例分析验证