JVM整理

自己这段时间去看了深入理解Java虚拟机这一本书,一方面是因为秋招面试吃尽了苦头,一方面也是自己想去整理和学习一些知识,就在这里对于JVM一些总结和整理。

java内存区域

在这里插入图片描述其中最重要的就是其中的五大部分的结构,分别是程序计数器,虚拟机栈,本地方法栈,堆,方法区。下面来具体的介绍一下。

程序计数器(Program Counter Register)

程序计数器是一个块很小的内存空间(他不在RAM上面,他是在CPU上面的,程序员是无法去控制他的),他可以看做当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更加高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复都是依赖这个计数器来完成的。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器的执行时间的方式里实现的,给定一个确定的时刻,一个处理器(对于多核处理器来说就是一个内核),都只会执行一条线程中的指令。因此为了线程切换以后能够恢复到正确的执行位置,每一个线程都需要一个独立的程序计数器,各个线程之间计数器互不影响,独立储存,我们称这类的区域称作“线程私有”的内存。
如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是native方法,这个计数器的值则为空(Undefined)。此内存区域时唯一个Java虚拟机规范中没有规定任何的OutOfMemoryError的区域。

Java虚拟机栈(Java Virtual Machine Stacks)

与程序计数器一样,Java虚拟机栈也是线程私有,他的生命周期和线程的相同。虚拟机栈描述的是Java方法执行的内存模型:每一个方法在执行的时候会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用到执行完成的过程,就对应这个一个栈帧从入栈到出栈的过程。
人们之前常说的Java内存结构分为栈内存(stack)和堆内存(head),这里就是那个栈了,或者是说是局部变量表部分。
局部变量表存放了编译期可知的各种基本数据类型,对象引用(reference类型,他不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向了一个代表对象的句柄活其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
其中64位长度的long和double会占据两个局部变量空间(Slot),其余的数据类型只占1个。局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小的。
在Java虚拟机规范中,对于这个区域规定了两种异常,如果线程请求的栈的深度大于虚拟机所允许的栈的深度,将抛出StackOverflowError异常;如果虚拟机栈是可以动态扩展的(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展到无法申请足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈(Native Method Stack)

本地方法栈与虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。在虚拟机规范中对于本地方法栈中方法使用的语言,使用方式与数据结构并没有强制的规定,具体的虚拟机可以自由的实现。甚至有的虚拟机将虚拟机栈和本地方法栈合二为一(Sun 公司的HotSpot虚拟机),和虚拟机栈一样都会抛出StackOverflowError和OutOfMemoryError异常。

Java堆(Java Heap)

对于大多数应用来说,Java堆是虚拟机所管理的内存中最大的一块。是所有线程共享的内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有对象的实例都是在这里分配内存。
根据Java虚拟机规范的规定,Java堆可以在物理空间上不连续,只要逻辑上连续就可以,就像我们的磁盘空间,在实现时,既可以实现成固定大小的,也可以是扩展的,不过当前主流的虚拟机都是按照可扩展的来实现的,如果堆中没有内存完成实例的分配,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。

方法区(Method Area)

与堆一样,是线程共享的内存区域,它用于存储已被加载的类的信息,常量,静态变量,即时编译器编译后的代码等数据。
虚拟机规范对于方法区的限制非常宽松,除了和Java堆一样不需要连续的物理内存和可选择固定大小和可扩展以外,可以选择不实现垃圾收集。
根据根据Java虚拟机规范里面要求的,当方法区无法满足内存分配需求时,将抛出OutOFMemoryError异常。

变化

在jdk1.7及之前的时候,方法区=永久代,Jdk1.8变成了元空间(metaspace)
字符串常量池,jdk1.6在方法区,1.7去了堆,1.8去了元空间

对象的新建过程

虚拟机遇到一条new指令,首先去检查这个指令的参数能否在运行时常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。如果没有,那必须先执行类的初始化过程。
类加载检查通过后,虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后便可以完全确定,为对象分配空间就是从java堆中划分出一块确定大小的内存而已。这个地方会有两个问题:
如果内存是规整的,那么虚拟机将采用的是指针碰撞法 来为对象分配内存。意思是所有用过的内存在一边空闲的内存在另一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是吧指针向空闲那边挪动一段与对象相等的距离。 如果垃圾收集器选择的是Serial,ParNew这种基于压缩算法的,虚拟机采用的就是这种分配方式。
如果内存不规整,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。通俗来说,就是JVM维护了一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 。如果垃圾收集器选择的是CMS这种基于标记-清除算法 的,虚拟机采用这种分配方式。
还有一个问题就是及时保证new对象时候的安全性 ,因为虚拟机正在给对对象A分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针来分配内存的情况。 虚拟机采用了CAS+失败重试的方式保证更新操作的原子性和TLAB两种方式来解决这个问题。
内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头) 。这一步保证了对象的实例字段在java代码中不用赋初值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值(默认值)。
对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的hash值,对象的gc分代年龄等信息,这些信息存在在对象的对象头中。
执行 < init > 方法,把对象按照程序员的意思进行初始化,这样一个对象才算完全生产出来

对象的访问

对象的访问方式是取决于虚拟机实现而定的,目前主流的访问方式有使用句柄和直接指针两种。

  1. 句柄访问
    在这里插入图片描述如果使用句柄访问的话,那么Java堆中将会划分一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
  2. 指针访问
    在这里插入图片描述
    如果使用指针直接访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
    优点与缺点
    使用句柄来访问的好处:
    reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
    使用直接指针访问方式的好处:
    速度更快,他节省了一次指针定位的时间开销,由于对象的访问在java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
    就HotSpot而言,它是使用第二种方式进行对象访问的,从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。

如何判断对象“死活”?

引用计数法(Reference Counting)

给对象添加一个引用计数器,每当有一个地方引用他计数器的值就加一,当引用失效计数器就减一,任何时刻计数器的之为0的对象就是不可能在被使用的,

难以解决的问题

对象之间相互循环引用的问题

可达性分析算法(Reachability Analysis)

在使用的(Java C#,lisp)都是通过可达性分析来判断对象是否存活。这个算法的基本思路竟是通过一系列的称为GC Roots的对象对为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots 没有任何引用链项链时,证明此对象是不可用的。

Java语言中GC Roots的对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地房发展中JNI(native方法)引用的对象

引用

JDK1.2以后,Java对于引用有了新的概念,分为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference)
强引用指的是代码中普遍存在的,Object obj=new Object();只要强引用还在,垃圾回收就永远不会回收。
软引用用来描述一些有用但是非必需的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存就会抛出内存溢出异常。JDK1.2以后提供了SoftReference类来实现。
弱引用也是用来描述非必需的对象的,但是要比软引用要弱,被弱引用关联的对象只能够生存到下一次垃圾回收发生之前。当垃圾收集器工作室,无论内存是否足够,都会回收掉只被弱引用关联的对象,JDK1.2以后,提供了WeakReference来实现。
虚引用也称作幽灵引用或者是幻影引用,最弱的一种引用,一个对象是否有虚引用的存在完全不会对其生存周期造成影响,也无法通过虚引用来取得一个对象实例,为一个对象设置的目的是为了在这个对象被垃圾回收的时候收到一个系统通知,JDK1.2以后,提供了PhantomReference来实现。

对象死亡的过程

当对象在可达性分析中不可达的时候也并不是标着这这个对象一定会被垃圾回收死亡有一个过程:如果对象在可达性分析之后发现没有有GCRoots相连接的引用链,那她就会被第一次标记并且进行一次筛选,筛选的条件是这对象是否有必要执行finalize方法,当对象没有覆盖finalize方法(没有重写),或者finalize方法已经被虚拟机调用过,这两张情况虚拟机都是为没有必要去执行。
如果这个对象被判定为有必要执行finalize方法,那么这个对象会被放置在一个叫做F-Queue的队列中,并且稍后由一个虚拟机自动建立,低优先级的Finalizer线程去执行他,所谓的执行是指虚拟机会触发这个方法,但是不承诺活等待他运行结束,这样做的原因,他如果这个对象在finalize方法中执行缓慢,或者发生了死循环,将会导致整个F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize是对象逃脱死亡的最后一次机会,稍后GC会对F-Queue中的对象进行第二次小规模的标记,如果对想要在finalize中成功的拯救自己,只需要重新与引用脸上的任何一个对象建立关联即可,譬如(this关键字)把自己赋值给某个类变量或独享的成员变量,那在第二次标记时他会被移除即将回收的集合,如果这个对象还没有逃脱那基本上他就是要被回收了。
尽量避免去使用,finalize能够做到的事情,tryfinally也可以去完成,

回收方法区

很多人认为方法区(或者是HotSpot虚拟机中的永久代)是没有垃圾回收的,虚拟机规范中也是确实说过不要求虚拟机在方法去实现垃圾收集的,而且在方法区中进行垃圾收集确实性价比也比较低。
永久代垃圾回收主要是两部分:废弃常量和无用的类。判断一个常量是否是无用常量和回收java堆中的对象非常类似,判断一个类是否是无用类条件相对而言比较苛刻同时满足下面三个条件:
该类的所有实例都已经被回收了,也就是Java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收了
该类对应的Java.Lang,Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
满足上面的条件仅仅是可以被回收,而不是不再使用就会被回收

垃圾收集算法

标记清除算法(Mark-Sweep)

分为标记和清除两个阶段,:首先标记出所有需要回收的对象,再标记完成后统一回收所有被标记的对象,他的标记过程就是标记出和GCRoots相连的对象。
不足:
一效率问题。标记和清除的效率都不高。
二空间问题,标记清楚以后会产生大量的不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大的对象是,无法找到连续的内存而不得不触发另一次垃圾收集动作

在这里插入图片描述

复制算法(copying)

把可用的内存分为大小相等的凉快,每一次只使用其中的一款举哀,当一块的内存用完了,就将还存活的对象复制到另外一个上面,这样每一次都对一般的区域进行回收不用考虑内存碎片的问题,但是这个代价是把自己的内存缩小了原来的一般。
在这里插入图片描述(现在商业虚拟机使用的这一种算法回收新生代(Java堆中存放对象,分为老年代和新生代)将内存分为一块较大的Eden和两块较小的Survivor,比例是8:1:1,每一次使用的都是Eden和一块Survivor,当回收时,将Eden和一块Survivor存活的对象移动到另外一块Survivor,如果Survivor空间不够的时候会使用老年代的一点东西,进行分配担保)
我想在这里去去叙述一下完成的复制算法:有一个很有意思的地方,每当小的对象被新建,这个对象会分配到Eden区,然后呢Eden区满了以后然后,会触发一次新生代的垃圾回收(minor GC),

标记整理

根据老年代的特点,提出了标记整理这种算法,标记过程和其他的类似,但是后续步骤不是对可回收对象直接进行清理,而是让所有存活的对象都想一端移动,然后直接清理掉段边界以外的内存。
在这里插入图片描述

分代收集算法

因为新生代中会有大量的对象死亡,只有少量存活,所以采用复制算法。
老年代对象存活率高,没有额外空间对于对他进行分配担保就必须采用标记清除或者标记整理算法

垃圾收集器

并行和并发

垃圾回收的时候并行和并发
并行:指的是多条线程一起进行垃圾回收。
并发:一遍垃圾回收一遍工作。

Minor GC和FullGC

新生代GC(Minor GC):指的是新生代的GC,非常频繁,回收速度比较快,
老年代GC(Major GC或者Full GC)出现Major GC经常会伴随着至少一次MinorGC(但非绝对)。MajorGC一般比MinorGC慢10倍以上

长期存活的对象和较大的对象进入老年代

长期存活的对象,如果对象在Eden出生并且经过一次Minor GC仍然存活并且能被Survivor容纳,每熬过一次Minor年龄就增加一岁,15岁进会被晋升到老年代。

Serial收集器

发展历史最悠久的,在JDK1.3.1之前是新生代的唯一选择,单线程的收集器,在垃圾回收的时候必须暂停所有的工作Stop The World,,直到他收集结束,这项工作时后台自动发动和自动完成的。
新生代采用复制算法,老年代采用标记整理算法。
虽然是单线程的,但是仍是虚拟机运行在Client模式下的默认新生代收集器。
优点:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,因为Serial没有线程之间切换,在永华的桌面应用场景中停顿的时间可以控制在几十毫秒最多一百多毫秒。

ParNew收集器

是Serial的多线程版本,除了使用多条线程进行垃圾收集之外,其他的收集算法,stop the world 算法等都和Serial一样,两者共用了不少的代码
新生代复制算法,老年代标记整理算法
ParNew除了多线程收集之外其他没有创新,但是这个却是很多的Server模式下的虚拟机中首选的新生代收集器。还有一个很重要的原因是,除了Serial以外只有他可以和CMS合作。

Parallel Scavenge收集器

新生代收集器,目标是达到一个可控制的吞吐量(Throughtput)。所谓的吞吐量就是CPU用于运行用户代码的时间和CPU总消耗的时间的比值,即吞吐量=(运行用户代码时间)/(运行用户代码时间+垃圾回收时间)。
停顿时间越短越是和需要和用户需要交互的程序,良好的响应速度能提升用户体验,而高吞吐量可以高效的利用CPU时间,尽快的完成程序的运算任务,主要适合在后台的运算而不需要太多的交互的任务。

Serial Old收集器

是Serial的老年代版,单线程,标记整理算法,主要是给Client模式下的虚拟机使用的。
在server模式下两种作用:在jdk1.5之前和Parallel Scavenge收集器搭配使用另一种用途是做CMS的收集器的后备

Parallel Old收集器

是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法,jdk1.6开始提供的。
在此之前,Parallel Scavenge比较尴尬
新生代使用Parallel Scavenge,老年代使用只能够使用Serial Old,但是因为Serial Old,太差了,在这个Parallel Old出来以后在注重CPU吞吐量的地方以及CPU资源敏感的场合都优先考虑这种组合,

CMS收集器(Concurrent Mark Sweep)

是一种以获取最短回收停顿时间为目标的收集器,目前很大一部分的Java应用几种在互联网站或者B/s系统的服务端上,这类应用非常重视服务的响应速度,希望系统停顿时间最短。
基于标志清除算法实现的
分为四个步骤
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
初始标记重新标记都是需要Stop The World
初始标记仅仅是标记一下GCRoots能够关联的对象,速度很快,并发阶段就是GCRoots Tracing的过程,而重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致的标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会远比初始标记阶段稍长一些,但是远比并发标记时间短。
在这里插入图片描述由于整个过程中最耗时的并发标记和并发清除过程,收集器线程都可以和用户线程一个工作,所以从总体上来说CMS是内存回收和用户线程是一起并发的。
优点:并发收集,低停顿
缺点:对于CPU资源很敏感,
无法处理浮动垃圾(Floating Garbage),可能会出现,Concurrent Mode Failure失败导致另外一次Full GC
因为选取的是标记清除所以会有大量的空间碎片

G1收集器(Garbage First)

面向服务端,
优点:
并行和并发:利用多核cpu缩短stop-the-world,可以通过并发的方式使Java程序在运行的同时继续进行GC
分代收集:不需要其他收集器的配合就可以完成全部的,但是他会使用不同的方式去处理不同的对象
空间整合:整体上实现的标记整理算法,从局部的话就是基于复制算法,保证没有空间碎片
可预测性的停顿:可以建立可预测的停顿时间模型,能够让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。
使用G1的时候是对于整个Java堆进行垃圾回收,将整个Java堆划分为大小相等的独立区域(Region),虽然还保留这新生代和老年代的概念,大师新生代和老年代已经没有物理隔离了。
如果不考虑维护RememberedSet的操作,G1收集器的运作流程大概划分为下面的几个阶段
初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)
初始化标记是仅仅只是标记一下GCRoots能够关联的对象并且修改TAMS(Next Top at Mark Start)的值,让下一程序并发运行时,能在正确可以用的Region中创建新的对象,这阶段血药停顿线程但是耗时很短。
并发标记阶段阶段是从GCRoots开始对堆中的对象进行客单性分析找出存活的对象,耗时较长但是这个是和用户并发执行。
最终标记是为了修正并发标记期间因用户程序继续运作而导致的标记产生变动的那一部分对象的标记记录,这阶段需要停顿线程但是可并行执行。
最后在筛选回收极端首先对哥哥Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划

