Java 架构师 JVM 启动流程和内存结构,程序员必看!
JVM启动流程
JVM是Java程序运行的环境,同时是一个操作系统的一个应用程序进程,因此它有自己的生命周期,也有自己的代码和数据空间。
JVM工作原理和特点主要是指操作系统装入JVM,是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境。
1.创建JVM装载环境和配置
JVM装入环境,JVM提供的方式是操作系统的动态连接文件
2.装载JVM.dll
通过第一步已经找到了JVM的路径,Java通过LoadJavaVM来装入JVM.dll文件.装入工作很简单就是调用Windows API函数:
LoadLibrary装载JVM.dll动态连接库.然后把JVM.dll中的导出函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs挂接到InvocationFunctions变量的CreateJavaVM和GetDefaultJavaVMInitArgs函数指针变量上。JVM.dll的装载工作宣告完成。
3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例
这样就可以在Java中调用JVM的函数了.调用InvocationFunctions->CreateJavaVM也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例.
4.调用JNIEnv实例装载并处理class类。
JVM基本结构
· 程序计数器(PC寄存器)
PC(Program Couneter)寄存器是每个线程私有的,Java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个Java线程总是在执行一个方法,这个方法称为当前方法,如果当前方法不是本地方法,PC寄存器总会执行当前正在被执行的指令,如果是本地方法,则PC寄存器值为Underfined。每执行一条指令 PC 都会自增,因此 PC 存储了指向下一条要被执行的指令地址。JVM 用 PC 来跟踪指令执行的位置,PC 将实际上是指向方法区(Method Area)的一个内存地址。
· 方法区(永久代)
方法区存储了每个类的信息,比如类型的常量池、字段,方法信息、方法字节码。所有线程共享同一个方法区,因此访问方法区数据的和动态链接的进程必须线程安全。如果两个线程试图访问一个还未加载的类的字段或方法,必须只加载一次,而且两个线程必须等它加载完毕才能继续执行。
(JDK1.7中,已经把放在永久代的字符串常量池移到堆中。JDK1.8撤销永久代,引入元空间。元空间是直接存在内存中,不在java虚拟机中的,因此元空间依赖于内存大小。当然你也可以自定义元空间大小。)
· 方法区不需要连续的内存,可以选择固定大小或者可扩展。并且还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
· 堆(Heap)
应用系统对象都保存在Java堆中,堆被用来在运行时分配类实例、数组。不能在栈上存储数组和对象。因为栈帧被设计为创建以后无法调整大小。栈帧只存储指向堆中对象或数组的引用。与局部变量数组(每个栈帧中的)中的原始类型和引用类型不同,对象总是存储在堆上以便在方法结束时不会被移除。对象只能由垃圾回收器移除。所有线程共享Java堆。
简述垃圾回收
为了支持分代垃圾回收机制,堆内存可以划分为新生代和老年代两个区域(默认新生代与老年代的空间大小为1:2)。新生代可以再划分为Eden区、From Survivor区和To Survivor区(三者比例为8:1:1)。几乎所有的新对象的创建都是在Eden区进行的。在垃圾回收(GC)过程中,Eden中的活跃对象会被转移到Survivor区,当再到达一定的年龄(经历过的Minor GC的次数,每经过一次新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄后),会被转移到老年代中。
· Java栈(Java Stack)
Java栈是线程私有的内存区域,其中存储的是栈帧。,每一次方法调用创建一个帧,并压栈,调用完毕出栈。下面是内存的线程公有私有示意图:
如果方法methodOne方法调用了methodTwo,那么methodOne就会先入栈创建一个栈桢,接着methodTwo再入栈成为栈顶(假设没有其他的方法执行),methodTwo执行完先出栈,接着methodOne执行完出栈。
一般由三部分组成:局部变量表、操作数据栈和帧数据区
局部变量表:可以存放的数据有8种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用和returnAddress类型。其中long和double因为是64位,会占用两个局部变量的空间。
每一个块就是一个栈,如图是两个栈
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度(比如递归调用的y时候),将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
操作数栈:主要保存计算过程的中间结果,同时作为计算过程中的变量临时的存储空间。
下图是一个两数相加的操作数栈的过程:
帧数据区:除了局部变量表和操作数据栈以外,栈还需要一些数据来支持常量池的解析,这里帧数据区保存着访问常量池的指针,方便计程序访问常量池,另外当函数返回或出现异常时卖虚拟机子必须有一个异常处理表,方便发送异常的时候找到异常的代码,因此异常处理表也是帧数据区的一部分。
栈上分配
小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
直接分配在栈上,可以自动回收,减轻GC压力
大对象或者逃逸对象无法栈上分配
· 本地方法栈(Native Method Stack)
本地方法栈也是线程私有的内存区域,与java栈比较相似,不同之处在于该区域主要是保存Native方法相关的数据。Native方法是非Java语言编写的方法。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
评论