JVM的类加载机制和内存模型

虚拟机

所谓虚拟机(Virtual Machine)就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上虚拟机可以分为系统虚拟机和程序虚拟机。

系统虚拟机:玩Linux时装的VMware Fusion就属于系统虚拟机,它完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台

程序虚拟机:典型代表就是Java VM,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令我们称为Java字节码指令

Java虚拟机

定义

Java虚拟机是一台执行Java字节码的虚拟计算机,它拥有独立的运行机制,但凡符合其标准的字节码文件都可以在Java虚拟机上运行,并不是只局限于Java文件编译出来的字节码文件。这样便使得Java虚拟机特有的跨平台性,优秀的垃圾回收器和可靠的即时编译器可以被其平台的各种语言所共享。可以说Java虚拟机是整个Java技术的核心,因为所有的Java程序都运行在Java虚拟机内部。

作用

Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细的定义,如怎么取操作数,怎么处理操作树,处理结果放在哪里等。

特点

一次编译,到处运行

自动内存管理

自动垃圾回收管理

位置

架构

Java代码在JVM中的执行流程

生命周期

启动:Java虚拟机的启动是通过引导类加载器创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的

执行:程序开始运行时他就执行,程序结束它就停止,外在表现为一个叫做Java虚拟机的进程

退出:程序正常执行结束或者遇到了异常终止或者操作系统导致的Java虚拟机进程终止或者人为调用System类的exit方法或者是      Runtime类的halt方法都会导致jvm的退出

发展历程

当前比较流行的三大主流虚拟机分别是hotSpot,JRockit和j9;Sun Classic VM是世界上第一款商用的Java虚拟机,JDK1.4时完全淘汰;JDK1.3发布时,HotSpotVM正式发布并就此成为了Java的默认虚拟机,一直沿用至今。

 

类加载器子系统

概述

从jvm的架构图我们不难得知,jvm第一步做的是加载字节码文件,那么这一过程是如何实现的呢?

类加载器子系统负责从文件系统或者网络中加载Class文件,Class文件开头有特定的标识(CA FE BA BE);

ClassLoader只负责字节码文件的加载,至于它是否可以运行,由执行引擎(Exeuction Engine)决定

加载的类信息存放于一块称为方法区的内存空间。除了类信息外,方法区中还会存在运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

类的加载过程

jvm加载字节码文件主要分为上述三个过程:

加载:

       1.通过IO的方式将字节码文件转换成二进制字节流

       2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

       3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

链接:

      验证:验证字节码文件的字节流包含信息是否符合JVM的规范和要求,保证被加载类的正确性以及不会对JVM有什么影响

      准备:为类变量分配内存并且设置该类变量的默认初始值,即零值

public class Test{

   private static int a = 2 //在准备的阶段 a=0
}

但是只局限于类变量,如果是被final修饰的static,在编译的时候就会被分配和显式初始化
也不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中

      解析:符号引用转换成直接引用的一个过程,可以简单理解成通过常量字符引用变成指针引用,主要针对类,接口,字段,方法等

初始化:

      这个阶段就是执行类构造器方法<clinit>()的过程;

      这个方法不需要我们定义,是JavaC编译器自动收集类中的所有类变量的赋值动作静态代码块的语句合并而来

      这个方法中指令按语句在源文件中的出现顺序执行

public class Test1{

  static int a;   //没有赋值操作也没有静态块,jvm加载该类时就不会定义Clinit()方法

}


public class Test2{

 static num = 1


 static{
   num = 2;
   number = 20;
 }


 static number = 10;


public static void main(String[] args){

     System.out.println(num);//2
     
     System.out.println(number);//10

} 

}

类加载器的分类

jvm支持两种类型的加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)

划分的标准可能会不一样,但是常用的只有三个

需要注意的是,他们之间并不是上下级关系,更不是继承关系,而是一种包含关系

常见的获取类加载的方式,通常默认都系统加载器(System Class Loader)

双亲委派机制

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要该使用到该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式;

其工作原理主要为:

1.如果一个类加载器收到类类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;

2.如果父类的加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器;

3.如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载

优势

避免类的重复加载

保护程序安全,防止核心API被随意篡改

沙箱安全机制

沙箱安全机制主要是JVM出于安全的考虑,防止用户自定义核心API或者包的路径和核心的API包路径一致而导致程序报错制定的一种机制,比如说你自定义一个String类,且包名为java.lang,运行时就会报错,显示找不到main方法;

 

运行时数据区

运行时数据区总共分成5个部分,其中,方法区和堆区是线程共享的,程序计数器,虚拟机栈以及本地方法栈是线程私有的,即每个线程都有自己的一份。

程序计数器(PC寄存器)

程序计数器又叫PC寄存器,寄存器的命名源于CPU的寄存器,寄存器存储指令相关的信息,CPU只有把数据装载到寄存器才能执行;JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。它在Java虚拟机内部表现为一块很小的内存空间,小到几乎可以忽略不计,但是它的运行速度极快,在JVM规范中,PC寄存器是线程私有的,每个线程都有自己的一份,它的生命周期和线程的生命周期保持一致;它的作用就是存储当前线程正在执行方法的下一条要执行的指令,即将要执行的代码;PC寄存器中存储的指令供执行引擎读取和使用。

关于PC寄存器的两个问题

1.为什么使用PC寄存器记录当前线程的执行地址呢?或者说使用PC寄存器存储字节码指令地址有什么用呢?

因为CPU需要不停的切换各个线程,当线程切换回来的时候,需要知道接着从哪开始执行,JVM的字节码解释器通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

2.PC寄存器为什么要被设计成线程私有的呢?

