java虚拟机内部原理分析
1 java虚拟机的分类
- Sun HotSpot 也是目前大部分开发人员使用的虚拟机。目前被Oracle公司收购,如何知道我们安装的是哪个虚拟机呢,如下所示:
kar:~ karl$ java -version
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
- BEA JRockit VM BEA开发出来的虚拟机号称世界上最快的java虚拟机,专注服务器硬件和服务端应用场景高度优化的虚拟机,后来也被Oracle公司收购,然后在java8 中其实是相互取Sun HotSpot 和 BEA JRockit VM的优点。
- IBM J9 VM 定位和Sun HotSpot很像,主要应用在自身的产品中。
总结:大部分如果没有特殊的说明,我们所说的java虚拟机都是指HotSpot虚拟机。
1.1 java 虚拟机运行时的数据区结构
- Java 虚拟机在执行java程序的过程中会他所管理的内存划分为若干个不同的数据区中,每个数据区各司其职,以及创建和销毁的时间。在java虚拟机规范中规定,java虚拟机所管理的内存包括五大区域,分别是:程序计数器,虚拟机栈,本地方法栈,堆和方法区。
如下图所示:需要注意的点 方法区和堆 是线程共享的 ,只有在这两个区域的数据才会出现线程安全性问题。
java编译加载过程可参考:https://blog.csdn.net/qq_33249725/article/details/88880213
java程序 – > .java文件 —>(通过javac) 编译成.class文件 --> 类加载器读取class文件 -->存放在java虚拟机中运行,
这只是一个简单程序运行流程,下面我们将的重点是.class文件如果存放在java虚拟机内部,具体存在在哪个位置。
1.1.1 java 虚拟机如何存放java程序
程序代码大概可以抽象成三个部分组成
- 数据 可以理解为定义的成员变量,静态变量,变量等…
- 指令 是指方法中的执行语句。a方法中调用B方法,B方法中调用C方法,数据可以传入指令中
- 控制流 可以理解为分支,循环,跳转。逻辑结构。
不同的东西就会存放不同的位置:大概的思路
静态变量,常量 -->方法区中
成员变量:如果是对象 -> 堆中
Native相关的 : -->本地方法栈中
我们只有每一个区域都理解很清楚,才知道具体该存放在哪个位置。
1.2 java虚拟机内部区域分析
1.2.1 程序计算器
- 在java内存中占用的非常小,几乎可以忽略不计,我们指内存的溢出等问题一般不会指这个域。
- 它指的是:指的是当前线程所执行的字节码行号指示器,类似指针地址,他和线程进行绑定的,线程与线程之后是独立的,互不影响。每个程序启动都是要有线程去执行。
举个例子(我一直认为例子是帮忙我们最快理解的最好途径)
我们启动main方法,就会启动一个线程值执行,这个时候绑定一个程序计数器,当程序加载编译的时候已经确定了整个代码的执行逻辑了或者执行顺序了。记录当前要执行哪条指令调用哪些方法,方法的位置在哪里就是类似指针位置,这个方法执行完毕后计数器就会发生改变,指向下一行代码,它可以看作一个执行逻辑顺序的指针有点类似于数据库的游标的概念。
1.2.2 虚拟机栈
- 从上图可知,虚拟机栈是线程独立的,多个线程有各种独立的虚拟机栈,不会存在线程安全问题。
- 虚拟机栈的生命周期和线程相同,在线程启动的时候被创建,在线程结束的时候被销毁。
- 虚拟机栈是用来存储java方法运行时的数据。 下面我们主要来讲这一块虚拟机栈如果存储数据的。
1.2.2.1 虚拟机栈如何存储数据
- 我们要想了解虚拟机栈如何存储数据的,首先要知道虚拟机栈的数据结构,这个数据单位称之为:栈帧。
- 同样举个例子来说明栈帧:
- 虚拟机栈就像袋子,里面都是糖,糖不是直接放在袋子里面的,糖有自己的包装,以这个包装的形式存在袋子里面。而这个包装就是栈帧。
- 下图是栈帧的结构图:
- 当程序执行一个方法时,会创建一个栈帧,称之为入栈,当方法结束的时候,栈帧就会销毁称之为出栈。
- 栈帧的内部结构 如上图栈结构图可知:
-
局部变量表: 用于存储局部变量的变量表,存放方法的参数,以及方法内部的局部变量
-
操作栈: 是一个先入后出,同局部变量表一样,操作数栈的最大深度在编译的时候写到方法的code属性的max_stacks数据项中,操作数栈可以理解为正在操作中需要处理的数据和结果。
- 用自己的白话理解为:
- 因为程序编译完成之后,进到方法栈的时候方法要被调用多少次在编译完成之后就知道了这个方法执行完要操作多少个指令。假如我们一个方法一共调用了五行代码,每一行代码都会涉及的数据(传进去的值也好 调用别的方法返回的值等…)
- 用自己的白话理解为:
-
动态链接: class文件中常量池有很多符号引用,这些符号有一部分讲在每次运行期转行为直接引用称之为动态连接。
- 调用常量池的时候是不是有名字在内存中是不是代表着符号,这些符号转行完之后就直接代表着数据了(有直接引用就会有渐进引用),直接引用就称之为动态连接(编译完成之后加载到内存中就可以直接使用了,而有一些需要编译完后解释运行到具体的代码中才知道具体的值是什么)
-
返回地址:记录改方法要返回到被调用的位置(返回原来的地址,一般通过地址进行记录)
- 举个例子:一个方法中调用3个其他方法,当把第一个方法执行完后要返回这个地址往下去执行,怎么返回就是通过方法的返回地址进行返回。
-
1.2.3 本地方法栈
- 本地方法栈与虚拟机栈所发挥的作用非常类似,他们之间的区别不过是虚拟机栈为虚拟机执行方法的字节码服务的,而本地方法栈则为虚拟机调用Native方法服务的。
- Native方法是指java调用c或者c++方法。
- 具体的执行流程是:本地方法栈—>调用本地方法接口—>通过走jni和c语言进行调用。(C语言存放在本地方法库里面)
- 同时需要注意的事情有的虚拟机(Hotspot)之间把本地的方法和 虚拟机栈合二为一,与虚拟机栈一样,本地方法区域也会抛出StackOverFlowError(栈内存溢出错误)和OutOfMenmoryError(内存溢出)异常
1.2.4 堆
- 基本的一些概念:java堆是java虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域(首先要想到线程安全问题),在虚拟机启动时创建。在内存区域中的位置目的就是存放对象的实例和数组,几乎所有的对象实例都在这里进行分配内存(static修饰的不在这里)
1.2.4.1 堆的内存结构
如下图所示
- 年轻代 大部分情况下java程序中刚创建的对象是从新生代分配内存的,存放在新生区中的伊甸区。同时S0和S1区内存大小是完全相同的,后续的文章中的垃圾回收机制中的复制算法会讲到这里为什么相同。在这里不详细讲解。
- 年老代 如果gc多次回收发现对象依然被引用无法回收会从伊甸区到s0到s1 最后会放入到老年代中。
- 持久代 存放静态文件,如java类,方法等
在这里没有对堆进行详细的讲解,放在后续的java虚拟机性能优化中详细讲解
1.2.5 方法区
- 方法区与java堆一样,是各有各线程共享的内存区域,它用于存储已经被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据(可以理解为.class文件存放的位置)。java虚拟机规范把方法区描述为堆的一个 逻辑部分,其实在一个区域,只是为和堆进行区分,方法区有一个别名叫非堆。
1.3 java 程序运行时java虚拟机的内存结构引用示例
代码如下所示:
/**
* @author: karl
* @date: 2019/4/14
* @Description: java程序运行时java虚拟机的内存结构引用示例
**/
public class Memory {
public static void main(String[] args) { //1
int i = 2; //2
Object obj = new Object(); // 3
Memory mem = new Memory(); // 4
mem.foo(obj); // 5
} //9
private void foo(Object param) { //6
String str = param.toString(); // 7
System.out.println(str); //8
}
}
- 文字描述如下所示:
- 第一行代码解释:一旦我们运行这个是程序时把类加载到堆空间,main方法就会入栈,就会在栈内存中创建一个栈帧同时也会有一个线程。
- 第二行:创建一个私有的本地变量,创建并且并且存储在main方法当中。
- 第三行:创建一个对象,堆内存存示例,栈内存存引用
- 第四行:创建一个对象,堆内存存示例,栈内存存引用
- 第五行:调用方法,方法入栈,创建栈帧。通过值得传递
- 第六行:在foo的堆栈 中创建一个 新对象引用,
- 第七行:创建字符串,存到创建堆的字符串的常量池,并且在foo的堆栈空间创建一个引用。
- 第八行:foo方法终止,这个时候堆的foo的内存也会释放,方法出栈
- 第九行:main方法终止,销毁main方法,
- 图示如下: