写在前面
Java代码是如何执行的?简单说Java的代码通过Java编译器编译成字节码,Java虚拟机加载编译好的字节码,完成执行。字节码在虚拟机上运行,这就是我们平时经常说到的Java语言与平台无关,无论什么样的系统环境,最终执行的都是Java的虚拟机。
Java内存
下图是Java虚拟机的内存图,引用自《深入理解Java虚拟机》,推荐这本介绍Java虚拟机的图书,内容详实而且易懂。
白色内存区域(虚拟机栈、本地方法栈和程序计数器)是线程私有的,也就是每一条正在执行的线程都具有自己独享的虚拟机栈、本地方法栈和程序计数器。
程序计数器
执行Java方法,记录的是线程正在执行的虚拟机字节码指令的地址,也就是指示程序执行到了哪里。如果执行的是本地的native方法,计数器为空。
虚拟机栈
Java方法执行的线程内存模型,每一个方法的执行,Java虚拟机都会同步创建一个栈帧,用于存放局部变量表、操作数栈等信息。虚拟机栈是线程执行方法是所需要用到的。栈帧可参考Java虚拟机——运行时栈帧结构。
本地方法栈
本地方法栈与虚拟机栈相似,虚拟机栈为执行Java方法使用,本地方法栈为执行native方法服务。
蓝色内存区域(方法区和堆)为所有线程共享的内存区域。
Java堆
“所有的对象实例以及数组都应当在堆上分配”,是内存管理最重要的区域,垃圾回收器主要工作的区域。经常被提起的Java回收主要就是对Java堆上实例的回收。
方法区
包括运行时常量池,存储被虚拟机加载的类型信息、常量、静态变量等信息。因为大多数程序使用HotSpot虚拟机,HotSpot虚拟机设计团队把分代的设计应用在方法区上,所以很多人习惯称之为永久代。JDK7开始把字符串常量池、静态变量等移到Java堆上,到了JDK8,永久代被元空间取代。
直接内存
JDK1.4中增加了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的IO方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作。
对象的创建
堆内存的分配
Java对象实例创建在堆上,堆上内存分配包括两种方式——指针碰撞和空闲列表。
- 指针碰撞
在内存空间上存在一个指针指向空闲区域的首地址,每次分配一个对象实例就把指针移动一段对象大小相等的距离,这样就完成了一次对象空间分配。这种分配方式当堆空间不规整时,例如进行垃圾回收而产生内存碎片,就显得无能为力,需要额外的整理空间的操作。 - 空闲列表
把堆空间上的空闲区域与一张列表关联,这样通过查询空闲列表就知道哪些区域是空闲的。这种分配方式,会产生大量零碎空间导致大对象无法及时分配。
至于虚拟机选择哪种堆内存的分配方式与所选择的垃圾回收器有关。
对象创建初始化
分配到内存空间后,虚拟机初始化空间为零值,并且进行一些必要的设置,这时虚拟机已经构造出了一个新的对象,字段都为默认的零值;接下来,Java代码执行构造函数,设置对象实例的字段等。
对象的访问
访问对象的方式有两种:
-
使用句柄
系统分配一块空间作为句柄池,句柄池中的句柄指向对象实例和类型数据。
-
直接指针
指针直接指向对象实例和类型数据。
虚拟机HotSpot主要使用直接指针方式进行对象访问。