JVM虚拟机学习-精简版

2019-10-08  本文已影响0人  Skybike

JVM虚拟机学习

Jdk:java developmentkit

Jre:java runtime environment

1:自动内存管理机制

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

Java的内存区域结构如下

程序计数器:

1:为了应对多线程运行环境,每个线程都会有独立一个程序计数器,记录自己执行到哪个指令,某线程执行完了一条语句,会改变自己的程序计数器的值来指向下一条指令。

每个线程的程序计数器之间互不影响,独立存储,为线程私有的内存

2:如果线程在执行一个java方法,则程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是native方法,这个计数器值为空。

       3:此内存区域是虚拟机中唯一一个没有OutOfMemoryError情况的区域

Java虚拟机栈

1:Java虚拟机栈是线程私有的,他的生命周期和线程相同。即启动一个线程时,会在该线程内部启动一个虚拟机栈,用来管理这个线程执行过的所有方法。

2:每一个方法在调用时会创建一个栈帧,将这个栈帧放入虚拟机栈中,等待方法执行完毕后,将这个栈帧进行出栈。

3:一个栈帧中存放的有:局部变量表,动态链接(???),操作数栈(???),方法出口等信息

4:局部变量表存放了编译期可知的所有基本数据类型(boolean,byte,char,short,int,long,float,double),对象引用(reference类型,但不等同于对象本身,可能是一个指向对象起始地址的引用指针)和returnAddress类型(执行了一条字节码指令的地址)

5:如果线程请求的栈深度大于虚拟机栈的深度,将抛出stackoverflow异常

6:如果虚拟机站可以动态扩展,且扩展时无法申请到足够的内存,就会抛出OutOfMemoryErrory异常

本地方法栈:

1:本地方法栈和java虚拟机栈是差不多的,只不过本地方法栈的作用对象是native方法,java虚拟机栈的作用对象是java方法。

2:和虚拟机栈相同的是,本地方法栈区域也会抛出StackoverFlow和OutOfMemoryError异常。

Java堆—垃圾收集器管理的主要区域

1:在虚拟机启动时创建

2:被所有线程共享的一块内存区域

3: 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

4:从内存回收的角度来说,java堆可细分为新生代和老年代,再细致一点可分为,Eden空间,From Survivor空间,To Survivor空间

5:从内存分配角度来看,线程共享的java堆可能划分出多个线程私有的分配缓冲区。

6:java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的就行

方法区

1:是每个线程共享的内存区域。

2:存储应用启动时加载的相关类信息,常量,静态变量,即时编译器编译后的代码。

       3:这个区域的垃圾回收目标主要是针对常量池的回收和对类型的卸载(也是垃圾回收左右的范围之一)

直接内存:

       1:是在虚拟机对内存之外,但属于机器内存的一块经常使用的内存

       2:使用native函数库直接分配堆外内存,再通过java堆中的DirectByteBuffer来操作这块内存

运行时常量池

1:运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

Java堆

对象的创建:

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

类加载检查通过后,这时对象所需要分配的空间就可以完全确定下来,虚拟机按照这个大小为新生对象分配内存空间(堆内存空间)

接下来,虚拟机要对对象进行必要的设置,比如如何确定这是那个对象的实例,如何才能找到元数据信息,对象的hash码,对象的GC分代年龄等信息,这些信息存放在信息头中。

上面过程做完了之后,其实只是将这个对象与类进行关联,分配内存等。还没对这个对象进行初始化,

对象的内存布局:

对象在内存中的存储布局分为三个部分:对象头(Header),实例数据(Instance Data),对齐填充(Padding)

对象头:

              1:用于存储对象自身的运行时数据,称之为MarkWord(用于在对象锁处理时重点关注的地方),记录诸如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程的ID等等

              2:用于存储类型指针,即对象指向它的类元数据的指针,指明它属于哪一个类的实例对象

              3:如果对象是一个数组,那么对象头中还必须有一块用于记录数组长度的数据,因为虚拟机需要从普通java对象的元数据信息确定数组大小,但是从数组元数据中无法获取数组大小信息

 实例数据:

              1:存储对象的信息,比如某个字段属性的实际值,无论是父类继承下来的还是子类中自己定义的,都需要记录

 对齐填充:

              1:没有特殊意义,仅起到占位符的作用。原因是因为HotSpot要求对象起始地址必须是8字节的整数倍,如果实例数据部分的数据不够8字节的整数倍,那么就需要对齐填充来完成

