深入理解java虚拟机

来源:互联网 发布:linux中vim退出 编辑:程序博客网 时间:2024/05/01 22:29

Java内存区域与内存溢出异常

运行时数据区域


程序计数器
每个线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,为“线程私有”的内存。
如果线程正在执行一个Java方法,计数器是正在执行的虚拟机字节码指令的地址;如果是Natvie方法,计数值为空。

Java虚拟机栈
也是线程私有,生命周期与线程相同。每个方法被执行都会创建一个栈帧用于存储局部变量表、操作栈、动态连接、方法出口等信息。
局部变量表存了编译期可知的各种基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址),方法运行期间不会改变局部变量表的大小

本地方法栈
为虚拟机使用Native方法服务

Java堆
被所有线程共享的一块内存区域,用于存放对象实例。可以处于物理上不连续的内存空间中,只要逻辑连续即可

方法区
各线程共享的内存区域,存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
String.intern()这个Native方法想运行时常量池中添加内容,如果池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象,否则,将Sting对象半酣的字符串添加到常量池中,并返回此String对象的引用

直接内存
并不是虚拟机运行时数据区的一部分。NIO引入的通道和缓冲区,使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作。能避免Java堆和Native堆中来回复制数据。直接内存分配不会收到Java堆大小的限制。

对象访问

对于在方法体中的  Object obj = new Object(); Object obj在Java栈本地变量表中,作为一个reference类型数据出现。new Object()在Java堆中形成一块存储了Object类型所有实例数据值的结构化内存。Java堆中包含能查找到此对象类型数据(对象类型、父类、实现的接口、方法等)的地址信息,这些类型的数据存储在方法区中。


句柄访问方式,Java堆中将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息

直接指针访问方式中Java堆对象的布局中就必须考虑如何防止访问类型数据的相关信息,reference中直接存储的就是对象地址。


句柄方式在对象被移动时只改变句柄中的实例数据指针,而reference本身不需要被就该;直接指针方式速度快,由于对象的访问非常频繁,可以节省执行成本。Java虚拟机采用的是直接指针访问对象方式

虚拟机类加载机制

概述

Java的类型加载和链接都是在程序运行期间完成的。

类加载的时机

类的生命周期:加载、验证、准备、解析、初始化、使用和卸载。


解析有可能在初始化之后开始。

必须对类进行“初始化”的四种情况:
1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则要先触发其初始化。
2.使用java.lang.reflect包的方法对类进行反射调用的时候。
3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先出发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

对于静态字段,只有直接定义这个字段的类才会被初始化,通过其子类来引用父类中定义的静态字段,只会出发父类的初始化而不会出发子类的初始化。
定义数组时,并不会加载数组元素对应的元素类型,只会生成一个元素类型为指定类型的数组类
public class ConstClass {    static {            System.out.println("ConstClass init!");    }    public static final String HelloWorld = "hello world";}public class NotInitialization {    public static void main(String[] args) {        System.out.println(ConstClass.HelloWorld);    }}
编译简短ConstClass类中的常量HelloWorld存储到NotInitialization类的常量池中,对常量ConstClass.HW的引用被转化为Notinitialization类对自身常量池的引用。不会加载ConstClass类


接口与类加载有所区别:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其付接口全部都完成了初始化,只有在真正使用到付接口的时候才会初始化

类加载的过程

即加载、验证、准备、解析和初始化这五个阶段

加载,虚拟机完成三件事:通过一个类的全限定名来获取定义此类的二进制字节流;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
Class对象最为程序访问方法区中的这些类型数据的外部接口。加载与链接交叉进行,但夹在加载阶段之中进行的动作任然属于链接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

验证,文件格式验证:字节流是否符合Class文件格式的规范,保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求;元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;字节码验证:进行数据流和控制流分析。对类的方法体进行校验分析。保证被校验类的方法在运行时不会作出危害虚拟机安全的行为;符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,在解析阶段中发生,可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性的校验。

准备,正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。初始值“通常情况”下是数据类型的零值。public static int value = 123;准备阶段的初始值为0而不是123,把value复制为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,把value赋值为123将在初始化阶段才会被执行。如果类字段属性表中存在ConstantValue属性,那在和尊卑阶段变量value就会被初始化为ConstantValue属性所指定的值,public static final int value = 123;准备阶段value就会被赋值为123.

