深刻理解Java虚拟机

目录

第一部分:概述java

1 走进Java算法

    1.1 概述  编程

    1.2 Java技术体系数组

第二部分:自动内存管理机制缓存

2 Java内存区域与内存溢出异常安全

    2.1 运行时数据区域数据结构

    2.2 HotSpot虚拟机对象探秘多线程

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

 3.1 对象已死吗编程语言

 3.2 垃圾收集算法

 3.3 HotSpotd的算法实现

 3.4 垃圾收集器

 3.5 内存分配与回收策略

第三部分: 虚拟机执行子系统

6 类文件结构

 6.1 无关性的基石

 6.2 Class类文件的结构 

7 虚拟机类加载机制

 7.1 类加载的时机

 7.2 类加载的过程

 7.3 类加载器

8 虚拟机字节码执行引擎

 

 

 

1 走进Java

1.1 概述

  Java优势:它摆脱了硬件平台的東缚,实现了“一次编写,处处运行”的理想;它提供了一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄露和指针越界问题;它实现了热点代码检测和运行时编译及优化,这使得Java应用能随着运行时间的增长而得到更高的性能;它有一套完善的应用程序接口,还有无数来自商业机构和开源社区的第三方类库来帮助它实现各类各样的功能等等。

1.2 Java技术体系

sun官方定义:

  1 Java程序设计语言

  2 各类硬件平台上的Java虚拟机

  3 Class文件格式

  4 Java API类库

  5 来自商业机构和开源社区的第三方Java类库

把Java程序设计语言、Java虚拟机、 Java API类库这三部分统称为JDK(Java Development Kit),JDK是用于支持Java程序开发的最小环境。把 Java API类库中的Java SE API子集和Java虚拟机这两部分统称为JRE( Java Runtime Environment),JRE是支持Jav程序运行的标准环境。

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

 2.1 运行时数据区域

 

(1)程序计数器

 程序计数器( Program Counter Register)是一块较小的内存空间,它能够看做是当前线程所执行的字节码的行号指示器。字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。

特色:因为Java虚拟机的多线程是经过线程轮流切换并分配处理器执行时间的方式来实现的在任何一个肯定的时刻,一个处理器(对于多核处理器来讲是一个内核)都只会执行一条线程中的指令。所以,为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程私有”的内存。若是线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;若是正在执行的是 Native方法,这个计数器值则为空( Undefined)。

异常:此内存区域是惟一一个在Java虚拟机规范中没有规定任何 Outofmemory Error状况的区域

(2)Java虚拟机栈

虚拟机栈描述的Java方法执行的内存模型:每一个方法在执行的同时都会建立一个栈( Stack Frame)用存铺局部变量表、操做数、动态链、方法口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程

栈帧(Stack Frame):每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame).每一个独立的栈帧通常包括:

  • 函数的返回地址和参数
  • 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其余临时变量
  • 函数调用的上下文

局部变量表存放了细期可知的各类基本数据类型、对象引用( reference类型,它不等同于对象自己,多是一个指向对象起始地址的引用指针,也多是指向一个表明对象的句柄或其余与此对象相关的位置)和returnAddress(指向了一条字节码指令的地址)

特色:线程私有,生命周期与线程相同。

异常:StackOverflowError、OutOfMemoryError。

(3)本地方法栈

本地方法栈与虚拟机栈所发挥的做用相似,虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用到的Native方法服务。有的虚拟机(Sun HotSpot)将两者合二为一。

异常:StackOverflowError、OutOfMemoryError

(4)Java堆

Java堆是被全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,所以不少时候也被称作“GC堆”( GarbageCollected Heap)

异常:OutOfMemoryError

(5)方法区

 方法区是各个线程共享的内存区域,存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Java规范中方法区为堆的一个逻辑部分但有一个别名Non-Heap(非堆)。

特色:垃圾收集行为在这个区域较少出现,回收目标主要是针对常量池的回收和对类型的卸载。

(6)运行时常量池

运行时常量池是方法区的一部分,用于存放编译期生成的各类字面量和符号引用,这部份内容当类加载完成后进入常量池存放。

特色:具有动态性,在运行期间也可将新的常量放入常量池中如String类的intern()方法

异常:OutOfMemoryError

(7)直接内存

 直接内存并非虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。

 2.2 HotSpot虚拟机对象探秘

(1)对象建立

  • 1 虚拟机遇到new指令时检查可否在常量池中定义到一个类符号的引用,并检查是否执行过类加载过程,并执行类加载过程。
  • 2 为对象分配内存。根据是堆否规整:指针碰撞、空闲列表
  • 3 在分配时考虑并发问题
  • 4 将对象分配到的内存空间初始化为0(不含对象头)
  • 5 对对象进行设置

从虚拟机角度而言便结束了,从Java程序语言而言才刚开始,执行初始化方法,把对象按照代码的设置初始化。

