JVM虚拟机

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

1.1.1 程序计数器java

内存空间小,线程私有。字节码解释器工做是就是经过改变这个计数器的值来选取下一条须要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖计数器完成算法

若是线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;若是正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是惟一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 状况的区域。数据库

1.1.2 Java 虚拟机栈数组

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每一个方法在执行时都会床建立一个栈帧(Stack Frame)用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。安全

局部变量表:存放了编译期可知的各类基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)网络

StackOverflowError:线程请求的栈深度大于虚拟机所容许的深度。
OutOfMemoryError:若是虚拟机栈能够动态扩展,而扩展时没法申请到足够的内存。数据结构

1.1.3 本地方法栈并发

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。布局

1.1.4 Java 堆优化

对于绝大多数应用来讲,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。能够位于物理上不连续的空间,可是逻辑上要连续。

OutOfMemoryError:若是堆中没有内存完成实例分配,而且堆也没法再扩展时,抛出该异常。

1.1.5 方法区

属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

 

1.2.1 对象的建立

遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,而且检查这个符号引用表明的类是否已经被加载、解析和初始化过。若是没有,执行相应的类加载。

类加载检查经过以后,为新对象分配内存(内存大小在类加载完成后即可确认)。在堆的空闲内存中划分一块区域(‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)。

前面讲的每一个线程在堆中都会有私有的分配缓冲区(TLAB),这样能够很大程度避免在并发状况下频繁建立对象形成的线程不安全。

内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪一个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。

执行 new 指令后执行 init 方法后才算一份真正可用的对象建立完成。

1.2.2 对象的内存布局

在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机经过这个指针肯定这个对象是哪一个类的实例。另外,若是是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,由于普通对象能够经过 Java 对象元数据肯定大小,而数组对象不能够。

实例数据(Instance Data):程序代码中所定义的各类类型的字段内容(包含父类继承下来的和子类中定义的)。

对齐填充(Padding):不是必然须要,主要是占位,保证对象大小是某个字节的整数倍。

1.2.3 对象的访问定位

使用对象时,经过栈上的 reference 数据来操做堆上的具体对象。

经过句柄访问

Java 堆中会分配一块内存做为句柄池。reference 存储的是句柄地址。详情见图。

使用直接指针访问

reference 中直接存储对象地址

 

 

比较:使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不须要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。若是是对象频繁 GC 那么句柄方法好,若是是对象频繁访问则直接指针访问好。

2. 垃圾回收器与内存分配策略

程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(由于是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操做。而 Java 堆和方法区则不同,一个接口中的多个实现类须要的内存可能不同,一个方法中的多个分支须要的内存也可能不同,咱们只有在程序处于运行期才知道那些对象会建立,这部份内存的分配和回收都是动态的,垃圾回收期所关注的就是这部份内存。

2.2.1 引用计数法

给对象添加一个引用计数器。可是难以解决循环引用问题。

2.2.2 可达性分析法

经过一系列的 ‘GC Roots’ 的对象做为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

可做为 GC Roots 的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即通常说的 Native 方法) 引用的对象

2.2.3 再谈引用

前面的两种方式判断存活时都与‘引用’有关。可是 JDK 1.2 以后,引用概念进行了扩充,下面具体介绍。

下面四种引用强度一次逐渐减弱

强引用

相似于 Object obj = new Object(); 建立的,只要强引用在就不回收。

软引用

SoftReference 类实现软引用。在系统要发生内存溢出异常以前,将会把这些对象列进回收范围之中进行二次回收。

弱引用

WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集以前。在垃圾收集器工做时,不管内存是否足够都会回收掉只被弱引用关联的对象。

虚引用

PhantomReference 类实现虚引用。没法经过虚引用获取一个对象的实例,为一个对象设置虚引用关联的惟一目的就是能在这个对象被收集器回收时收到一个系统通知。

2.2.4 生存仍是死亡

即便在可达性分析算法中不可达的对象,也并不是是“facebook”的,这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:若是对象在进行中可达性分析后发现没有与 GC Roots 相链接的引用链,那他将会被第一次标记而且进行一次筛选,筛选条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种状况都视为“没有必要执行”。

若是这个对象被断定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫作 F-Queue 的队列中,并在稍后由一个由虚拟机自动创建的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,若是对象要在 finalize() 中成功拯救本身 —— 只要从新与引用链上的任何一个对象简历关联便可。

finalize() 方法只会被系统自动调用一次。


2.2.5 回收方法区

在堆中,尤为是在新生代中,一次垃圾回收通常能够回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。

永久代垃圾回收主要两部份内容:废弃的常量和无用的类。

判断废弃常量:通常是判断没有该常量的引用。