对象的访问定位:

       通过java虚拟机栈中存储的reference类型来确定调用对象的地址

              1:如果使用句柄访问的话,那么java堆中会另外开辟出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中存储对象实例数据和对象所属类元数据各自的地址信息,如图:

       2:如果使用直接指针访问,reference中存储的是直接对象地址,包括实例数据部分和实例所属类元数据的地址信息,如图

垃圾收集器与内存分配策略

如何判定对象是否存活?

引用计数算法:

       1:原理:给对象添加一个引用计数器,但有一个对象引用它时,就加一,当引用失效时,就减一,任何时刻计数器的值为0时,该对象就不会再被引用

       2:优点:实现简单,判定效率高

       3:缺点:很难解决对象之间循环引用的问题

可达性分析算法:

从一系列GC ROOT为初始节点开始向下搜索,节点之间的直线成为引用链,当一个对象到这些GC ROOT之间没有任何引用链时,则证明此对象是不可用的。如图:

可作为GC

ROOT的对象包括以下几种:

1:虚拟机栈中局部变量表中引用的对象(reference类型)

2:方法区中静态属性引用的对象

3:方法区中常量引用的对象

4:本地方法栈中Native方法引用的对象

四种引用类型:

强引用:类似 Object obj = new Object();只要强引用还在,就不会被回收

软引用:描述一些还有用但非必需的对象,在系统将要发生内存溢出之前,将会把对象列入回收范围之内进行第二次回收,如果这次回收还没有足够的内存,则会抛出内存溢出异常。

提供SoftReference类来实现软引用

弱引用:描述非必需的对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当开始进行垃圾回收时,无论当前内存是否足够,都会回收弱引用的对象。提供WeakReference类来实现弱引用

虚引用:最弱的一种引用,一个对象是否有虚引用存在,完全不会影响其生存时间,也无法通过虚引用来取得一个对象引用,提供PhantomReference引用

回收方法区:(HotSpot虚拟机中的永久代)

主要包括回收废弃常量和无用的类

判定废弃常量的标准是某一个常量进入了常量池,但是没有任何一个对象是这个常量的引用,则可以将这个常量废弃

判定无用的类的标准:

       1:该类的所有实例对象都已被回收

       2:该类的ClassLoader类已经被回收,如果不自定义ClassLoader类,那么系统中所有的类都是“有用”类

       3:没有任何地方有调用这个类的地方,甚至不能通过反射机制访问该类的方法

垃圾收集算法:

标记--清除算法

前提(没有跟GC ROOT相关联的对象并不会被立即回收,会经历两次回收动作,第一次会被打上标记(对象没有覆盖finalize()方法,或者finalize方法已经被虚拟机调用过),第二次会被回收)

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

缺点:一是效率问题,标记和清除两个过程的效率都不高

另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作

复制算法:

将内存按容量划分为大小相同的两块,每次只是使用其中的一块,当这块的内存用完了,就将还存活着的对象复制到另一块上去,然后再把已使用过的内存空间一次清理掉,然后吧剩余的不回收的内存块,移动堆顶指针按顺序分配内存。

缺点在于每次就只用一半的内存。

应用场景:

新生代中98%的对象都会很快的回收掉,所以并不需要按照1:1的比例来划分内存空间,新生代分为eden区,survivor一区,survivor二区,比例是8:1:1,当回收时,需要将eden区和一区的对象进行回收,然后放到二区中。如果,万一说,回收后存活的量大于10%,那么多余的将会放到老年区。

 

标记--整理算法

复制算法存在大量的复制操作,而且,如果存在极端情况比如所有的对象都不需要回收,或者90%以上的对象都不需要回收,那么就需要有额外的空间来提供担保空间针对老年代的回收。