(2)对象的内存布局

包含三部分:对象头、实例数据、对齐填充。

  • 对象头

包含两部分信息:存储对象自身的运行时数据;类型指针,用来肯定是哪一个对象的实例。可是第二部分类型指针不必定全部的虚拟机上都有。当对象是数组时对象头还须要一块用于记录数组长度的数据。

  • 实例数据

实例数据是对象真正存储的有效信息,也是程序代码中所定义的各类类型的字段内容包含:父类继承的,子类特有的。

  • 对齐填充

并非必然存在的,由于HOtSpot VM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍(1~2倍)。

(3)对象的访问定位

  • 句柄访问

 

  •  经过直接指针访问对象

 

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

主要关注:哪些内存须要回收?何时回收?如何回收?

 3.1 对象已死吗

(1)判断对象是否已死:

  (1)引用计数法。添加引用计数器,但很难解决对象间相互循环引用问题,实际中基本不用。

  (2)可达性分析算法。以GC Roots对象为根进行搜索。

 GC Roots对象包含:虚拟机栈(栈帧中的本地变量表)中引用的对象;方法区中类静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI(即Native方法)引用的对象。

(2)再谈引用

在JDK1.2前引用的定义:若是 reference类型的数据中存储的数值表明的是另一块内存的起始地址,就称这块内存表明着一个引用。这种定义太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态。咱们但愿能描述这样一类对象:当内存空间还足够时,则能保留在内存之中,若是内存空间在进行垃圾收集后仍是很是紧张,则能够抛弃这些对象。不少系统的缓存功能都符合这样的应用场景。

后来对引用概念作了扩充:将引用分为强引用( Strong Reference)、软引用( Soft Reference)、弱引用( Weak Reference)、虚引用( Phantom Reference)4种,这4种引用强度依次逐渐减弱。

  • 强引用就是指在程序代码之中广泛存在的,相似“ Object obj= new Object0”这类的引用,只要强引用还存在,垃圾收集器水远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用但并不是必需的对象。
  • 弱引用也是用来描述非必需对象的,可是它的强度比软引用更弱一些。
  • 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。

(3)生存仍是死亡

可达性分析算法中不可达的对象,处于缓刑阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。若是对象在进行可达性分析后发现没有与 GC Roots相链接的引用链,那它将会被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。若是这个对象被断定为有必要执行 finalizer()方法,那么这个对象将会放置在一个叫作F-queue的队列之中,虚拟机会触发这个方法,但并不承诺会等待它运行结束,稍后GC将对 F-queue中的对象进行第二次小规模的标记,若是对象要在finalize()中成功拯救本身一一只要从新与引用链上的任何一个对象创建关联便可,如把本身(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;若是对象这时候尚未逃脱,那基本上它就真的被回收了。

(4)回收方法区

方法区(永久代)的垃圾收集主要回收两部份内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象很是相似。要判读无用的类则须要知足下面三个条件:

  • 该类全部的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader已经被回收
  • 该类对应的java.lang.Clas对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。

虚拟机能够对知足上述3个条件的无用类进行回收,但仅仅是能够而不是必定。

 3.2 垃圾收集算法

1)标记-清除算法(Mark-Sweep)

该算法分为两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象,它的标记过程在对象标记断定时已经介绍过了。后续的收集算法都是基于这种思路并对其不足进行改进而获得的。

它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高:另外一个是空间问题,标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能会致使之后在程序运行过程当中须要分配较大对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。

(2)复制算法

复制算法是为了解决效率问题。把内存分为大小相等的两个块,每次只使用其中一块,当一块用完了把其中还存活的对象复制到另外一块中。这样也不用考虑内存分配时内存碎片问题了。

不足:只是这种算法的代价是将内存缩小为了原来的一半。

商用中采用将内存分为一块较大的Eden和两块较小的Survivor,每次使用Eden和一块Survivor,回收时将活着的对象复制到另外一块Survivor中。HotSpot默认Eden和Survivor大小为8:1。但没有办法保证每次回收都只有很少于10%的对象存活,当 Survivor空间不够用时须要依赖其余内存(这里指老年代)进行分配担保( Handle Promotion)。即若是另一块 Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接经过分配担保机制进入老年代。

(3)标记-整理算法(Mark-Compact)

 复制收集算法在对象存活率较高时就要进行较多的复制操做,效率将会变低,此外老年代对象通常是100%存活的所以不能直接选用这种算法。根据老年代的特色标记-一整理(Mark- Compact)算法,标记过程仍然与标记一清除算法同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。

(4)分代收集算法

目前普遍使用的是分代收集算法,根据对象存活周期的不一样将内存划分为几块。通常是把Java堆分为新生代老年代,这样就能够根据各个年代的特色采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法。而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记一清理”或者“标记一整理”算法来进行回收。

 3.3 HotSpot的算法实现

