本篇博客大部分内容都摘抄自周志明写的《深入理解java虚拟机》,这本书很好,推荐!
我们所写的Java代码,首先经过编译,形成Class文件(字节码),然后Java虚拟机会读取Class文件(类加载)的信息,为变量分配内存,执行代码逻辑。这一切都是在Java虚拟机的内存中完成的。我们需要了解Java虚拟机的内存模型,这对我们Java进阶至关重要。
Java虚拟机的内存模型如下图所示
image.png
我们需要重点了解运行时数据区,这块内存与我们所写代码的运行直接相关。
程序计数器
程序计数器是一块较小的内存,用来控制程序的执行,保存字节码中当前应该执行的行数。分支、循环、跳转、异常处理都需要依赖这个程序计数器。
java虚拟机的多线程是通过线程轮流切换获取处理器的时间片来实现的。因此,每个线程都应该有一个程序计数器来标记该线程下一条应该执行的指令的位置,各个线程的程序计数器互不干扰。
如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,这个计数器值为空。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。
虚拟机栈
image.png虚拟机栈也是线程私有的,他的生命周期与线程相同。
虚拟机栈保存着程序运行的状态和变量,比如:局部变量,对象的引用变量(引用对象的实例放在堆中,指向实例的引用变量放在栈中),方法的返回地址等。
栈中存放的直接元素是栈帧,每个方法执行时都会创建一个栈帧,并将栈帧压入栈中。当方法执行完毕后,栈帧就会弹出。
栈帧可分为几个重要的部分:局部变量表、操作数栈、动态连接、方法出口信息等。
局部变量表
局部变量表存放了编译器可知的各种基本数据类型(Boolean、byte、char、int、byte、long、double、float),对象引用(对象实例存放在堆内存中,栈内存存放指向实例的地址或者句柄),返回地址。
局部变量表需要多大内存在编译的时候就已经确定了。
局部变量表的容量以槽(slot)为最小单位。在64位虚拟机中一个slot占64位,32位虚拟机中一个slot占32位。在32位虚拟机中,Boolean、byte、char、short、int、float、reference、return address都可以使用一个slot保存,而double、long使用两个slot保存。
局部变量表中的slot是可以重用的,如果超过了变量的作用域,这个变量的slot就可以被其他变量使用。
操作数栈
是一个后入先出的栈。
在进行数字运算时需要将待计算的数据压入栈中。例如,在进行a+b的操作时,就会先将a压入操作数栈中,然后将b压缩操作数栈中,然后将这两个数出栈,调用执行引擎计算结果,将结果压入栈中。
在调用其他方法时,参数的传递也需要使用到操作数栈。需要先将参数压入操作数栈中,然后将参数复制到被调用方法的局部变量表中。为了减少参数的复制,很多虚拟机会将一个方法的操作数栈和另外一个方法的局部变量表一部分重合,入下图所示。
image.png
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属的方法的引用,这个引用也叫直接引用。class文件的常量池中包含了大量方法的符号引用,这些方法符号应用并没有确定方法执行的细节,在程序执行的时候符号引用才会转化为直接引用。
方法返回地址
方法有两种可能进行返回,一个是执行完成,一个是遇到错误抛出异常。
不管哪种返回,都需要返回到本方法被调用的地方。
本地方法栈
本地方法栈的功能与虚拟机栈的功能类似。虚拟机栈为java方法服务,本地方法栈为native方法服务。
堆
对于大多数应用来说,java堆是java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,唯一的功能就是存放对象实例,几乎所有的对象实例都在这里分配(编译器优化,也可能会在栈上分配或者用标量来替换)。
java堆是垃圾回收的主要区域。现在很多虚拟机都使用分代收集算法,将需要回收的内存分为新生代和老生代。java堆是新生代,在新生代回收内存一般使用复制算法。
方法区
方法区可是线程共享的区域,用来保存已经被虚拟机加载的类的信息,变量、静态变量、即使编译器编译后的代码。
方法区中的内容的生命周期一般都比较长,因此方法区也被称为老生代。老生代的GC算法一般使用标记/清除算法
方法区中有运行时常量池,例如字符串常量池、基本数据类型中int、char、byte、short、boolean的封装类型也有常量池。
学习java虚拟机,才知道我们写的程序是怎么被编译、加载、执行的,对于提升代码能力很有帮助!
网友评论