内存是操作系统中不可或缺的部分,一台机器有内存才能正常稳定地运行。编程语言历来都有自己的内存管理机制,尤其是Java,它不像C一样需要自己维护内存的关系,而是通过自己的内部机制JVM来管理内存。这虽然降低了开发同学们的入手难度,但同时也使得在运行时一旦抛出内存异常,很难知道发生了什么。以下就简单地来介绍一下JVM内存的结构。
废话不多说,先看下面图表:
▲图表1 JVM整体结构▲
此时可能有些人会一头雾水,别着急,我们一点点来看。
先看第一部分:
▲图表2 类装载系统▲
这个代表了一个类被装入JVM的过程。如果用更简单的比喻,它类似初学JDBC时Class.forName()所做的事情。具体来讲,会分为以下几步:
加载:将字节码文件按照双亲委托机制(当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作。如果上级的类加载器没有加载,自己才会去加载这个类,方块中对应的便是Bootstrap Class Loader(启动类加载器),Extension Class Loader (标准扩展类加载器),Application Class Loader(系统类加载器))进行加载;
链接字节码文件:分为三个步骤,分别是字节码验证(verify)、class类数据结构分析(prepare)以及相应的内存分配和最后的符号表的链接(resolve);
初始化操作:比如类中静态属性和初始化赋值,以及静态块的执行等。
一个类就是这样被装入了内存,那么在程序中会发生什么呢?别着急,且听我慢慢道来:
▲图表3 Java内存结构▲
这张图是运行时数据区,表示了当前JVM的所有状态,让我们一个区一个区看看:
1. 方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。运行时常量池也是方法区的一部分,比如String w = ”hello”;中,hello就被放在了方法区里。方法区是线程共享的。有一点要注意,JDK1.8 使用元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存;
2. 堆区(Heap Area):堆区是JVM中占地最大的区域,所有的实例对象全部都在堆区上,这个位置也是线程共享的;
3. 栈区(Stack Area):存放了每一个线程的当前状态,每一个线程都有一个自己的栈,而栈中存放了以下数据组成的一个个栈帧:操作数、局部变量表、动态链接、返回地址,需要注意的是,栈中只存引用或者基本类型,而且线程不共享(并没有指内部的优化动作);
4. 程序计数器(PC Registers): 它是当前线程执行字节码的行号指示器。在多线程中,为了让每个线程切换回来后能够恢复原来执行的指令,就需要为每个线程启动一个PC计数器,这些计数器之间是互补影响的,因为程序计数器和栈一样都是线程私有的。当然程序计数器是JVM唯一个不会出现内存溢出的组件;
5. 本地方法栈(Native Method Statck):保存了本地方法,它是当程序调用类库(本地方法)中的方法时才会用到它,即native method。
接下来就要介绍为我们勤勤恳恳工作的执行引擎啦:
▲图表4 Java执行引擎▲
Java执行分为编译执行(JIT compilation)和解释执行(Interpreter)。
首先我们要明白什么是编译执行,什么是解释执行:
Bash就是属于解释执行语言,一行行解释代码来完成命令;
C就是编译执行, 将文件编译成字节码,接着运行。
那为什么Java会用两套编译手段呢?先看下面这个例子:
假定你是导演,写了个剧本,让演员表演。
一种方式是让演员把整个剧本都背下来,吃透到脑子里,然后连续表演一个小时。
另一种方式是让演员表演两分钟,再看两分钟脚本,思考一下,再表演两分钟,再看一会脚本,思考一下…
单纯从效率上来讲,第一种方式一定会比第二种方式表演起来更熟练,但是现实往往不允许,或者不必要。如果不是非常需要表演的技巧,简单地看一下剧本就好啦。在特别考验表演技巧时,才需要背下整个剧本,这样才能在表演时更好地展现自己的风采。
Java也是如此,由JIT发现热代码后,将指令集优化(比如重排,合并),然后生成字节码供系统运行。至于其他的代码呢,简单的解释执行完就好了~
在运行的过程中一定会产生很多的垃圾,因为随着系统运行,很多对象都会废弃不用,此时就需要使用垃圾回收机制Garbage Collection(垃圾回收内容太多了,以后可以单拿出来讲讲)。
最后,来看下Java与系统底层交互:
▲图表5 Java与系统底层交互▲
JNI(Java本地接口)通过使用Java本地接口书写程序,可以确保代码在不同的平台上方便移植。通过JNI实现与本地方法库的调用交互,使得在Java虚拟机内运行的Java代码能够与其它编程语言互相操作,包括创建本地方法、更新Java对象、调用Java方法,引用Java类,捕捉和抛出异常等,也允许Java代码调用 C/C++或汇编语言编写的库。
好啦,今天就到这里,相信大家对JVM的内存模型已经有了一定的认知,希望在遇到这种问题时,本篇内容可以帮到正在阅读的你。学习技术是一个日积月累的过程,切不可急躁~如果写代码有天意,那一定是让你修炼!