判断无用的类:要如下三个条件都知足

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

 

2.3.1 标记 —— 清除算法

效率低,产生空间碎片

2.3.2 复制算法  新生代选用

把空间分红两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另外一块上面。Eden 和 Survivor 和 Survivor  8:1:1。

2.3.3 标记-整理算法  老年代选用

不一样于针对新生代的复制算法,针对老年代的特色,建立该算法。主要是把存活对象移到内存的一端。

2.3.4 分代回收

根据存活对象划分几块内存区,通常是分为新生代和老年代。而后根据各个年代的特色制定相应的回收算法。

新生代

每次垃圾回收都有大量对象死去,只有少许存活,选用复制算法比较合理。

老年代

老年代中对象存活率较高、没有额外的空间分配对它进行担保。因此必须使用 标记 —— 清除 或者 标记 —— 整理 算法回收。

2.6 内存分配与回收策略

2.6.1 对象优先在 Eden 分配

对象主要分配在新生代的 Eden 区上,若是启动了本地线程分配缓冲区,将线程优先在 (TLAB) 上分配。少数状况会直接分配在老年代中。

通常来讲 Java 堆的内存模型以下图所示:

新生代 GC (Minor GC)

发生在新生代的垃圾回收动做,频繁,速度快。

老年代 GC (Major GC / Full GC)

发生在老年代的垃圾回收动做,出现了 Major GC 常常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度通常会比 Minor GC 慢十倍以上。

2.6.2 大对象直接进入老年代

2.6.3 长期存活的对象将进入老年代

2.6.4 动态对象年龄断定

2.6.5 空间分配担保

3. Java 内存模型与线程

3.1 Java 内存模型

3.1.1 主内存和工做内存之间的交互

操做 做用对象 解释
lock 主内存 把一个变量标识为一条线程独占的状态
unlock 主内存 把一个处于锁定状态的变量释放出来,释放后才可被其余线程锁定
read 主内存 把一个变量的值从主内存传输到线程工做内存中,以便 load 操做使用
load 工做内存 把 read 操做从主内存中获得的变量值放入工做内存中
use 工做内存 把工做内存中一个变量的值传递给执行引擎,
每当虚拟机遇到一个须要使用到变量值的字节码指令时将会执行这个操做
assign 工做内存 把一个从执行引擎接收到的值赋接收到的值赋给工做内存的变量,
每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操做
store 工做内存 把工做内存中的一个变量的值传送到主内存中,以便 write 操做
write 工做内存 把 store 操做从工做内存中获得的变量的值放入主内存的变量中
3.1.2 对于 volatile 型变量的特殊规则

关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。

一个变量被定义为 volatile 的特性:

保证此变量对全部线程的可见性。可是操做并不是原子操做,并发状况下不安全。
若是不符合 运算结果并不依赖变量当前值,或者可以确保只有单一的线程修改变量的值 和 变量不须要与其余的状态变量共同参与不变约束 就要经过加锁(使用 synchronize 或 java.util.concurrent 中的原子类)来保证原子性。

禁止指令重排序优化。
经过插入内存屏障保证一致性。

3.1.3 对于 long 和 double 型变量的特殊规则

Java 要求对于主内存和工做内存之间的八个操做都是原子性的,可是对于 64 位的数据类型,有一条宽松的规定:容许虚拟机将没有被 volatile 修饰的 64 位数据的读写操做划分为两次 32 位的操做来进行,即容许虚拟机实现选择能够不保证 64 位数据类型的 load、store、read 和 write 这 4 个操做的原子性。这就是 long 和 double 的非原子性协定。

3.1.4 原子性、可见性与有序性

回顾下并发下应该注意操做的那些特性是什么,同时加深理解。

原子性(Atomicity)
由 Java 内存模型来直接保证的原子性变量操做包括 read、load、assign、use、store 和 write。大体能够认为基本数据类型的操做是原子性的。同时 lock 和 unlock 能够保证更大范围操做的原子性。而 synchronize 同步块操做的原子性是用更高层次的字节码指令 monitorenter 和 monitorexit 来隐式操做的。

可见性(Visibility)
是指当一个线程修改了共享变量的值,其余线程也可以当即得知这个通知。主要操做细节就是修改值后将值同步至主内存(volatile 值使用前都会从主内存刷新),除了 volatile 还有 synchronize 和 final 能够保证可见性。同步块的可见性是由“对一个变量执行 unlock 操做以前,必须先把此变量同步会主内存中( store、write 操做)”这条规则得到。而 final 可见性是指:被 final 修饰的字段在构造器中一旦完成,而且构造器没有把 “this” 的引用传递出去( this 引用逃逸是一件很危险的事情,其余线程有可能经过这个引用访问到“初始化了一半”的对象),那在其余线程中就能看见 final 字段的值。