Class文件

Class文件的头四个字节称为魔数(Magic Number)
值为0xCAFEBABE(也就是咖啡宝贝)
在向后的四个字节是Class文件的版本号,第五个第六个字节数次版本号(Minor Version),第七和第八是主版本号(Major Version)

类加载

分为七个过程,分别是加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using),卸载(Unloading)。其中验证准备解析这三个阶段称为连接(Linking)
在这里插入图片描述
其中加载,验证,准备,初始化,卸载这五个阶段顺序是确定,类加载必须按照这种顺序按部就班的去完成,解析就不一样。这些阶段通常是交叉进行的
虚拟机规范中严格规定了5种情况必须立即对类进行初始化
1) 遇到new,getstatic,putstatic,或者是invokestatic这四条指令时,,如果雷没有进行过初始化,就需要先触发其初始化,生成上面四条指令的最常见的场景就是使用new实例化对象时。
2) 使用java.lang.reflect包对于进行反射调用的时候,如果没有初始化,先去初始化。
3) 当初始化一个类时,发现父类没有进行初始化,先触发对其父类的初始化。
4) 当虚拟机启动的时候,用户需要去指定一个要执行的主类(main方法的那个类),虚拟机会先去初始化这个类。
5) 当时用JDK1.7的动态语言支持时,如果一个Java.lang.invoke.MethodHandle的实例最后解析的结果是REF_getstatic,REF_petstatic,REF_invokestatic,方法句柄,并且这个句柄没有被初始化,就先去触发这个的初始化。
虚拟机规范中规定上五个场景的形容词是有且只有,上面五个场景称为对于一个类进行主动引用。