标记整理算法是先将需要回收的对象进行标记,把不需要回收的对象向一端移动,然后清理掉边界以外的内存。

分代收集算法:

对新生代的区域,使用复制算法只需要付出少量存活对象的复制成本就可以完成收集。

对于老年代的收集,因为存活率高,没有额外的担保空间,就必须使用标记-清理或者标记-整理算法

3.4  HotSpot的算法实现

枚举根节点

在进行枚举根节点时,理论上我们需要将全系统的上下文扫描一遍,这种方式对大系统来说会很耗时,无法在GC时进行可达性分析进行。因此虚拟机需要使用某种方式获取哪些地方使用了哪些对象的引用。再HotSpot中,使用OopMap数据接口来存储这些对象引用。在类加载完毕后,HotSpot就将对象内使用的其他对象引用计算出来,放入OopMap中, 这样GC在扫描时就可以直接得知这些信息。

安全点:

当执行系统停顿下来以后,并不需要一个不漏的检查完所有的执行上下文和全局的引用位置,虚拟机可以通过OopMap的数据结构来达到这个目的,用来存储哪些地方存放着对象引用,在JIT编译过程中,也会在“特定的位置”记录下栈和寄存器中哪些位置时引用。但如果OopMap存储的指令非常多,如果为每个指令都生成对应的OopMap,会造成空间成本特别高。因此设置了安全点,

如果同时使用final和static来修饰一个变量(常量),并且这个变量的数据类型是基本类型或者让java.lang.string的话,就生成ConstantValue属性来进行初始化;如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<cl-init>方法中进行初始化。

注:ConstantValue  =  final+ static  +(基本类型/String)

cl-init =  没有被final修饰或者并非基本类型及字符串

内存分配与回收策略

对象优先再Eden区分配对象,如果Eden区内存不够用,虚拟机就会发起一次针对Eden区的垃圾回收,使用复制算法,将Eden和Survivor-1的存活对象分配到Survivor-2中(下一次将把Eden和Survivor-2的存货对象分配到Survivor-1中),如果有多余的,将会通过担保方式进入老年代。如果是大对象(典型的时数组大对象),则直接进入老年代。可以通过-XX:PretenureSizeThreshold来设定大于这个值的对象就进入老年代。对象在Survivor区中每经历一次垃圾回收,就将年龄增加1,当达到15时(默认)还没有被回收的话,就将进入老年带,可以通过-XX:MaxTenuringThreshold来设定年龄大小。如果Survivor中某个年龄的对象数量占用Survivor一半以上的内存,则年龄大于等于这个年龄的直接进入老年代

空间分配担保:

Eden,Survivor区垃圾回收后,如果另一个Survivor不够装下剩余存活的对象,则有对象要进入老年代,这时候虚拟机会有 先检查自己剩余的连续空间是否能装下这些对象,如果足够,则顺利进行。如果不够,看HandlePromotionFailure设置是否允许担保失败,如果不允许,则需要进行一次Full GC(Full GC会带来性能的损耗)。如果允许,则会查看历次进入老年代的平均值与剩余最大连续空间,如果是,则尝试进行Minor GC,如果不是,则进行一次Full GC

虚拟机性能监控与故障处理工具

Jps—虚拟机进程状况工具

1:Jps –mlv 可用于显示正在运行的虚拟机进程

       2:使用频率最高的jdk命令行工具,因为其他的jdk工具大多需要输入它查询到的LVMID来确定要监控的是哪一个虚拟机今称

       3:LVMID:本地虚拟机唯一ID  local virtual machine identifier

Jstat—虚拟机统计信息监视工具

       1:jstat [ option vmid [interval[ms|s] [count]] ]

       2:用于监视虚拟机各种运行状态信息的命令行工具,interval代表查询间隔,默认是毫秒为单位,count为查询次数