如果是PC寄存器是共享的,那么在并发的情况下,指令就会一直处在一个被覆盖的状态,对于单个线程而言,就无法顺序执行当前方法的指令,所以,为了能够准确记录每个线程将要执行的字节码指令地址,最好的办法就是给每一个线程分配一个PC寄存器。

虚拟机栈

定义

java虚拟机栈是,早期也叫Java栈;每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈桢,对应着一个个的Java方法;是线程私有的,其生命周期和线程保持一致;它的作用就是主管Java程序的运行,它保存方法的局部变量,部分结果,并参与方法的调用和返回。

运行原理

JVM直接对Java栈的操作只有两个,就是对栈桢的压栈和出栈,遵循先进后出的原则;在一条活动线程中,一个时间点上,只会有一个活动的栈桢,即只有当前正在执行的栈桢是有效的,这个栈桢被称作当前栈桢,与当前栈桢相对应的方法称作当前方法,定义这个方法的类叫做当前类;执行引擎运行的所有字节码指令只针对当前栈进行操作;如果当前方法调用了其他方法,那么新的栈桢就会被创建出来并放在栈顶的位置,成为新的当前栈桢;

栈中可能出现的异常

Java虚拟机允许Java栈的大小是动态的或者固定不变的

1.如果采用的是固定不变的Java栈,那么,当线程请求分配的容量超过Java虚拟机允许的最大栈容量时,就会抛出一个StackOverFlowError异常;

2.如果采用的是动态扩展容量的Java栈,那么当线程在尝试扩展的时候却无法申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的Java栈时,就会抛出一个OutOfMemoryError异常

栈桢的内部结构

从宏观的角度来说每一个栈桢都对应着一个Java方法,但是从微观的角度我们又可以把一个栈桢细分为5个区域,如下图所示

局部变量表(local variables):

局部变量表也被称之为局部变量数组或本地变量表;是一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这类数据包括各类基本类型,引用类型以及返回返回类型(调用其他方法的返回值);局部变量表中的数据是线程的私有数据,不存在数据安全问题;且局部变量表的长度是在编译时就确定了的,在方法运行期间不会改变;

对于一个局部变量表,它的最小存储单元是Slot(变量槽);在它存储的所有数据类型中,32为以内的占据一个slot,64位的比如long,doubule占据两个slot;

JVM为局部变量表中的每一个Slot都分配一个访问索引,类比于数组的下标,通过这个索引就能访问到对应的局部变量的值;对于那些占据两个slot 的变量而言,它的访问索引等于其实索引;当一个方法被调用时,这个方法的参数以及它内部定义的局部变量会按照定义的先后顺序分配在局部变量表的每一个slot上;需要注意的事,当这个方法是实例方法或者构造方法时,它的局部变量表首先会将指向该对象本身的this变量先放在index为0的位置上,然后其余参数再按照顺序进行排列;而如果这个方法是一个静态方法时,则不会有this变量,这也是为什么在静态方法中不能使用this的原因;

操作树栈

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间;操作数栈是JVM执行引擎的一个工作区,当一个方法开始执行时,一个栈桢就会被创建,该栈桢中的操作数栈也就生成了,但是此时其是空的,其长度在编译时被确定;需要注意的是操作数栈并非通过索引的方式进行数据访问的,而是只能通过出标准的出栈入栈来完成一次数据的访问

动态链接

Java源文件被编译到字节码文件时,所有的变量和方法引用都作为符号引用(#123)保存在class文件的常量池里,而每一个栈桢又表示一个方法,动态链接里面存储的就是该方法在常量池里的地址信息;所以我们认为动态链接的作用就是将符号引用变成直接引用

方法返回地址

存放调用该方法的PC寄存器的值;一个方法无论是正常退出还是因为异常退出,都需要返回到该方法被调用时的位置,正常退出时,调用者的PC寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址;而通过异常退出的,栈桢一般不保存这部分信息

一些附加信息

栈桢中还允许携带与Java虚拟机实现相关的一些信息,例如对程序调试提供支持的一些信息

本地方法栈

Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用,这是两者唯一的区别,其他基本保持一致。

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取类类文件以后,需要把类,方法,常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆是运行时各线程共享的一块区域,其内部组成如下图所示:

新生区是是类的诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分成两大部分,伊甸区和幸存区;所有类都是在伊甸区被new出来的,当伊甸区内存不够时就会触发轻GC(Minor GC),未被GC销毁的对象就会被复制到幸存0区,当伊甸区再次出现内存告急,又一次触发GC时,GC此时就会扫描伊甸区和幸存0区,未被销毁的对象就会被复制到幸存1区;幸存区总有一块是空的,就是为了每次触发GC时,都能将幸存者复制到空的幸存区中去;如此反复交换15次后还能幸存的对象将被复制到养老区,这里的15次由JVM参数MaxTenuringThreshold决定,这个参数默认值为15;养老区如果内存告急,就会触发MajorGC(Full GC),如果MajorGC后对象依旧无法保存,就会抛出OOM异常。

方法区

方法区是用来存储类的结构信息,常量池,静态域,构造函数和普通方法的字节码内容等。这是一种规范,不同的虚拟机的实现方式是不一样的,最典型的实现方式是永久代和元空间。方法区是运行时各线程共享的一块区域,需要注意的是实例变量存储在堆内存中,和方法区无关。方法区存储的大部分都是JDK自身所携带的Class,Interface的元数据,这一部分数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。在JVM规范中,方法区被描述为堆的一个逻辑部分(永久代/元空间),但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。

 

执行引擎

通过类装载器装载的,被分配到JVM的运行时数据区的字节码会被执行引擎执行。其主要由3部分组成,分别是解释器,即时编译器(JIT)和垃圾回收器。解释器主要负责解释运行时数据区的字节码指令并执行,即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。当堆区间内存不足时,执行引擎会调用垃圾回收器进行处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值