java虚拟机中内存的划分
这篇文章不是讲Java内存管理的,也不是Java运行时内存如何划分的。而是JVM内的内存如何划分的,所以不包含直接内存部分。
假设现在是30年前,还没有Java,而你穿越回来想要帮助老高(没错,就是那个老高)快点诞生Java。 你已经知道虚拟机对于Java“一次编写,随处运行”的必要性,所以打算帮老高先设计一下虚拟机。 那么问题来了:你觉得虚拟机最基本的作用是什么?
不要想得太复杂,最基本就是最基本,不要说什么“不同平台”之类的话。 最基本的作用就是把硬盘的代码读入内存并执行,对吧?
当然了,Java源代码在进入虚拟机之前已经被编译成字节码了(这一块设计你另外找时间和老高说吧,这里跳过), 字节码才是存储在硬盘上给虚拟机来读取的代码。由通过C语言编写的类加载器,硬盘上的代码可以进入到内存里,这块内存是虚拟机这个进程专属的。
类中可执行的当然是方法了。虚拟机在执行类的方法的时候会产生大量的变量信息,这些变量放在哪呢? 存到文件里?肯定不行,虚拟机要读取的是内存中的数据。放到其他进程的内存里?虚拟机倒想,但你不能想。 思来想去(这么简单的问题),你决定专门在虚拟机内开辟一块空间放这些变量。 为了和刚刚加载进来的代码区分开,你给两块内存分别取了名字:代码因为都是方法,那块内存就叫“方法区”吧;变量是方法执行过程中的数据,就叫“数据区”吧(我瞎掰的)!
好像很简单。如同“程序=算法+数据结构”,虚拟机=字节码+数据。
现在程序开始运行了。Java程序是以线程为单位运行的,没有多线程环境的话就是一个线程,整个进程在主线程上。 你忽然想到,系统上同时会运行很多进程的线程,CPU是并行处理的:跑跑那个,跑跑这个。该怎么记录每个线程的运行快照呢? 看来还得往“数据区”加点东西,用来记录当前执行的现场。
CPU的执行可以阅读《重温操作系统——“共享”CPU》
首先要记录正在执行的方法信息,方法是有调用链的,所以使用栈吧。后进先出,正好符合链式规则。 接下来方法内有不少代码,执行到那一行了也要记下来。这些记录下来的信息是每个线程都有的,和前面代码执行过程中的数据不一样,那个是虚拟机中就一份。
你对自己的设计很满意,为了区分开线程独有和线程共享,打算给他们起个名字。 咦等等,Java可以执行的方法除了Java实现的以外,还有C实现的本地方法呢,要不要把他们区分开呢?你想了想,似乎没有分开的必要,那就在一个栈吧。 所以,这个栈就是“虚拟机栈”,它由Java方法栈和本地方法栈合并组成;代码执行地址的记录模仿寄存器叫“程序计数器”吧。 这样“数据区”就拆出去栈和计数器了,剩下的看它是什么数据结构就叫什么吧:堆!
堆是有根结点的,Java堆有吗?有的,没有连接到根上的是要被清理掉的
现在结构清晰了:虚拟机内分两大部分,一部分是死的方法区(永久代),一部分是不断变化的数据区;数据区根据是否线程独享分为堆和栈;栈分为方法栈和计数器。
程序计数器大小是固定的,因为只用来记录代码的地址。所以不会出现内存溢出的问题。
因为线程有独享内存,所以如果线程太多就会撑爆内存,报OutOfMemoryError(注意这是一个Error)。
方法栈的栈帧用数组记录局部变量和方法入参(还有this指针),除了两种小数类型占用两个数组单元,其他基本数据类型和引用类型都占用一整个数组单元,byte也是。这和堆上分配是不一样的
每个线程能够占据的独享空间有限,所以方法栈也不能太深,不然也会报错StackOverflowError(就是那个网站的名字)。
虚拟机占据的内存有有限。尽管方法区也可能变化,但是大部分内存变化大发生在堆上。 如果堆上变量超多,也会报OutOfMemoryError。
更多关于虚拟机内存内容的介绍,可以参阅《JVM的内存区域划分》。