类加载的过程

加载

在加载的阶段,需要完成一下的三件事
1) 通过一个类的全限定类名来获取此类的二进制字节流。
2) 将通过这个字节流所代表的静态储存结构转化为方法区的运行时数据结构。
3) 在内存中生成一个代表这个类的java.lang.Class对象,作为方法去这个类的各种数据的访问入口。
相对于类加载的其他过程,一个非数组类的加载过程(是在加载阶段中获取类的二进制字节流的动作)的开发人员可控性最强的,因为加载阶段既可以使用用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,卡法人员可以同股哟定义自己的类加载器去控制字节流的获取方式(即重写一个LoadClass()方法)
对于数组而言情况不同,数据类本身不通过类加载器创建,它是由Java虚拟机直接创建。但是数组类与类加载器仍然有很密切的关系,因为数组类的元素类型(数组中的元素)最终是要靠类加载器去创建,一个数组类创建过程遵循以下的规则。
如果是引用型,递归采用本节中定义的加载过程去加在这个类型,数组将在加载该组件类型的类加载器的类名称空间上被标记。
如果是基本型,虚拟机会把数组标记为与引导类加载器关联。
数组的可见性与他的组件类型的可见性应该保持一致。,如果是基本型那他的可见性默认是public。
内存中实例的 java.lang.Class 对象存在方法区中。作为程序访问方法区中这些类型数据的外部接口。
加载阶段与连接阶段的部分内容是交叉进行的,但是开始时间保持先后顺序。