此部分为看,主要是分析代码的

3.4 垃圾收集器

收集算法是内存回收的方法论,垃圾收集器则是内存回收的具体实现。下图中连线表示能够相互配合:

 

 (1)Serial收集器

单线程;在工做时必须暂停其余全部的工做线程(Stop The World);简单高效。

 (2)ParNew收集器

Serial的多线程版本。

 注:并行( Parallel):指多条垃圾收集线程并行工做,但此时用户线程仍然处于等待状态。

并发( Concurrent):指用户线程与垃圾收集线程同时执行(但不必定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另ー个CPU上

(3)Parallel Scavenge收集器

其余收集器的关注点是尽量地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量( Throughput)

吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合须要与用户交互的程序,良好的响应速度能提高用户体验,而高吞吐量则能够高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不须要太多交互的任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的参数以及直接设置吞吐量大小的参数

(4)Serial Old收集器

单线程;给Client模式下的虚拟机使用;在Server模式下可做为CMS收集器的后备预案

(5)Parallel Old收集器

多线程。在注重香吐量以及CPU资源感的场合,均可以优先考虑 Parallel Scavenge加 ParallelOd收集器

(6)CMS收集器

(7)G1收集器

3.5 内存分配与回收策略

Java内存自动化管理最终目的是自动化解决:给对象分配内存以及回收分配给对象的内存。

 分配策略:

  • 对象优先在Eden分配,当没有足够空间时虚拟机发起一次Minor GC
  • 大对象直接进入老年代如:字符串、数组。
  • 长期存活的对象进入老年代。采用对象年龄计数器,默认15岁以上进入
  • 动态对象年龄断定。若Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可直接进入老年代。
  • 空间分配担保。一共有多少对象存活在实际完成回收前没法肯定,所以取以前每次回收到老年代对象容量的平均大小值做为经验值,与老年代的剩余空间比较,决定是否Full GC。

 注: Minor GC和 Full GC区别

新生代GC( Minor GC)指发生在新生代的拉圾收集动做,由于Java对象大多都具有朝生夕灭的特性,因此 Minor GC很是频繁,通常回收速度也比较快

老年代GC( Major GC/ Full GC):指发生在老年代的GC,出现了 Major GC,常常会伴随至少ー次的 Minor GC(但非绝对的,在 Parallel Scavenge收集器的收集策略里就有直接进行 Major GC的策略选择过程)。 Major GC的速度通常会比 Minor GC慢10倍以上。

6 类文件结构

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,倒是编程语言发展的一大步。

6.1 无关性的基石 

实现语言无关性的基础是虚拟机字节码存储格式。Java虚拟机不和包括Java在内的任何语言绑定,它只与Clas文件这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其余辅助信息。Java虚拟机能够运行其余语言编译的字节码文件。

6.2 Class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,中间无分隔符。Class文件格式以下图所示:

 

 (1)魔数与Class文件的版本

每一个 Class文件的头4个字节称为魔数( Magic Number),它的惟一做用是肯定这个文是否为一个能被虚拟机接受的Css文件。紧接着魔数的4个字节存储的是CS文件的版本号:第5和第6个字节是次版本号( Minor Version),第7和第8个字节是主版本号( Major Version)。

 (2)常量池

主次版本以后即是常量池,从1开始而不是从0开始计数(只有常量池特例),为了表达某些指向常量池的索引值的数据在特定的状况下须要表达:不引用任何一个常量池项目的含义。

常量池中主要存放两大类常量:字面量和符号引用。字面量接近于Java语言层面的常量概念,如文本字符串、声明为fnal的常量值等。符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名( Fully Qualified Name)
  • 字段的名称和描述符( Descriptor)
  • 方法的名称和描述符

 (3)访问标志

在常量池结束以后,紧接着的两个字节表明访问标志( access flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类仍是接口;是否认义为 public类型;是否认为 abstract类型;若是是类的话,是否被声明为 final等。

 (4)类索引、父类索引、接口索引集合

类索引( this class)父类索引( super class)都是一个u2类型的数据,而接口索引集合( interfaces)是一组u2类型的数据的集合,Cass文件中由这三项数据来肯定这个类的继承关系。类索引用于肯定这个类的全限定名,父类索引用于肯定这个类的父类的全限定名接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按 implements语句(若是这个类自己是一个接口,则应当是 extends语句)后的接口顺序从左到右排列在接口索引集合中。

 (5)字段表集合

字段表( (field info)用于描述接口或者类中声明的变量。字段(feld)包括类级变量以实例级变量,但不包括在方法内部声明的局部变量。

 (6)方法表集合

表结构和字段表相似。

 (7)属性表集合

在Class文件、字段表、方法表均可以携带本身的属性表集合,来描述某些场景专有的信息。属性表中的属性较多,这里只以Code属性为例其他的可参考书籍。Java程序方法体中的代码通过 Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出如今方法表的属性集合之中,但并不是全部的方法表都必须存在这个属性,如接口或者抽象类中的方法就不存在Code属性。

若是把一个Java程序中的信息分为代码(方法体里面的Java代码)和元数据( 包括类、字段、方法定义及其余信息)两部分,那么在整个 Class文件中,Code属性用于描述代码,全部的其余数据项目都用于描述元数据。

7 虚拟机类加载机制

虚拟机的类加载机制:虚拟机把描述类的数据从 Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虛拟机直接使用的Java类型。

7.1 类加载的时机

Java类的生命周期以下图所示:

 

加载、验证、准备、初始化和卸载这5个阶段的顺序是肯定的,类的加载过程必须按照这种顺序循序渐进地开始,而解析阶段则不必定:它在某些状况下能够在初始化阶段以后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

对于初始化阶段,虚拟机规范则是严格规定了有且只有5种状况必须当即对类进行“初始化”(而加载、验证、准备天然须要在此以前开始):

  • 遇到new、 getstatic、putstatic 或 invokestatic这4条字节码指令时,若是类没有进行过初始化,则须要先触发其初始化。
  • 使用 java. lang reflect t包的方法对类进行反射调用的时候,若是类没有进行过初始化,则须要先触发其初始化。
  • 当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化。
  • 当虚拟机启动时,用户须要指定一个要执行的主类(包含main0方法的那个类),虚拟机会先初始化这个主类
  • 当使用JDK1.7的动态语言支持时,若是一个java. lang invoke. Methodhandle实例最后的解析结果 REF_getstatic、 REF_putstatic、 REF_invokestatic I的方法句柄,而且这个方法句柄所对应的类没有进行过初始化,则须要先触发其初始化。

除此以外,全部引用类的方式都不会触发初始化,称为被动引用

  • 经过子类引用父类的静态字段,不会致使子类初始化
  • 经过数组定义来引用类不会触发此类的初始化
  • 常量在编译阶段会存入调用类的常量池中,本质上并无直接引用到定义常量的类,所以不会触发定义常量的类的初始化

   接口的初始化与类的初始化基本相似,只有类的初始化场景中的第三种不一样:一个接口初始化并不要求其父接口所有都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

 7.2 类加载的过程

(1)加载

“加载”是“类加载”( Class Loading)过程的一个阶段,在加载阶段,虚拟机须要完成如下3件事情:

  • 1)经过一个类的全限定名来获取定义此类的二进制字节流。
  • 2)将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构
  • 3)在内存中生成一个表明这个类的 java. lang Class对象,做为方法区这个类的各类数据的访问人口

 一个非数组类的加载阶段能够由用户自定义的类加载器去完成,而对数组类而言因为数组类自己不一样过类加载器建立,是有Java虚拟机直接建立。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义。注意,加载阶段与链接阶段的部份内容(如一部分字节码文件格式验证动做)是交叉进行的,加载阶段还没有完成,链接阶段可能已经开始,但这两个阶段的开始时间仍而后固定前后顺序。

 (2)验证

