内存区划分
Java虚拟机在执行Java程序的过程中会把它锁管理的内存划分为若干个不同的数据区域。 这些区域有各自不同的用途,以及创建和销毁的时间。 有的区域随着虚拟机的进程一直存在,有的区域依赖用户线程的启动和结束而建立和销毁。
根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存,包含一下几个运行时的数据区域。如图:
一般我们面试的时候,都是笼统的说:堆、栈、以及方法区,JDK 8之后方法区变为“元空间”。
这个说法不错,下面我们详解一下。
程序计数器
程序计数器区域⼀块内存较小的区域,它⽤于存储线程的每个执行指令,每个线程都有自己的程序计数器,此区域不会有内存溢出的情况。
由于Java虚拟机的多线程是通过线程的轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只执行一个线程。
那么为了每次切换线程后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器, 所以这个程序计数器区域,是一个线程私有的内存。
虚拟机栈与本地线程栈
在很多情况下,我们都是把这两个合在一起说的。
两方面的原因,一个是《Java虚拟机规范》对本地方法栈使用的语言、使用方式等没有任何规定。 二是有些虚拟机确实把这两个合二为一,比如Hot-Spot虚拟机。
如果按照我这里的浅显理解的话,他们之间的区别可以这么说。
虚拟机栈的生命周期与线程相同,主要就是存储线程执行相关的局部变量信息。
本地方法栈也是如此,但是所执行的内容可能是虚拟机本地方法Native。
所以我们在面试的时候,可以只说虚拟机栈,甚至直说本地方法栈都可以。(在我面试数十次的经历中,确实没有遇到,挑这两个问区别的)
回到虚拟机栈,在每个方法被执行的时候,Java虚拟机都会在这个线程栈中创建一个“栈帧”,用于存储局部变量表,操作数栈,方法出口等等信息。每一个方法被调用,直到执行完毕,就对应着一个栈帧从入栈到出栈的过程。
因为我们线程调用方法是一层层递进的,所以这个栈帧也是一层层叠加。当调用深度超过虚拟机所允许的深度时,就会抛出StackOverflowError异常。
还有一个情况是,栈帧层层递进的时候,虚拟机栈也是在不断增大。
如果栈的内存不足,并且提前分配好不能扩展;或者是扩展的时候无法申请到足够的内存,就会发生OutOfMemoryError异常。
堆区
堆区是各个线程共享的区域。
这也是老生常谈的问题,主要存放JVM启动时创建的数组和对象实例,还有垃圾回收也主要在堆区发生,还有分代模型等等巴拉巴拉。
具体内容我们可以到分析垃圾回收的时候详细说下。
需要注意的是,堆区也是可以被固定大小的,当内存不足无法扩展时,也会抛出OutOfMemoryError异常。
方法区
与堆区一样,也是各个线程共享的区域。
主要存储那些常亮,静态变量,代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为一个堆区的逻辑部分,但是名字却为“Non-Heap” 非堆,这就是要与堆区做区分。
这里不得不提一下“永久代” 这个概念,以前很多时候都是把永久代与方法区混为一谈。
这是因为HotSpot虚拟机的设计团队把垃圾收集器的分代设计扩展到了方法区,把方法区称为永久代来管理垃圾回收机制。
但是后来又把永久代的概念放到了本地内存中,一直到JDK8 之后,完全放弃了永久代的概念,改为在本地内存新实现的“元空间”来代替
粗略来说,就是最早时候方法区=永久代,后来统一并入本地内存的“元空间”中。
运行时常量池
也是方法区的一部分,主要 存储Class文件中编译期生成的各种字面量与符号引用
字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量;
符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用。符号引用就是⼀个字符串,只要我们在代码中引用了⼀个非字面量的东西,不管它是变量还是常量,它都只是由⼀个字符串定义的符号,这个字符串存在常量池里,类加载的时候第一次加载到这个符号时,就会将这个符号引用(字符串)解析成直接引用(指针)。