Jinfo-Java配置信息工具

       1:jinfo可以实时查看并调整虚拟机各项参数

       2:命令格式:jinfo [option] pid

       3:使用-v参数可以查看虚拟启动时显示指定的参数列表

       4:使用-flags查看未被显式指定的参数列表

       5:使用-sysprops吧虚拟机进程的System.getProperties()的内容打印出来

       6:使用-flag [+|-] name或者-flag name=value修改一部分运行期可写的虚拟机参数值

Jmap:java内存映像工具

       1:用于生成堆转储快照(称为heapdump或者dump文件)

       2:还有另外三种可以获取java堆转储快照

              1:通过-XX:+HeapDumpOnMemoryError参数,可以让虚拟机在OOM一场出现之后自动生成dump文件

              2:通过-XX:+HeapDumpOnCtrlBreak参数,可以使用【Ctrl】+【Break】键让虚拟机生成dump文件

              3:在linux系统中通过执行kill -3命令发送进程推出信号吓唬一下虚拟机,也能拿到dump文件

       3:查询finalize执行队列

       4:查询java堆的详细信息

       5:查询永久代的详细信息

       6:命令格式:jmap [ option ] vmid

Jstack—java线程堆栈跟踪工具

       1:用于生成虚拟机当前时刻的线程快照(threaddump或者javacore文件)。就是当前虚拟机每一条线程正在执行的方法堆栈的集合。目的:为了分析定位线程出现长时间停顿的原因,如死锁,死循环,请求外部资源导致的长时间等待。可以知道线程正在做什么事情

       2:命令格式:jstack [ option ] vmid

JConsole--Java监视与管理控制台

展示功能包括,概述,内存,线程,类,VM摘要,MBean

VisualVM—多合一故障处理工具

       1:显示虚拟机进程以及进程的配置,环境信息(jps,jinfo)

       2:监视应用程序的CPU,GC,堆,方法区以及线程的信息(jstat,jstack)

       3:dump以及分析堆转储快照(jmap,jhat)

       4:方法级的程序运行性能分析,能够找出被调用最多,运行时间最长的方法

       5:提供BTrace动态日志跟踪

              1>:作用:通过通过HotSwap技术动态加入原本并不存在调试代码

              2>:应用场景:比如在需要打印日志进行排错的时候,加入原本不存在的日志输出代码

比如:需要在BTraceTest方法add中打印出a,b的值

通过创建如下代码,可实现在add方法中打印参数a,b

虚拟机类加载机制

类文件结构

Class文件采用一种称为伪结构来存储数据。这种伪结构只有两种数据类型:无符号数和表

无符号数:

属于基本的数据类型,无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串值

以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节,8个字节的无符号数

表:

由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表基本以_info结尾

注意:其中展示的顺序是不允许被更改的

Magic:

魔数,占用4个字节,值为0xCAFEBABE

作用:确定这个文件是否为一个能被虚拟机接受的Class文件

Minor_version:

       存储Class文件的版本号中的次版本号

Major_version:

       存储Class文件的版本号中的主版本号(每个大版本,将会在主版本号上加1,虚拟机拒绝运行超过其版本号的Class文件,即被jdk8编译的文件在jdk7环境的jvm中无法运行)

常量池计数:

       占用两个字节,从1开始计数,如果是00 16(16进制)表示有21个常量15+6,如果是00 00表示没有常量

常量池:

       可以理解为Class文件的资源仓库,占用空间大,第一个出现表类型的数据项目

       主要存放两大类常量:

字面量(文本字符串,声明为final的常量值)

符号引用(类和接口的全限定名,方法的名称和描述符,字段的名称和描述符)

常量池中每一项常量都是一个表,这14中常量类行各自有各自的结构,比如CONSTANT_Class_Info结构如下:

Tag是标志位,用于区分常量类行

Name_index是一个索引值,代表了这个类或者接口的全限定名

CONTSTANT_Utf8_Info结构如下:

Tag是标志位,用于区分常量类型

Length值说明了这个UTF-8编码的长度是多少字节

Bytes是在length后面紧跟着长度为length字节的连续数据

值得注意的是,由于Class文件中方法,字段等都需要使用CONSTANT_Utf8_info型常量来描述名称,因此CONSTANT_Utf8_Info的最大长度也就是我们可以定义的方法,字段的最大长度(64KB,两个字节2^16)

