首先上一张代表性图片:
Java虚拟机运行时数据区域被分为五个区域:堆(Heap)、栈(Stack)、方法区(Method Area)、本地方法栈(Native Stack)、程序计数器(Program Count Register)。其中我们可以看到有两个区域是线程共享的。
堆(Heap)
堆是JVM所管理的内存中最大的一块,被所有线程所共享,在JVM启动时创建。所有的对象实例、数组和大部分非静态成员变量储存于堆中,因此,此内存区域的唯一目的就是存放对象实例。
Java作为一门高级语言,拥有一个非常棒的特性:内存回收无需自己操心,JVM会帮你做到。只要聊到堆,我们第一时间想到的就是:这么多的对象存放在这一大块区域上,那么Java GC机制当然也和这一块内存息息相关咯。是的,GC于堆密不可分。在这里我们只简单的介绍一下Java GC机制:
总的来说,JVM将堆分为2个区块:新生代(Eden区+2个Survivor区)和老年代。
- 新生代
新生代中可以继续分为1个Eden区和2个Survivor区,两者默认按4:1的比例分配内存。新创建的对象会进入Eden区;在一次GC之后,仍然存活的对象从Eden区和第一个Survivor区进入第二个Survivor区;而再次GC,存活的对象从Eden区和第二个Survivor区进入第一个Survivor区。 - 老年代
如果一个对象在多次新生代中的GC中(Young GC)仍然存活,则会被复制到老年代。同时若新创建的对象太大(如长字符串或大数组),新生代空间不足,也会直接分配到老年代上(因为大对象容易触发GC,应避免使用生命周期短的大对象)。老年代空间更大,GC次数也会更少。
栈(Stack)
我们一般说的栈指的是虚拟机栈,程序会在这块内存中运行,每个方法被执行的时候,都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。而在方法从调用到执行的过程,对应着一个栈帧在虚拟机中从如栈到出栈的过程。因此,栈中保存的是基本类型以及对象的引用。栈的生命期是跟随线程的生命期,线程创建时创建,线程结束栈内存也就释放(栈是线程私有的)。
*堆与栈的对比
栈是运行时的单位,堆是存储的单位!
堆解决数据怎么存、存哪里,栈解决程序如何运行,如何处理数据。堆中存的是对象实体,栈中存的是基本数据类型和堆中对象的引用,一个对象的大小是动态变化的,无法估计;但是在栈中,一个对象引用只对应一个4byte的空间。
栈也是内存空间的一部分,虽然也能存储数据,但还是将它们分隔开来。主要是因为:
- 从软件设计角度分析,栈代表逻辑,堆代表数据,堆栈分离会使处理逻辑更清晰。
- 每个线程都有自己的线程栈,当堆栈分离时,不同栈能够共享相同堆内资源,节约内存。
- 栈因为运行是需要,比如保存系统运行的上下文,需要地址段的划分,由于栈只能向上增长,因此限制住栈存储内容的能力,而堆是根据需要可以动态增长的,因此栈和堆的拆分,使得堆动态增长成为可能,相应栈只需要记住堆中的一个地址即可。
- 契合面向对象思想。对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。
方法区(Method Area)
JVM方法区又名永久代,与堆十分类似(其实它就是堆中开辟的另一个部分),储存已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它和堆一样,也是线程共享的,但它是一片连续的堆空间。方法区的垃圾收集和堆中的老年代捆绑在一起,因此无论谁满了,都会触发方法区和老年代的垃圾收集。
本地方法栈(Native Stack)
本地方法栈和虚拟机栈十分类似,只是使用方式不同:虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。
本地方法可以通过JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和JVM相同的能力和权限(最著名的本地方法应该是System.currentTimeMillis(),JNI使Java深度使用OS的特性功能,复用非Java代码)。在项目过程中,因为本地方法是一方不属于Java的天地,因此我们将其当作了一个黑盒看待。可是我们不知道这个黑盒里藏着的是宝藏还是一枚定时炸弹,所以我们将栈分成了两部分,这样即使本地方法崩溃也不会影响JVM的稳定。
程序计数器(Program Count Register)
Register 的命名源于CPU的寄存器,CPU只有把数据装载到寄存器才能够运行。
寄存器存储指令相关的现场信息,由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?
每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域也不会发生内存溢出异常。每条线程都有一个独立的程序计数器,因此它是线程私有的,随着线程的创建而创建,随着线程的结束而死亡。