众所周知C,C++系开发工程师在内存管理方面,既是"至高无上"的总裁,又是每天"996"的基层员工---他们拥有对象的所有权利,但是又要慢慢辛苦地维护对象的生命周期。是对"一般"的C,C++程序员的一项艰巨挑战。
但是Java程序员不需要再为每一个对象去担心何时new/delete对象,也不容易出现内存泄漏问题(不需要编写复制构造函数,析构函数,重载=表达式)。由于jvm虚拟机的存在一切显得非常美好。But,也正是这种"至高无上"的权利交给jvm,一旦程序出现莫名的泄露,oom问题的时候会一下子无所适从。Java强的地方不是语言部分而是JVM生态。现在由于cpu发展从高频率编程多核心,并行计算非常重要,Java外围的Apache的Hadoop Map Reduce等大数据技术以及Google的Dalvik,Art虚拟机都是基于JVM生态技术,所以我们需要深入学习Java虚拟机的内在!
在讨论JVM内存区域划分之前,先来看一下Java程序具体执行的过程:
Java程序执行的过程
pic来源于cnblogs
首先java源程序会被javac编译成字节码(.class结尾),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在运行过程中JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。我们生成的对象一般由他统一自动管理。
Runtime Data Area (我们关注的重点)
下面我们来了解一下运行时数据区的每部分具体用来存储程序执行过程中的哪些数据。
来源于Internet
运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。
️1⃣️.程序计数器类似于CPU里的PC寄存器的概念(保存下一条指令的所在存储单元的地址),表示当前thread执行的字节码的行号的标示。由于在JVM中multithreading是通过thread轮流切换来获得CPU 时间片的,因此,在任一具体时刻,一个CPU的内核只会执行一条thread中的指令,因此,为了能够使得每个thread都在thread切换后能够恢复在切换之前的程序执行位置,每个thread都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,这类内存区域为"thread私有"内存。
️2⃣️.Java虚拟机栈(Java Vitual Machine Stack), 跟C/C++的数据段中的栈类似。
每个thread执行一个方法时候会创建一个栈帧,栈帧存放了局部变量表,操作数的栈,运行时常量的引用。局部变量表存放了:基本数据类型,对象引用和方法返回地址。当thread执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,thread当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了。一般会抛出OutOfMemoryError。每个thread都会有一个自己的Java栈,互不干扰。
️3⃣️. Java堆(Java Heap)
这里是JVM管理内存中最大的一块,所有thread都共享这块内存,几乎所有的对象(实例)都在这里创建,被分配内存空间。所以这里也是垃圾收集者管理的主要区域,也叫GC堆(Garbage Collected Heap),内存大小不是固定不变,可以不需要连续存储。具体结构后面会详细说明。
️4⃣️. Java方法区(Method Area / Class Area)
和Heap区一样,这里是所有thread共享的,主要用于存储已被类加载器加载的类信息,常量,static变量,JIT编译后的代码等。一般把这个区域也叫做Permanent Generation(永久代)。和Heap一样,内存大小不是固定不变,可以不需要连续存储,还可以不参与垃圾回收。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
/**
* 运行是常量池溢出
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List <string> list =new ArrayList();
int i =0;
while(true) {
list.add(String.valueOf(i++));
}
}
}
//结果JavaHotSpot(TM)64-Bit Server VM warning: ignoring option PermSize=10M; support was removedin8.0JavaHotSpot(TM)64-Bit Server VM warning: ignoring option MaxPermSize=10M; support was removedin8.0// 分析我使用的时jdk8.0发生如上错误,这个说明永久代已经在java8.0中被移除,被元空间替代。关于为啥要移除替代本篇不累赘。如果使用的jdk8.0一下版本则会出现OOM:PermGen space这样可以证明常量池属于方法区//VM Args:-XX:MaxMetaspaceSize=2M(这样设置元空间大小 )
5⃣️ ️. 本地方法栈
在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
直接内存(Direct Memory)
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它直接从操作系统中分配,因此不受Java堆大小的限制,但是会受到本机总内存的大小及处理器寻址空间的限制,因此它也可能导致OutOfMemoryError异常出现。在JDK1.4中新引入了NIO机制,它是一种基于通道与缓冲区的新I/O方式,可以直接从操作系统中分配直接内存,即在堆外分配内存,这样能在一些场景中提高性能,因为避免了在Java堆和Native堆中来回复制数据。关于NIO的详细使用可以参考Java NIO
对象实例化分析
对内存分配情况分析最常见的示例便是对象实例化:
String s = new String("Hello");
这段代码的执行会涉及java栈、Java堆、方法区三个最重要的内存区域。假设该语句出现在方法体中,及时对JVM虚拟机不了解的Java使用这,应该也知道obj会作为引用类型(reference)的数据保存在Java栈的本地变量表中,而会在Java堆中保存该引用的实例化对象,但可能并不知道,Java堆中还必须包含能查找到此对象类型数据的地址信息(如对象类型、父类、实现的接口、方法等),这些类型数据则保存在方法区中。
参考资料:
www.google.com
《深入理解Java虚拟机》