14中常量项的数据结构

访问标志:access_flag

用于识别一些类或者接口层次的访问信息

类索引,父类索引与接口索引集合

类索引this_class,确定该类的全限定名,指向一个类型为CONSTANT_Class_Info的类描述符常量,通过CONSTATNT_Class_Info类型的常量中的索引值可以找到CONSTANT_Utf8_Info类型的常量中的全限定名字符串)和父类索引super_class,确定这个类的父类的全限定名,指向一个类型为CONSTANT_Class_Info的类描述符常量,通过CONSTATNT_Class_Info类型的常量中的索引值可以找到CONSTANT_Utf8_Info类型的常量中的全限定名字符串)都是一个u2类型的数据

接口索引(interfaces)是一组u2类型的数据集合,在这之前是一个u2类型的数据interface_count表示所引表的容量。

字段表集合大小:一个u2类型的数据,表示字段表集合大小(field_count)

字段表集合(field_Info):用于描述接口或者类中声明的变量,包括类级变量以及实例级变量,但不包括方法内声明的局部变量

字段表集合中单个field的数据结构为

Access_flags中存有字段修饰符,name_index存有字段的简单名称,descriptor_Index存有字段和方法的描述符,后面两个都是对常量池的引用

全限定名:指的是类的className,包括包名

简单名称:指的是没有类型和参数修饰的方法和字段名称,如m字段的简单名称为m,方法inc()的简单名称为inc

方法和字段的描述符:用来描述字段的数据类型,方法的参数列表(包括数量,类型以及顺序)和返回值

根据描述符规则

基本数据类型(boolean,byte,char,short,int,long,float,double)以及代表无返回值的void类型都用一个大写字母表示

对象类型则用字符L加对象的全限定名来表示