验证是链接阶段的第一步,这一阶段的目的是为了确保 Class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。验证阶段包含:文件格式验证、元数据验证、字节码验证、符号引用验证。

 (3)准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被saic修饰的变量),而不包括实例变量,实例交量将会在对象实例化时随着对象一块儿分配在Java雄中。其次,这里所说的初始值一般状况下是数据类型的零值。

 (4)解析

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

  • 符号引用( Symbolic References):符号引用以一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可。
  • 直接引用( Direct References):直接引用能够是直接指向目标的指针、相对偏移量是一个能间接定位到目标的句柄。

 (5)初始化

类的初始化是类加载的最后一步,到了初始化阶段才真正开始执行类中的Java程序代码(字节码)。初始化阶段是执行类构造器<clinit>()方法的过程

 7.3 类加载器

(1)类与类加载器

对于任意一个类,都须要由加载它的类加载器和这个类自己一同确立其在Java虚拟机中的惟一性,每个类加载器,都拥有一个独立的类名称空间。

(2)双亲委派模式

从Java虚拟机角度只存在两种不一样的类加载器:一种是启动类加载器( Bootstrap Classloader),这个类加载器使用C++语言实现,是虚拟机自身的一部分:另外一种就是全部其余的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,而且全都继承自抽象类java.lang.ClassLoader。

 

上图为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加載器外,其他的类加载器都应当有本身的父类加载器。这里类加载器之间的父子关系通常不会以继承( Inheritance)的关系来实现,而是都使用组合( Composition)关系来复用父加载器的。

双亲委派模型的工做过程:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的类加载器都是,所以全部的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈本身没法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器オ会尝试本身去加载。

实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

0

相关文章
相关标签/搜索