有序性(Ordering)
若是在被线程内观察,全部操做都是有序的;若是在一个线程中观察另外一个线程,全部操做都是无序的。前半句指“线程内表现为串行的语义”,后半句是指“指令重排”现象和“工做内存与主内存同步延迟”现象。Java 语言经过 volatile 和 synchronize 两个关键字来保证线程之间操做的有序性。volatile 自身就禁止指令重排,而 synchronize 则是由“一个变量在同一时刻指容许一条线程对其进行 lock 操做”这条规则得到,这条规则决定了持有同一个锁的两个同步块只能串行的进入。

6. 虚拟机类加载机制


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

在 Java 语言中,类型的加载、链接和初始化过程都是在程序运行期间完成的。

6.1 类加载时机
类的生命周期( 7 个阶段)

 

6.2 类的加载过程

6.2.1 加载

经过一个类的全限定名来获取定义次类的二进制流(ZIP 包、网络、运算生成、JSP 生成、数据库读取)。
将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个表明这个类的 java.lang.Class 对象,做为方法去这个类的各类数据的访问入口。
数组类的特殊性:数组类自己不经过类加载器建立,它是由 Java 虚拟机直接建立的。但数组类与类加载器仍然有很密切的关系,由于数组类的元素类型最终是要靠类加载器去建立的,数组建立过程以下:

若是数组的组件类型是引用类型,那就递归采用类加载加载。
若是数组的组件类型不是引用类型,Java 虚拟机会把数组标记为引导类加载器关联。
数组类的可见性与他的组件类型的可见性一致,若是组件类型不是引用类型,那数组类的可见性将默认为 public。
内存中实例的 java.lang.Class 对象存在方法区中。做为程序访问方法区中这些类型数据的外部接口。
加载阶段与链接阶段的部份内容是交叉进行的,可是开始时间保持前后顺序。

6.2.2 验证

是链接的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机要求。

文件格式验证

是否以魔数 0xCAFEBABE 开头
主、次版本号是否在当前虚拟机处理范围以内
常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
指向常量的各类索引值中是否有指向不存在的常量或不符合类型的常量
CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
Class 文件中各个部分集文件自己是否有被删除的附加的其余信息
……
只有经过这个阶段的验证后,字节流才会进入内存的方法区进行存储,因此后面 3 个验证阶段所有是基于方法区的存储结构进行的,再也不直接操做字节流。

元数据验证

这个类是否有父类(除 java.lang.Object 以外)
这个类的父类是否继承了不容许被继承的类(final 修饰的类)
若是这个类不是抽象类,是否实现了其父类或接口之中要求实现的全部方法
类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)
这一阶段主要是对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。

字节码验证

保证任意时刻操做数栈的数据类型与指令代码序列都鞥配合工做(不会出现按照 long 类型读一个 int 型数据)
保证跳转指令不会跳转到方法体之外的字节码指令上
保证方法体中的类型转换是有效的(子类对象赋值给父类数据类型是安全的,反过来不合法的)
……
这是整个验证过程当中最复杂的一个阶段,主要目的是经过数据流和控制流分析,肯定程序语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,保证校验类的方法在运行时不会作出危害虚拟机安全的事件。

符号引用验证

符号引用中经过字符创描述的全限定名是否能找到对应的类
在指定类中是否存在符方法的字段描述符以及简单名称所描述的方法和字段
符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
……

6.2.3 准备

这个阶段正式为类分配内存并设置类变量初始值,内存在方法去中分配(含 static 修饰的变量不含实例变量)。

public static int value = 1127;
这句代码在初始值设置以后为 0,由于这时候还没有开始执行任何 Java 方法。而把 value 赋值为 1127 的 putstatic 指令是程序被编译后,存放于 clinit() 方法中,因此初始化阶段才会对 value 进行赋值。

6.2.4 解析

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

6.2.5 初始化

前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。

6.3.1 双亲委派模型

从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另外一种是其余全部类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)

    1. 启动类加载器
      加载 lib 下或被 -Xbootclasspath 路径下的类

    2. 扩展类加载器
      加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类

    3. 引用程序类加载器
      ClassLoader负责,加载用户路径上所指定的类库。

 

除顶层启动类加载器以外,其余都有本身的父类加载器。
工做过程:若是一个类加载器收到一个类加载的请求,它首先不会本身加载,而是把这个请求委派给父类加载器。只有父类没法完成时子类才会尝试加载。

 

6.3.2 破坏双亲委派模型

keyword:线程上下文加载器(Thread Context ClassLoader)

相关文章
相关标签/搜索