对于数组类型,每一维将使用一个前置的“[“字符来描述,如定义一个java.lang.String[][]类型的二维数组,将被记录为”[[Ljava/lang/String;“,一个整型数组”int[]“将被记录为”[I“

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在()中,如:

Void inc() 的描述符为()V

Java.lang.String toString()的描述符为()Ljava/lang/String

Int indexOf(char[] source,int

sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int

fromIndex) 的描述符为([CII[CIII)I

方法表集合

与字段表几个的access_flags访问标志的可选项中基本类型

删减了volatile和transient关键字,添加了synchronized,native,strictfp,abstract关键字

方法中的代码经过表义诚字节码指令后,存放在属性标记和中一个名为Code的属性里面,

属性表集合(attribute_info)

属性表中会存在很多属性标签,相应的标签用来存放相应的内容,比如上述的Code在方法表集合中用来存放编译过后的代码,ConstantValue用来表示如果某个字段是否被声明为final static,Exception用来存放方法抛出的异常,InnerCLass内部类列表等等

对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_Info类型的常量来表示,而属性值的接口则是完全自定义的。

ConstantValue属性

作用是通知虚拟机自动为静态变量赋值。对于非static类型的变量的赋值是在实例构造器<init>方法中进行的。对于类变量,则有两种方式可以选择:在类构造器<clinit>方法或者使用ConstantValue属性。

如果同时使用final和static来修饰基本类型或者java.lang.String的变量的话,就生成ConstantValue属性来初始化。

如果这个变量不是基本类,字符串或者没有被final修饰,则将会选择在<clinit>方法中进行初始化

虚拟机类加载机制

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制

在java中,类型的加载,连接和初始化过程都是在程序运行期间完成的,称之为运行期动态加载和动态连接。

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

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

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

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

Public class superclass{

       Static{

              System.out.println(“superclassinit”)

       }

       Publicstatic int value = 123;

}     

Public class subclass extends superclass{

       Static{

              System,out.println(“subclassinit”);

       }

}

Main(){

       System.out.println(subclass.value)

}

对于与静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会出发父类的初始化而不会触发子类的初始化。

public class ConstClass {

static {

        System.

out.println("ConstClass

init !");

}

public static final String HELLO_WORLD="hello world";

}

public class NotInitialization {

public static void main(String[] args) {

        System.

out.println(ConstClass.HELLO_WORLD);

}

}

结果没有输出ConstClass init ,因为HELLO_WORLD被申明为static final 同时又是个String,所以会被ConstantValue修饰,在编译阶段通过常量传播优化,就已经将HELLO_WORLD存储到了NotInitialzation类的常量池中,以后NotInitialization对常量ConstClass.HELLO_WORLD的引用实际都被转化为NotInitialization类对自身常量池的引用。

如果将HELLO_WORLD的修饰符中final去掉,或者将String换成其他非基本类型,就可以打印出ConstClass init

一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口定义的常量)才会被初始化

加载:

虚拟机需要完成的一下3件事情

通过一个类的全限定名来获取定义此类的二进制字节流

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

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

加载阶段完成之后,二进制流就存储在了方法区之中,作为程序访问这个类型数据的外部接口

验证:

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。

验证阶段是非常重要的,这个阶段是否严谨,直接决定了java虚拟机是否能承受恶意代码的攻击,从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中又占了相当大的一部分。整体上看,验证阶段大致上会完成下面4各阶段的校验动作:文件格式验证,元数据验证,字节码验证,符号引用验证。验证失败的话会抛出java.lang.cerifyError异常,及其子类异常。

文件格式验证:

这一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

验证点:

是否以魔数0xCAFEBABE开头

主次版本号是否在当前虚拟机处理范围之内

常量池的常量中是否有不被支持的常量类型

指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量

CONSTANT_UTF8_INFO型的常量中是否有不符合UTF8编码的数据

Class文件中各个部分及文件本身是否有被删除或附加的其他信息     

等等。。。

元数据验证:

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,这个阶段可能包括的验证点如下:

除了java.lang.Object以外,所有的类都应当有父类

这个类的父类是否继承了不允许被继承的类

如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法

类中的字段,方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现了不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同

字节码验证:

这一阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的,例如:

保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不饿能出现类型这样的情况,在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。

保证跳转指令不会跳转到方法体以外的字节码指令上

保证方法体中的类型转换是有效的,录入可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系,完全不相干的一个数据类型,则是危险和不合法的。

符号引用验证:

符号引用验证可以看作是对类自身以外(常量池中的各种富豪引用)的信息进行匹配性校验,例如:

符号引用中通过字符串描述的权限定名是否能找到对应的类

在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段

符号引用中的类,字段,方法的访问行是否可被当前类访问

对于虚拟机的类加载机制来说,验证阶段是一个非常重要的,但不是一定必要的阶段。如果所运行的全部代码都已经被反复使用和验证过,那么在实施阶段可以考虑使用–Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间

准备

准备阶段是正式为类变量分配内存并设置类变量 初始值的阶段。

其中类变量指的是带有static的变量,

初始值分为两种,一种变量呗static final修饰,则初始化为代码中指定的值,如:

Public static final int value= 123

这时value呗套上ConstantValue属性,value值为123

如果是:

Public static int value = 123

则初始值为0

解析

是虚拟机将常量池内的符号引用替换为直接应用的过程

符号引用:

符号引用再class文件中他以CONSTANT_Class_Info,CONSTANT_Fieldref_Info,CONSTANT_Methodref_Info等类型的常量出现。以一组符号来描述所引用的目标,符号可以是任何形式的字面量,符号引用的字面量形式明确定义在java虚拟机规范的Clas文件格式中。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

直接引用:

       直接引用可以是直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄。有了直接引用,那引用的目标必定已经在内存中存在。故与虚拟机实现的内存布局相关。

解析动作主要针对

初始化

是类加载过程的最后一步,在准备阶段,变量已经赋过一次系统要求的初始值,在初始化阶段,则根据程序员通过程序制定的主观计划去初始化变量和其他资源。初始化阶段实质性类构造器<clinit>方法的过程

方法是由编译器自动收集类中所有的类变量的赋值动作和静态语句块的语句合并产生的(所以如果没有,就将不会产生方法),编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块对定义在在静态语句块之后的类变量只具有赋值,不具备访问的能力,否则编译器会提示“非法向前引用“

<clinit>方法不需要现实的调用父类构造器,会保证在子类的<clinit>方法执行之前,父类的<clinit>方法已经执行完毕

虚拟机会保证一个类的<clinit>方法在多线程环境中被正确的加锁,同步,如果多线程同时去初始化一个类,那么只会有一个县城去执行这个类的<clinit>方法,其他线程都需要阻塞等待

类加载器:

对于任意一个类,都需要有加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每一个类加载器都拥有一个独立的类名称空间。即:比较两个类是否相等,只有在这两个类是有同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。这里所指的相等,包括类的Class对象的equals方法,isAssignableFrom方法,isInstance方法,也包括使用instanceOf关键字对象所属关系判定等情况。

双亲委派模型:

绝大多数java程序都会使用到以下3种系统提供的类加载器:

1:启动类加载器,这个类加载器负责将存放在JAVA_HOME\lib目录中,或者被-Xbootclasspath参数所制定的路径中,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)

2:扩展类加载器,这个加载器负责加载JAVA_HOME\libn\ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库

3:应用程序类加载器:负责加载用户类路径(ClassPath)上所制定的类库

以上图中的类加载器之间的这种层次关系,成为类加载器的双亲委派模型。

工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲只是parent的直译,并不代表两个父类加载器或者一个父类加载器一个母类加载器

双亲委派模型并不是一个强制性的约束模型。

虚拟机字节码执行引擎

代码编译的结果从本地机器码转变为字节码,是编程语言发展的一大步

运行时栈帧结构

1:栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,他是虚拟机运行时数据区的虚拟机栈的栈元素。

2:栈帧存储了方法的局部变量表,操作数栈,动态链接和方法返回地址等信息。

3:每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程

虚拟机栈中会有很多栈帧,处于栈顶的栈帧表示即将执行或者正在执行的方法。如图:

局部变量表:

1:用于存放方法参数和方法内部定义的局部变量

2:局部变量表的容量以变量槽(slot)为最小单位,但并不规定slot占用内存大小,能存放一个boolean,byte,char,short,int,float,reference,returnAddress类型的数据

3:在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第0位索引的slot默认是用于传递方法所属对象实例的引用

4:对于变量的赋值问题,

[if !supportLists]1>  [endif]如果是类变量,系统会由两次赋值过程,一次是在准备阶段,赋予系统初始值,另外一次是在初始化阶段,赋予程序员定义的初始值。

[if !supportLists]2>  [endif]如果是局部变量,如果一个局部变量定义了但没有赋初始值是不能使用的。

[if !supportLists]3>  [endif]所以,虚拟机并不是会为每一个变量都赋予初始值

操作数栈:

操作数栈中存访的是数据,例如

1:计算a+b=c,首先会将a和b放入操作数栈,然后将两个值取出来,进行加法运算后,再将c写入操作数栈。

2:调用其它方法的时候是通过操作数栈来进行参数传递的

所以,在方法开始执行的时候,这个方法的操作数栈是空的,在执行的过程中会有各种指令对操作数栈中进行写入和提取数据,也就是出栈/入栈操作。

动态链接:

每个栈帧(线程->虚拟机栈->栈帧)都包含一个指向运行常量池(虚拟机内存->方法区->运行时常量池)中该栈帧所属方法的引用。

Class文件的常量池中存有大量的符号引用,这些符号引用一部分会在类加载阶段或者第一次使用的时候就转换为直接引用,这种转化成为静态解析。

另外一部分将在每一次运行期间转化为直接引用,这部分称为动态链接

方法返回地址:

用于方法执行过程中出现异常退出或者执行完成后返回上级方法,并恢复上级方法的执行状态(局部变量表和操作数栈,并把返回值(有的话)压入调用者站真的操作数栈中,调整程序计数器的值以指向后面的一条指令)

方法调用:

Class文件编译过后,在class文件的常量池中会存放方法的符号引用

在类加载阶段(解析阶段),会将其中一部分符号引用转化为直接引用,但并不包括所有的方法都会在解析阶段被转化为直接引用,需要符合“编译器可知,运行时不可变”的原则。符合这个原则的是

       1:invokestatic:静态方法

       2:invokespecial:实例构造器<init>,私有方法和父类方法

       3:invokevirtual:所有的虚方法

       4:invokeinterface:调用接口方法,会在运行时在确定一个实现此接口的对象

       5:invokedynamic:现在运行时动态解析调用点限定符所引用的方法,然后再执行该方法

Tomcat中java类库放置的目录路径及其作用范围:

1:放置在common目录中的,类库可悲tomcat和所有的Web应用程序共同使用,对应的类加载器为:CommonClassLoader

2:放置在server目录中,类库可被tomcat使用,对所有的web应用程序都不可见,对应的类加载器为:CatalinaClassLoader

3:放置在shared目录中,类库可被所有的Web应用程序共同使用,但对tomcat是不可见的,对应的类加载器为:SharedClassLoader

4:放置在/webapp/WEN-INF目录中:类库仅仅可被web应用程序使用,对tomcat和其他的web应用程序都不可见,对应的类加载器为:WebAppClassLoader

类加载流程图:

Java内存模型与线程

C/C++等语言直接使用物理硬件和操作系统提供的内存模型,因此会由于不同平台上的内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错,因此C/C++在某些场景下就必须针对不同的平台来编写程序。

Java试图提供一种内存模型,使得代码层面无需考虑各种硬件和操作系统的内存访问差异

Java中每个工作线程都有一个独立的工作内存,每个工作内存存放着本线程执行时需要的变量等信息,并且线程之间是不能共享变量值的。Java的内存模型主要解决“一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之类的实现原理”

Java内存模型中定义了一下8种操作来完成变量在工作内存与主内存之间的传递。

Lock 锁定,作用于主内存的变量,他把一个变量标志位一条线程独占的状态

Unlock 解锁作用于主内存的变量,他把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

Read 作用于主内存的变量,他把一个变量的值从主内存传输到线程的工作内存中

Load作用于工作内存的变量,他把read操作从主内存中得到的变量值放入工作内存的变量副本中

Use作用于工作内存的变量,他把工作内存的一个变量的值传递给执行引擎

Assign 作用于工作内存的变量,执行引擎接收到的值赋给工作内存的变量

Store 作用于工作内存的变量,他把工作内存中一个变量传送到主内存中

Write 作用于主内存的变量,他把store操作从工作内存中得到的变量的值放入主内存的变量中

Volatile变量修饰符

当一个变量定义为volatile之后,他将具备两种特性,

第一是保证此变量对所有线程的可见性,这里的可见性是指当一条线程改变了这个变量的值,新值对于其他线程是可以立即得知的。而普通变量则需要通过主内存与工作内存之间的变量值的传递来完成。但并不代表使用volatile在多线程环境中就一定是安全的,要保证安全,还是得靠synchronized或者java.util.concurrent的原子类(volatile具备可见性,但并不具备原子性)

第二是可以禁止指令重排序,普通变量仅仅会保证在该方法的执行过程中所有以来复制结果的地方都能获得正确的结果,而不能保证变量赋值操作的顺序与程序代码的执行顺序一致。作用方式是通过在使用volatile变量的地方添加了内存屏障,在重排序时不能把后页面的指令重排序到内存屏障之前的位置

Java内存模型是围绕在并发过程中如何处理原子性可见性和有序性这3个特征来建立的:

原子性:

由java内存模型来直接保证的原子性变量操作包括read,load,assign,use,store,write

可见性:

可以使用volatile,final,synhronized关键字来实现可见性

有序性:

如果在线程内观察,所有的操作都是有序的(线程内表现为串行);如果在一个线程中观察另一个线程,所有的操作都是无序的(指令重排序和工作内存与主内存同步延迟)。

Java通过提供volatile和synchronized两个关键字来保证线程之间操作的有序性。

Volatile本身就包含了禁止指令重排序的语义

Synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”

上一篇下一篇

猜你喜欢

热点阅读