验证

大致分为四个阶段
文件格式的验证
元数据的验证
字节码的验证
符号引用的验证

准备

准备阶段是正式为了类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。首先这里的变量仅仅是类变量(static)不包括实例变量,通常情况是指的是数据类型的零值。
也会特殊情况final,就会不是零值。

解析
初始化

类加载器

从虚拟机的角度来说,只存在两种不同的加载器,一种是启动类加载器(Bootstrap ClassLoader),这个加载器是使用C++实现的,是虚拟机的一部分;另一个是所有类的加载器,这些类加载器都是由Java语言实现,独立于虚拟机外部,并且全部都继承自抽象类java.lang.ClassLoader
从Java开发人员来看,类加载器还可以划分为以下三种,绝大多数的Java程序程序都会使用以下三种系统提供的类加载器。
启动类加载器(Bootstrap ClassLoader)
加载 lib 下或被 -Xbootclasspath 路径下的类
扩展类加载器(Extension ClassLoader)
加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类
引用程序类加载器(Application ClassLoader)
ClassLoader负责,加载用户路径上所指定的类库。

双亲委派(Parents Delegation Model)

双亲委派模型要求除了顶层启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器的父子关系一般不会以继承的关系来实现,而是通过组合关系复用父类加载器。
继承与组合都是面向对象中代码复用的方式。父类的内部细节对子类可见,其代码属于白盒式的复用,而组合中,对象之间的内部细节不可见,其代码属于黑盒式复用。继承在编码过程中就要指定具体的父类,其关系在编译期就确定,而组合的关系一般在运行时确定。继承强调的是is-a的关系,而组合强调的是has-a的关系。
在jdk1.2被广泛引用雨之后的几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计成推荐给开发者的一种类加载器实现方式。
双亲委派的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己的去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(他的搜索范围中没有找到所需的类)时,子加载器才会自己尝试去加载。
使用双亲委派来组织加载器之间的关系有个显而易见的好处就是Java类随着他的加载器一起具备了一种带有优先级的层次关系。自己编写得类和系统变量的类相同名字可能被编译但是永远不可能被加载运行。
双亲委派对于保证Java程序的稳定运作很重要,但是他的实现却非常简单,是双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法中。先去检查一个是否已经被加载过,若没有加载过则父类加载器的loadClass()方法,若父加载器为空,则默认使用启动类加载器作为父加载器。如果父加载器失败,则抛出ClassNotFoundException异常,再带用自己的findClass()方法进行加载。
破坏双亲委派模型第一次是发生在双亲委派出现之前,jdk1.2发布之前,已经不提倡用户去覆盖这个loadClass()方法,而是应当白自己的类加载逻辑写到findClass()方法中,在LoadClass方法逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成,这样可以保证新写出来的类加载器是符合双亲委派的。
双亲委派模型的第二次“被破坏”是这个模型自身的缺陷所导致的,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢。
这并非是不可能的事情,一个典型的例子便是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办? 为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。了有线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类加载器失败。
以上的三种破坏都是来源于书本深入理解java虚拟机,

总结

如果是用自己的话叙述呢,就是第一种是来自于本身的覆盖和重写,第二种是因为一些功能性的API 的使用,为了更加方便的去更新这些API方法,导致这些类并不是在虚拟机启动类的寻找路径上,所以会启动一个另外的类,第三种就是追求更高的要求,但是双亲委派并没有达到这个要求。

这就是虚拟机的一部分内容在后面的内容我会集合多线程进行完成博客。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

又是重名了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值