JVM知识点总结(全网最详细)!!!!

JVM知识总结

运行时数据区域

JVM叫做java虚拟机,它在执行java程序的过程中会把它所管理的内存划分为若干个不同的区域,这些区域各自有各自的用途以及特性,将它们成为 运行时数据区域
在这里插入图片描述
其中共享的区域有:方法区、堆
独有的区域有:虚拟机栈、本地方法栈、程序计数器

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它被看作是当前线程所执行的字节码的行号指示器。线程私有的
功能:记录线程的状态,记录指令的行号,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
它是唯一一个不会发生OutOfMemoryError情况的区域

它记录的东西有所不同
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)

OutOfMemoryError:发生内存数据溢出的错误

Java虚拟机栈

Java虚拟机栈也是线程私有的,与线程”同生共死“,也就是说和 线程的生命周期一样
功能(特点):每有一个方法 被执行,Java虚拟机栈就会创建一个栈帧,用于存储局部变量表操作数栈动态连接方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

局部变量表

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

这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型占用一个。局部变量表所需的内存空间在编译期间完成分配,

StackOverflowError异常和OutOfMemoryError异常

StackOverflowError异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
OutOfMemoryError异常:如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

Java堆

Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域。
功能:存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存
Java堆还是垃圾收集器管理的内存区域。因为垃圾回收,回收的是不再被程序中的任何引用变量引用的对象占用的内存
从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(TLAB),来提升对象分配时的效率

将Java堆细分的目的就是,为了更好的回收内存,或者更快的分配内存。

方法区

方法区也是线程所共享的区域
功能:用于存储已被虚拟机加载的类型信息常量静态变量即时编译器编译后的代码缓存等数据

运行时常量池

它是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,也就是说:
并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法

对象的创建

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

对象的内存分配

从Java堆的内存是否规整的角度分为两种方法:
1.(规整)指针碰撞
所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump ThePointer)
2.(不规整)空闲列表
虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)

选择哪种分配方式Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。

安全角度考虑:
1.CAS失败重试
对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败
重试的方式保证更新操作的原子性
2.本地线程分配缓冲
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local AllocationBuffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。

对象的内存布局

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

对象头

对象头包括两类信息,第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,称它为"Mark Word".
Mark Word被设计成一个有着动态定义的数据结构,它含有锁的结构
在这里插入图片描述

第二类是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例
【注意】
如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小

实例数据

实例数据是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。

对齐填充

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

对象的访问定位

创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象
由于reference类型,只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问方式也是由虚拟机实现而定的
主流的 访问方式主要有使用句柄直接指针两种

使用句柄

Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。具体如下图所示:
在这里插入图片描述

直接指针

Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,具体如下图所示:
在这里插入图片描述

使用句柄和直接指针的优缺点

使用句柄
优点reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要改变。
缺点:增加了一次指针定位的时间开销。

直接指针:
优点:节省了一次指针定位的开销。
缺点:在对象被移动时reference本身需要被修改。

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

垃圾回收,回收的是不再被程序中的任何引用变量引用的对象占用的内存

判断对象的生死

1.引用计数法

原理

引用计数法的原理很简单,是 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
优点:原理简单,效率很高
缺点:无法解决循环问题

2.可达性分析算法

原理

通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的
    参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
    NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

引用

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱

1.强引用

强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象

2.软引用

软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。

3.弱引用

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。

4.虚引用

虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。

回收方法区

方法区的垃圾收集主要回收两部分内容:废弃的常量不再使用的类型

分代收集理论

1.弱分代假说

内容:绝大多数对象都是朝生夕灭的

2.强分代假说

内容:熬过越多次垃圾收集过程的对象就越难以消亡。
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

3.跨代引用假说

内容:跨代引用相对于同代引用来说仅占极少数。
依据这条假说,我们可以不用为了少量的跨代引用而去扫描整个老年代,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描

垃圾收集算法

1.标记—清除算法

最早出现也是最基础的垃圾收集算法是“标记-清除”(Mark-Sweep)算法。
内容:算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程,
缺点:1.执行效率不稳定
2.内存空间的碎片化过程

标记-清除算法的执行过程如图所示
在这里插入图片描述

2.标记—复制算法

内容:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:实现简单、运行高效
缺点:可用内存缩小为原来的一半,空间浪费多

标记-复制算法的执行过程如图所示
在这里插入图片描述

现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代

标记—复制算法的优化

在1989年,Andrew Appel针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略,现在称为“Appel式回收”。
具体做法:把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,即每次新生代中可用内存空间为整个新生代容量的90%。
Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)

