欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java内存区域与虚拟机类加载机制

程序员文章站 2022-05-21 07:53:11
本文主要对Java运行时数据区域、对象的创建过程、对象的内存布局、对象的访问定位、虚拟机类的加载机制进行简单的介绍梳理。 ......

一、Java运行时数据区域 

Java内存区域与虚拟机类加载机制

1、程序计数器

  “线程私有”的内存,是一个较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器。Java虚拟机规范中唯一一个没有OutOfMemoryError情况的区域。

  字节码解释器工作时就说通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

2、Java虚拟机栈

  Java 虚拟栈,线程私有的,它的生命周期与线程相同。每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

  通过人们所说的“栈”就说虚拟机栈,或说是虚拟机栈中的局部变量表部分。

  • 局部变量表存放了编译期可知的各种基本数据类型(bloolean,byte,char,short,int,float,long,double),对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译器间完成分配,当进入一个方法是,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  • 一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression Stack)。操作数栈和局部变量表在访问方式上存在着较大差异,操作数栈并非采用访问索引的方式来进行数据访问的,而是通过标准的入栈和出栈操作来完成一次数据访问。每一个操作数栈都会拥有一个明确的栈深度用于存储数值,一个32bit的数值可以用一个单位的栈深度来存储,而2个单位的栈深度则可以保存一个64bit的数值,当然操作数栈所需的容量大小在编译期就可以被完全确定下来,并保存在方法的Code属性中。  

  这个区域有两个异常:
     ① 如果线程请求的栈深度大于虚拟所允许的深度,将抛*Error异常;
     ② 虚拟机栈可以动态扩展,但扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

3、本地方法栈

  本地方法栈作用与虚拟机栈相似,区别在于虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务,线程私有
  Native方法常用的两种请求:
    ① 在方法中调用一些不是有java语言写的代码;
    ② 在方法中用java语言直接操作计算机硬件;
  异常:*Error、OutOfMemoryError

4、Java堆(Java Heap)

  Java堆是Java虚拟机所管理的内存中最大的一块。在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
  Java堆是垃圾收集器管理的主要区域,因此也称为“GC堆”;
  如果在堆中没有内存完成实例分配,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。

5、方法区

  方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

  • 运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存储编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
  • 运行时常量池相对于Class文件常量池的另外一个特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用的比较多的便是String类的intern()方法。

 二、对象的创建

  1、虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过,如果没有则必须执行相应的类加载过程。

  2、在类加载检查通过后,接下来虚拟机将新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划出来。

  3、内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。

  4、虚拟机要对对象进行必要的设置,主要针对对象头的设置。

    • 对象头:
      • 第一部分,用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。(Mark Word)  
      • 第二部分,类性指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

  5、从虚拟机的角度来看,一个新的对象已经产生了,但从Java查询的视角来看,对象创建才刚刚开始—— <init>方法还没执行,所有的字段都还为零。

     执行完new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化。这样一个真正的对象才算完全产生出来。

三、对象的内存布局

  对象在内存中存储的布局可以分为3块局域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

  实例数据:对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

  对齐填充:并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

四、对象的访问定位

  建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。

  1、如果使用句柄访问的话,那么Java堆中将会划出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型各自的具体地址信息。

Java内存区域与虚拟机类加载机制

  2、如果通过直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象的地址。(Sun HotSpot的实现方式)

Java内存区域与虚拟机类加载机制

 

五、虚拟机类的加载机制

  (一)、类加载的时机

  1、类的生命周期:类从被加载到虚拟机内存中开始,到卸载出内存为止。

Java内存区域与虚拟机类加载机制

 

  图中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段则不一定。

  2、需立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)的有且只有的5种请求:

    ① 遇到new、gerstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。

    如:使用new关键字实例化对象时候,读取或设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

    ②使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

    ③当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

    ④当虚拟机启动时,用户需要指定一个要执行的主类(包括main()方法的那个类),虚拟机会先初始化这个主流。

    ⑤当使用JDK1.7的动态语言支持时。

  (二)、类加载的过程

  类加载的全过程:加载、验证、准备、解析和初始化这五个阶段。  

  1、加载

  “加载”是“类加载”过程的一个阶段。 

   ① 通过一个类的全限定名来获取定义此类的二进制字节流(通过类加载实现);

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

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

   2、验证

  连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  大致完成4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

  3、准备

  正式为变量分配内存并设置类变量初始值得阶段,这些变量所使用的内存都在方法区中进行分配。首先这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中,其次,这里所说的“初始值”通常情况下是数据类型的零值。

  4、解析

  解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

    • 符号引用:符号应用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时无歧义地定位到目标即可,与虚拟机实现的内存布局无关,引用的目标并不一定以及加载到内存中。
    • 直接引用:直接引用可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的。引用的目标必定已存在于内存中。

  在16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所有虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用之前才去解析它。

  解析动作主要针对类或接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用进行解析。

  5、初始化  

  初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。

  在准备阶段,变量已经赋过一次系统要求的初始值,而在初始阶段,则根据查询员通过查询制定的主观计划去初始化变量和其他资源,换而言之,初始化阶段是执行类类构造器<client>()方法的过程。

  在<client>()方法中,静态语句块中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,在前面的静态语句块可以赋值,但不能访问。

六、类加载器

  类加载器是类加载过程中加载阶段中“通过一个类的全限定名来获取描述此类的二加载字节流”的加载动作。

  主要分为启动类加载器(C++语言实现,是虚拟机自身的一部分)、扩展类加载器、应用程序类加载器,后面两类加载器由Java语言实现,独立于虚拟机外部,并全部继承自抽象类java.lang.Loader。

  其加载顺序的实现为双亲委托派模型,如下图所示:

Java内存区域与虚拟机类加载机制