解析,虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,不一定已经加载到内存中;直接引用:可以是直接指向目标的指针、相对偏移量或是一个能简介定位到目标的句柄。引用的目标必定已经在内存中存在。
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应常量池的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info四种常量类型。
类或接口的解析
字段解析,注:如果同一个字段同时出现在C的接口和父类中,或者同时在自己或父类的多个接口中出现,那编译器将可能拒绝编译。
类方法解析,本类中查找-》父类中查找-》接口中查找
接口方法解析,本接口-》父接口(直到Object类)

初始化,初始化阶段才真正开始执行类中定义的Java程序代码。
执行类构造器<clinit>()方法的过程,该方法有编译器自动收集类中的所有类变量的赋值动作和静态语句快中的语句合并产生的,编译器手机的顺序是由语句在源文件中出现的顺序所决定的,静态语句快中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。
<clint>()方法与类的构造函数不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clint>()方法的类肯定是Object
由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以部位这个类生成<clinit>()方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,执行接口的<clinit>()方法不需要先执行付接口的<clinit>()方法,只有当父接口中定义的变量被使用时,付接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步。

类加载器

类与类加载器,任意一个类,都需要由它的类加载器和这个累本身一桶确定骑在Java虚拟机中的唯一性,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类是来源于同一个Class文件,只要加载它们的类加载器不同,那么这两个类就必定不相等。

双亲委派模型
站在Java虚拟机的角度,只存在启动累加载器(C++实现,是虚拟机自身的一部分)和其他的类加载器(由Java预言实现,独立于虚拟机外部,继承自抽象类ClassLoader)
更细分:
启动类加载器,无法被Java程序直接调用,加载lib目录下的类库。
扩展类加载器,加载lib\ext目录中或者java.ext.dirs系统变量所指定的路径中的所有类库。
应用程序类加载器,也称系统类加载器,负责加载用户类路劲上所指定的类库,一般作为程序中默认的类加载器。
除顶层的启动类加载器外,其余的类加载器都应当自己的父类加载器。一般不会以继承的关系来实现,而都是使用组合关系复用父加载器。
双亲委派模型工作过程:如果一个类加载器受到了类加载的请求,先将这个请求委派给父类加载器,父加载器无法加载才尝试自己加载。

破坏双亲委派模型,JDK 1.2之后只要覆盖findClass方法,就可以保证新写出来的类加载器符合双亲委派规则。三次破坏


虚拟机字节码执行引擎

运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中虚拟机栈的栈元素。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址和一些额外的附加信息。栈帧大小和操作数栈在编译时就确定,需要分配的内存不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。


局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
局部变量表的容量以变量槽为最小单位。局部变量表是线程私有的。

操作数栈

动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这是静态解析。另外一部分将在每一次的运行期间转化为直接引用,这部分称为动态链接。

方法返回地址
方式一:遇到方法返回字节码指令;方法二:发生异常,并且没有在方法体内得到处理。
方法退出后都要返回到方法被调用的位置,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。
方法退出等同于当前栈帧出栈,因此退出时可能执行的操作有:回复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

方法调用

方法调用不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本。Class文件的编译过程中不包含传统便一种的链接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用)

解析
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会降其中的一部分符号引用转化为直接引用。
在Java中符合“编译期可知,运行期不可变”这个要求的方法主要有静态方法和私有方法两大类,前者与类型直接关联,后再在外部不可被访问,这两种方法都不可能通过继承或别的方式重写出其他版本,适合在类加载阶段进行解析。

分派
静态分派,典型的例子是方法的重载,编译时确定执行方法的版本。参数类型可按char->int->long->float->double->装箱->接口->父类->变长参数转型。
解析与分派并不是排他关系,它们是在不同层次上去筛选和确定目标方法的过程。静态方法会在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程是通过静态分派完成的。

动态分派,和重写有密切的关系。父类引用子类对象,调用重写的方法时,是优先查找子类中的方法,当子类中没有时再往父类中查找。

单分派与多分派,方法的接收者与方法的参数统称为方法的宗量。但分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个的宗量对目标方法进行选择。
静态多分派、动态但分派



原创粉丝点击