分配担保如何实现?
如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代

3.标记—整理算法

内容:针对老年代对象的存亡特征,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

标记—整理算法的示意图如图所示
在这里插入图片描述

标记—清除算法与标记—整理算法的区别

标记—清除算法是一种非移动式的回收算法,而整理算法是移动式的。

“Stop The World”

如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,这种停顿被称为“Stop The World”。

安全点

实际上HotSpot也的确没有为每条指令都生成OopMap,只是在“特定的位置”记录了这些信息,这些位置被称为安全点(Safepoint)。

安全点选取的标准

安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的
长时间执行”的最明显特征就是指令序列的复用,例如方法调用循环跳转异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点

如何在垃圾收集发生时让所有线程都跑到最近的安全点?

1.抢先式中断

抢先式中断不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。

2.主动式中断

主动式中断的思想是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。

安全区域

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点

记忆集和卡表

记忆集

记忆集是一种用于记录从非收集区域指向收集区域的指针集合抽象数据结构。
在实现记忆集的时候,便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范围以外的)的记录精度:

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个
    精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

第三种“卡精度”所指的是用一种称为“卡表”(Card Table)的方式去实现记忆集[1],这也是目前最常用的一种记忆集实现形式

经典的垃圾收集器

1.Serial收集器

Serial收集器是最基础、历史最悠久的收集器,这个收集器是一个单线程工作的收集器,它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
在这里插入图片描述

2.ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,其他与Serial收集器基本完全一样。
ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但是尤其是JDK 7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作

ParNew可以说是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器。

在这里插入图片描述

3.Parallel Scavenge收集器

同样是基于标记-复制算法实现的收集器,也是能够并行收集多线程收集器。
特点:它所关注的是可控制的吞吐量,而其他收集器关注的是缩短垃圾收集时用户线程的停顿时间

吞吐量:处理器用于运行用户代码的时间与处理器总消耗时间的比值
在这里插入图片描述
停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算不需要太多交互的分析任务。

Parallel Scavenge收集器也经常被称作“吞吐量优先收集器

垃圾收集的自适应的调节策略

虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略。

Parallel Scavenge收集器 区别与ParNew收集器的一个重要特性

自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性

4.Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

5.Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。

注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

6.CMS收集器(重点)

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现的。又被成为“并发低停顿收集器”。并发收集、底停顿
它的运作过程分为四个步骤:

1.初始标记
2.并发标记
3.重新标记
4.并发清除

其中初始标记重新标记这两个步骤仍然需要“Stop The World”。
初始标记:仅仅只是标记一下GCRoots能直接关联到的对象,速度很快

并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行

重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间比初始标记稍长一些但远不如其他两个阶段

并发清除:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

并发标记和并发清除两个阶段是耗时时间最长的,但是这两个阶段可以与用户线程并发运行。

CMS收集器的缺点
  1. CMS收集器对处理器资源非常敏感
  2. CMS收集器无法处理“浮动垃圾”
  3. 会产生大量的空间碎片,因为他是基于标记—清除算法实现的

7.G1收集器(重点)

Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。G1是一款主要面向服务端应用的垃圾收集器。

JDK 8 Update 40的时候,G1提供并发的类卸载的支持,补全了其计划功能的最后一块拼图。这个版本以后的G1收集器才被Oracle官方称为“全功能的垃圾收集器

特征:是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒
G1开创的基于Region的堆内存布局是它特征实现的关键

与其他收集器不同的地方
其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域

它的运行过程分为四个步骤:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

最后的两款收集器 Shenandoah和ZGC

几乎整个工作过程全部都是并发的,只有初始标记、最终标记这些阶段有短暂的停顿,这部分停顿的时间基本上是固定的,与堆的容量、堆中对象的数量没有正比例关系。
这两款目前仍处于实验状态的收集器,被官方命名为“低延迟垃圾收集器

内存分配策略

1.对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

2.大对象直接进入老年代

大对象是指需要大量连续内存空间的Java对象。

在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高额的内存复制开销。

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

多数收集器都采用了分代收集来管理堆内存

为了决策再回收时,哪些存活对象放在新生代,哪些存活对象放在老年代,为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

4.动态对象年龄判定

为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。

5.空间分配担保

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。

  • 如果这个条件成立,那这一次Minor GC可以确保是安全的。
  • 如果不成立,则虚拟机会先查看 >-XX:HandlePromotionFailure 参数的设置值是否允许担保失败(HandlePromotion Failure)
  • 如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小.
  • 如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;
  • 如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

