—-慢慢来比较快,虚心学技术—-
JVM内部结构体系
JVM内部结构体系分为如下内容:
类装载器子系统(ClassLoader)
用来加载 Java 类到 JVM中。负责读取java字节码文件(.class),并转换成java.lang.Class类的一个实例,每个这样的实例用来代表一个java类,通过该实例的newInstance()方法可以创建出该java类的一个对象
JVM中有两种类加载器:启动类加载器和自定义类加载器。启动类加载器是指JVM实现的部分,而自定义类加载器则是java代码的一部分,且必须是ClassLoader类的子类
JVM将整个类加载过程分成三个步骤:
一、装载
将字节码文件(.class)中的二进制数据读取到内存中,并分配不同数据区。如果有继承,则先装载父类
二、链接
对二进制字节码的格式进行校验、解析类中的调用的接口、类。校验是为了确认class文件的合法性,解析是对类中的所有属性,调用方法进行解析,以确保其需要调用的属性和方法都是存在的且具备调用权限(修饰符private/protect/public等)
三、初始化
执行类中的静态初始化代码,构造器代码以及静态属性的初始化
运行时数据区(Runtime Data Area)
JVM在执行java程序的过程中,从计算机内存中开辟一块内存存储JVM需要用到的对象、变量等,该内存区域分为:方法区、堆、虚拟机栈、程序计数器、本地方法栈。该内存就是运行时数据区
执行引擎(Execution Engine)
负责执行被装载类中包含的指令。执行引擎包括如下三部分:
解释器:更快地解释字节码,但是执行缓慢(方法重复调用会重复创建新的解释)
JIT编译器:辅助解释器,执行引擎在转换字节码时使用解释器,而遇到重复调用的方法代码时直接使用JIT编译器,将整个字节码编译为本地代码,后续调用将直接使用本地代码,提高性能
垃圾回收器(GC):收集和删除未引用的对象,释放内存空间
本地库接口(Native Interface)
提供一个标准的方式让Java程序通过虚拟机和原生代码进行交互,即java本地接口(Java Native Interface,JNI),其作用是使得JVM内部运行的java程序可以与用其他语言(如c/c++/汇编语言)编写的应用程序和库进行交互。
本地方法库(Native Method Liraries)
执行引擎所需的本机库集合,一个本地方法指的是一个java调用非java编写的接口,该方法的实现由非java语言实现,如C或汇编语言等
JVM运行时数据区内存模型
**接触过JVM的人估计对下图十分熟悉:
转而具体模型如下:
**
jdk1.6的jvm运行时数据区内存模型
然而,上述模型是jdk1.8之前的JVM模型,jdk1.8之后的模型些许不同,将方法区去除并归入直接内存,另名为:元空间区
jdk1.8的jvm运行时数据区内存模型
由上内容可知,运行时数据区内按线程共享性分为如下两类
-线程共享
堆
方法区
-线程独享
程序计数器
本地方法栈
虚拟机栈
那么,这些数据区域的作用都是什么呢?
一、程序计数器(Program Counter Register)
描述**:程序计数器是一块较小的内存空间,可以视为当前线程所执行的字节码的指令行号指示器**
作用**:**
1、JVM的概念模型里面,字节码解释器的工作就是通过改变计数器的值来选取下一条需要执行的字节码指令,比如分支、循环、跳转、异常处理等的逻辑顺序处理。
2、JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,在任意一个确定的时间,一个处理器都会执行一条线程中的指令,而各线程切换之间,为了保证指令顺序的正确,每个线程都会具备自己的程序计数器,这样就可以保证独立记录本线程执行到的指令行号而不会错乱
程序计数器模型概念
生命周期**:随线程创建而创建,销毁而销毁**
二、虚拟机栈(Java Virtual Machine Stacks)
描述**:**虚拟机栈存储当前线程运行方法所需要的数据、指令、返回地址。描述的是java方法执行的内存模型,每一个方法从调用直至执行完成,都对应有一个栈帧从虚拟机栈中从入栈到出栈(FILO)。
栈帧**:**每个方法在执行的同时会创建一个栈帧,保存着与所属方法有关的信息,如局部变量表、操作数栈、帧数据区、动态链接、方法出口等信息。某种意义来说,一个栈帧代表着一个方法。
局部变量表**:用于存放方法参数和方法内部定义的局部变量,是一个索引从0开始的数字数组**。该数组存放了编译器可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(reference类型,不是对象本身,而可能是指向对象的地址等),returnAddress类型(指向一条字节码指令地址,类似占位符)
操作数栈**:作为运行时工作区执行操作,被定义为一个字长为单位的数组。与局部变量表(以索引访问)不同的是,操作数栈是标准的栈操作,即出栈和压栈。虚拟机将操作数栈作为它的工作区,也就是说大部分指令都要从这里弹出数据,执行运算,然后将结果压回栈中**。如某个指令需要从操作数栈中弹出两个整数,执行完运算后将结果又押回栈中。
帧数据区**:与方法相对应的所有符号存储再此。**
动态链接**:**每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有该引用是为了支持方法调用(不涉及方法执行,仅仅是调用方法这一个动作)过程中的动态链接。
方法出口地址**:指向方法退出前往的地址。一个方法开始执行后,只有两种方式可以退出:一种是方法正常执行完毕(即遇到方法返回的字节码),一种是遇见异常且未在方法体内做处理。
实际上,无论采用何种方式退出,在退出方法后都要返回到方法被调用的位置,程序才能够继续运行,所以方法返回时需要栈帧中保存一些东西来帮助它恢复上层方法的执行状态。
一般来说,方法正常退出时,调用者的程序计数器的值可以作为返回地址,则栈帧中方法出口地址指向该程序计数器的值;当方法异常退出时,栈帧不保存信息辅助,而是通过异常处理器表来确定返回地址。
故而,虚拟机栈的组成如下:
作用**:虚拟机栈的大小直接决定了函数调用的可达深度(使用-XSS参数配置虚拟机栈大小)
如下递归函数代码,明显当提高了虚拟机栈的大小(调大-XSS)后,递归方法的递归深度显著加深(最后抛错时count变得更大)**:
public class Test{
private int count=0;
//没有出口的递归方法
private void count(){
count++;
count()
}
@Test
public void test(){
try{
count();
}catch(Exception e){
//打印栈的深度
System.out.println("栈深度:"+count);
e.printStackTrace();
}
}
}
生命周期**:随线程创建而创建,销毁而销毁**
注:java虚拟机栈会出现两种异常:StackOverFlowError 和OutOfMemoryError**(OOM)
StackOverFlowError **: 若java虚拟机栈不允许动态扩展,则当线程请求虚拟机栈的深度超过虚拟机最大深度**时,抛出StackOverFlowError 异常
OutOfMemoryError(OOM)**: 若java虚拟机栈允许动态扩展,则当线程请求虚拟机栈时内存用完**了,无法再动态扩展时,抛出OutOfMemoryError异常
三、本地方法栈
描述:本地方法栈是为JVM运行native方法准备的空间。描述的是Native方法执行的内存模型。其特性与java虚拟机栈类似。
作用**:与java虚拟机栈类似
生命周期:随线程创建而创建,销毁而销毁
注:所谓native方法指的是使用native关键词修饰的方法,这种方法实际上并不由java代码实现,而是由运行java程序的机器本地代码(非java语言,如:xx.dll)实现。也就是说本地方法是提供给java调用调用非java代码的接口。
public class Thread{
.....
public static native Thread currentThread();
.....
private native void start0();
.....
}
四、堆
描述:存放java程序中的对象实例和数组值的内存空间,堆中的对象内存都需要等待GC回收。
作用:用于存放对象实例和数组值,可以认为java中所有通过new创建的对象的内存都在此分配。
生命周期:虚拟机启动的时候进行创建,虚拟机关闭的时候销毁,其内的对象内存等待GC回收
注:
1、堆的大小既可以固定也可以扩展,对于主流虚拟机来说,堆的大小是可扩展的,因此当线程中请求分配内存,但堆已满,且内存无法再扩展时,将抛出OutOfMemoryError(OOM)异常。
2、堆的大小通过-Xms和-Xmx控制:
-Xms:堆内存初始大小,单位m、g;
-Xmx:堆内存最大允许大小,一般不要大于物理内存的80%
3、java堆的所使用的内存是不连续的,仅需要保证其逻辑的连续性,但是堆又是被所有线程共享的,所以对它的访问要注意同步一致性,也就是说,对堆上进行对象内存分配都需要进行加锁,这就是new对象开销较大的原因
4、堆内存模型:堆内存分为年轻代(Young Generation)和老年代(Old Generation),其中,年轻代又分为生成区(Eden)和幸存区(Survivor),幸存区由FromSpace(S0)和ToSpace(S1)组成
大部分对象都存活在Eden区(生成区)中生成 当Eden区满了之后,将经过几次(通过参数配置)minor gc还存活的对象复制到Survivor区(幸存区)的FromSpace(S0)中 再经过几次(通过参数配置)minor gc还存活的话进入ToSpace(S1)中 当达到minor gc的阈值之后进入老年代
也有可能大对象直接进入老年代
所以,换句话说,在年轻代中经过GC后还存活的对象会被复制到老年代中,当老年代空间不足,JVM会对老年代进行完全垃圾回收(Full GC),如果GC后依旧无法存放从Survivor区复制过来的对象,就会出现OutOfMemoryError(OOM)异常
五、方法区(jdk1.8后已被移除,成为元空间)
描述:定义存放已被虚拟机加载的类信息,常量,静态常量,即时编译器编译后的代码等数据的一种规范,不是实现。方法区的实现被称为“永久代”(实际上“元空间”)。
作用:用于存放class、运行时常量池、字段、方法、代码、JIT代码等
生命周期:线程共享,虚拟机启动的时候进行创建,虚拟机关闭的时候销毁,其内存可被GC回收也允许不被回收(jdk1.8变更为元空间(属于直接内存,不属于jvm内存),所以生命周期是永久的)
注:1、方法区中的信息一般需要长期存在,属于堆的一个逻辑部分。
2、方法区的内存回收效率低,回收一遍之后可能仅有少量信息无效,主要回收目标是:对常量池的回收,对类型的卸载
3、当方法区无法满足内存分配需求时,将抛出OutOfMemoryError(OOM)异常。比如不断往运行时常量池加入数据
4、方法区的结构如下:
其中,值得注意的是常量池的概念:
常量池分为两种,一种是静态常量池(也就是上图中的类型常量池),一种是运行时常量池
静态常量池(.class文件中的常量池):存放字面量及符号引用量
1、字面量: 文本字符串、被声明为final的常量值、基本数据类型的值等
2、符号引用量: 类和结构的完全限定名、字段名称和描述符、方法名称和描述符等
所谓符号引用量,是因为编译时,编译器并不知道java类中所引用的类、字段以及方法的实际地址,只能通过符号(CONSTANT_Class_info、CONSTANT_Fieldref_info 、CONSTANT_Methodref_info结构体形式)来标识,当所引用的类被加载后,符号引用量就会变成直接引用量用以定位到目标。
运行时常量池:当JVM完成类装载的操作后,将.class的常量池载入到内存
运行时数据区作用图示
假设现有一java类:
public class Test{
public static final Integer CONSTANT = 100;
public int add(){
int a = 1;
int b = 2;
int c = (a+b)*10;
return c;
}
public static void main(String args[]){
Test test = Test();
test.add();
}
}
更为具体的类加载与JVM运行时数据区细节图:
①Test类运行时创建main线程,同时创建main方法的栈帧,其局部变量表存放的是局部变量test的引用地址(因为test是个对象),该引用地址指向堆内的test实例(关联栈与堆)
②调用test实例的add方法时,创建add方法的栈帧
③将局部变量a的值1放入操作数栈内,然后将局部变量a存入局部变量表并同时将操作数栈内的1弹出,故局部变量表中有a=1的内容且操作数栈内的a的值已经弹出不再存在。局部变量b同理;
④将局部变量a与局部变量b的值压入操作数栈内,并把操作符压入栈内,计算出结果3,而后将10压入栈内,计算结果等于30,将局部变量c存入局部变量表并同时将操作数栈内的30弹出,故局部变量表中有c=30的内容
⑤add方法栈帧的方法出口指向main方法的栈帧地址。
⑥实际上,当线程内存在多个Test实例时,如何知道该调用的是哪个实例的add()方法呢?这时候就用到了方法区中的方法信息,或称静态链接,而所谓动态链接就是为每个实例的add方法提供一个指向静态链接的地址,从而实现独立调用。(关联栈和方法区)
⑦堆中的test实例对象头信息关联方法区的Test.class类元信息(关联方法区和堆)
如有贻误,还请评论指正