1.基本概念JVM可以理解为运行Java代码的虚拟机。虚拟机在执行java程序的过程中,会把自己管理的内存划分成几个区域,这些区域各司其职,相互配合,确保程序的完整运行。如上图所示,运行时数据区分为五个部分,分别是线程私有区:程序计数器、java虚拟机栈、本地方法栈,以及线程共享区:java堆、方法区。下面我们就来一一了解一下这几个划分区域的用途。程序计数器程序计数器是一个比较小的空间,相当于一个执行程序的行号指示符。那么什么是行号指示器呢?让我们在这里举个例子。每个人都应该玩过超级马里奥游戏。我们把马里奥头上的那一排砖块想象成一个一维数组。我们把这个一维数组当做内存,数组中的元素就是我们编译好的代码。马里奥用脑袋将砖块一块一块地敲开。他每弹出一个,就相当于执行了一行代码。这时候马里奥就相当于这个“行号指示器”。它记录了程序中下一条要执行的指令的地址,分支、循环、跳转、异常处理、线程回收等基本功能都需要这个小马里奥来完成。为什么程序计数器被称为“线程私有”内存?还是以马里奥为例,假设马里奥游戏有随意切换关卡的功能,我玩了第一关一分钟,爆完20块砖后想直接进入第二关玩,我来到第二层最后应该是从第二层的第一块砖开始,切换回第一层后应该是第20块砖。由于JVM在任意时刻只能在一个线程中执行指令,为了保证在多线程中线程来回切换后程序计数器的值是正确的,每个线程需要一个独立的程序计数器,并且每个线程之间的程序计数器计数器互不影响,独立存储。我们称它们为“私有线程”。还有一点需要注意的是,java中原生修饰的方法一般都是在本地声明的,C和C++在不同的地方实现。如果线程正在执行java方法,这个计数器记录的是正在执行的虚拟机的字节码地址。如果它正在执行本地修改的方法,则此计数器的值为空(未定义)。此内存是Java虚拟机规范中唯一未指定OutOfMemoryError的区域。Java虚拟机栈我们在写程序的时候,会封装很多方法,以便复用。每个方法在执行时都会创建一个栈帧(StackFrame)来存放局部变量表、操作数栈、动态链接和方法。执行函数时将使用的导出和其他信息。每个方法从调用到执行完成的过程对应一个栈帧从入栈到出栈的过程。注意这个区域可能会出现两种异常:如果线程请求的堆栈深度大于虚拟机允许的深度,则会抛出StackOverFlowError异常。大多数虚拟机都支持动态扩容,即当这种异常情况即将发生时,虚拟机回去申请足够的内存。如果扩展没有申请足够的内存,则会抛出OutOfMemoryError异常。和程序计数器一样,这个内存是线程私有的。本地方法栈本地方法栈的作用与虚拟机栈非常相似。它们的区别在于虚拟机栈是执行java方法的,而本地方法栈是native的。我们刚才讲程序计数器的时候提到过。本机方法在不同的地方用C和C++实现。和虚拟机栈一样,native方法栈也会抛出StackOverFlowError和OutOfMemoryErrorJava堆异常。大家都知道,Java是一门面向对象的语言,在我们日常的代码中无处不在。new语句创建对象,那么这些创建的对象放在内存的什么地方呢?答案是:堆。所以Java堆是内存中最大的一块区域,所有线程共享。这片区域就像是皇帝的后宫,所有皇帝的物件都放在这里。皇帝当然是写代码的你!Java堆是在虚拟机启动时创建的。由于Java堆是垃圾收集器的主要区域,(什么是垃圾收集器以及各种垃圾收集算法会在后面的系列文章中讨论)常被称为GC堆(GarbageCollectedHeap)。而且堆中还会有各种空间,关于Java堆中各个细分区域的分配细节,后面的文章会重点介绍。现在主流的虚拟机可以通过配置-Xmx和-Xms来扩展堆内存空间的大小。如果堆中的对象不能被填满,堆不能再满足扩容,就会抛出OutOfMemoryError异常。方法区方法区和Java堆一样,是线程间共享的内存区域。用于存放类信息、常量、静态变量和虚拟机已经加载的即时编译代码。在HotSpot虚拟机上,虚拟机的设计者使用永久代来实现方法区。(堆一般分为新生代、老年代、永久代三部分,后面会讲到)在Java8中,永久代已经被去掉,取而代之的是一块叫做“元数据区”(metaspace)的区域。元空间的本质类似于永久代,是JVM规范中方法区的实现。但是,元空间与永久代最大的区别在于,元空间不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存的限制。类的元数据放入本机内存,字符串池和类的静态变量放入java堆。这样,类的元数据能加载多少,就不再受MaxPermSize控制,而是受系统实际可用空间的控制。