JVM类加载机制

类加载的时机

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。
这七个阶段的发生顺序如图所示
在这里插入图片描述
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定。
对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

1.遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:

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

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

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

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

5.当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句
柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

6.当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

类加载的过程

加载

“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,
在加载阶段,Java虚拟机需要完成以下三件事情:
1.通过一个类的全限定名来获取定义此类的二进制字节流。

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

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

验证

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证元数据验证字节码验证符号引用验证

准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段.

首先是这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值.

以下列出了Java中所有基本数据类型的零值
在这里插入图片描述

“特殊情况”
如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,假设上面类变量value的定义修改为:

public static final int value = 123;

编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置将value赋值为123。

解析

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

  • 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。
  • 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行,分别对应于常量池的CONSTANT_Class_info、CON-STANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dyna-mic_info和CONSTANT_InvokeDynamic_info 8种常量类型

初始化

类的初始化阶段是类加载过程的最后一个步骤。
我们可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器< clinit >()方法的过程。< clinit > ()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物,但我们非常有必要了解这个方法具体是如何产生的,以及
< clinit >()方法执行过程中各种可能会影响程序运行行为的细节,这部分比起其他类加载过程更贴近于普通的程序开发人员的实际工作。

  • < clinit >()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如代码
public class Test {
	static {
	i = 0; // 给变量复制可以正常编译通过
	System.out.print(i); // 这句编译器会提示“非法向前引用”
	}
static int i = 1;
}
  • < clinit >()方法与类的构造函数(即在虚拟机视角中的实例构造器< init >()方法)不同,它不需要显
    式地调用父类构造器,Java虚拟机会保证在子类的< clinit >()方法执行前,父类的< clinit >()方法已经执行
    完毕。因此在Java虚拟机中第一个被执行的< clinit >()方法的类型肯定是java.lang.Object。
    -由于父类的< clinit >()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如代码清单7-6中,字段B的值将会是2而不是1。

< clinit >()方法执行顺序

static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
  • < clinit >()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成< clinit >()方法。
  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成< clinit >()方法。但接口与类不同的是,执行接口的< clinit >()方法不需要先执行父接口的()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也
    一样不会执行接口的< clinit >()方法。
  • ·Java虚拟机必须保证一个类的< clinit >()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的< clinit >()方法,其他线程都需要阻塞等待,直到活动线程执行完毕()方法。如果在一个类的()方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。下面是代码演示
static class DeadLoopClass {
	static {
	// 如果不加上这个if语句,编译器将提示“Initializer does not complete 		normally”并拒绝编译
		if (true) {
			System.out.println(Thread.currentThread() + "init DeadLoopClass");
				while (true) {
			}
		}
	}
}
public static void main(String[] args) {
	Runnable script = new Runnable() {
	public void run() {
			System.out.println(Thread.currentThread() + "start");
			DeadLoopClass dlc = new DeadLoopClass();
			System.out.println(Thread.currentThread() + " run over");
		}
	};
	Thread thread1 = new Thread(script);
	Thread thread2 = new Thread(script);
	thread1.start();
	thread2.start();
}

类加载器

Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。

双亲委派模型

站在Java虚拟机的角度来看,只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分
  2. 其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

站在Java开发人员的角度来看,类加载器就应当划分为三层类加载器双亲委派的类加载架构。
针对JDK 8及之前版本的Java来介绍什么是三层类加载器,以及什么是双亲委派模型。对于这个时期的Java应用,绝大多数Java程序都会使用到以下3个系统提供的类加载器来进行加载。

  1. 启动类加载器(Bootstrap Class Loader):前面已经介绍过,这个类加载器负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。

  2. 扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。

  3. 应用程序类加载器(Application Class Loader):这个类加载器由
    sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

                        类加载器双亲委派模型
    

在这里插入图片描述
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码

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

破坏双亲委派模型

双亲委派模型主要出现过3次较大规模“被破坏”的情况:

  1. 第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2面世以前的“远
    古”时代。由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在
    loadClass()中编写代码。上节我们已经分析过loadClass()方法,双亲委派的具体逻辑就实现在这里面,按照loadClass()方法的逻辑,如果父类加载失败,会自动调用自己的findClass()方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。
  2. 第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类
    加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在。
  3. 第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态
    性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。对于个人电脑来说,重启一次其实没有什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发者,尤其是大型系统或企业级软件开发者具有很大的吸引力。

感谢大家的观看,有错误的地方请大家指正,谢谢大家!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值