备战- Java虚拟机html
试问岭南应很差,却道,此心安处是吾乡。java
简介:备战- Java虚拟机程序员
在Java 运行环境参考连接:http://www.javashuo.com/article/p-mxyxzoee-dp.html。算法
在 JDK 1.4 中新引入了 NIO 类,它可使用 Native 函数库直接分配堆外内存,而后经过 Java 堆里的 DirectByteBuffer 对象做为这块内存的引用进行操做。这样能在一些场景中显著提升性能,由于避免了在堆内存和堆外内存来回拷贝数据。数组
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束以后就会消失,所以不须要对这三个区域进行垃圾回收。缓存
为对象添加一个引用计数器,当对象增长一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。安全
在两个对象出现循环引用的状况下,此时引用计数器永远不为 0,致使没法对它们进行回收。正是由于循环引用的存在,所以 Java 虚拟机不使用引用计数算法。网络
1 public class Test { 2
3 public Object instance = null; 4
5 public static void main(String[] args) { 6 Test a = new Test(); 7 Test b = new Test(); 8 a.instance = b; 9 b.instance = a; 10 a = null; 11 b = null; 12 doSomething(); 13 } 14 }
在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,所以当咱们把对 a 对象与 b 对象的引用去除以后,因为两个对象还存在互相之间的引用,致使两个 Test 对象没法被回收。多线程
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。并发
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 通常包含如下内容:
由于方法区主要存放永久代对象,而永久代对象的回收率比新生代低不少,因此在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
为了不内存溢出,在大量使用反射和动态代理的场景都须要虚拟机具有类卸载功能。
类的卸载条件不少,须要知足如下三个条件,而且知足了条件也不必定会被卸载:
相似 C++ 的析构函数,用于关闭外部资源。可是 try-finally 等方式能够作得更好,而且该方法运行代价很高,不肯定性大,没法保证各个对象的调用顺序,所以最好不要使用。
当一个对象可被回收时,若是须要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象从新被引用,从而实现自救。自救只能进行一次,若是回收的对象以前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
不管是经过引用计数算法判断对象的引用数量,仍是经过可达性分析算法判断对象是否可达,断定对象是否可被回收都与引用有关。
Java 提供了四种强度不一样的引用类型。
被强引用关联的对象不会被回收。
使用 new 一个新对象的方式来建立强引用。
Object obj = new Object();
被软引用关联的对象只有在内存不够的状况下才会被回收。
使用 SoftReference 类来建立软引用。
1 Object obj = new Object(); 2 SoftReference<Object> sf = new SoftReference<Object>(obj); 3 obj = null; // 使对象只被软引用关联
被弱引用关联的对象必定会被回收,也就是说它只能存活到下一次垃圾回收发生以前。
使用 WeakReference 类来建立弱引用。
1 Object obj = new Object(); 2 WeakReference<Object> wf = new WeakReference<Object>(obj); 3 obj = null; // 使obj 对象只被弱引用关联
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间形成影响,也没法经过虚引用获得一个对象。
为一个对象设置虚引用的惟一目的是能在这个对象被回收时收到一个系统通知。
使用 PhantomReference 来建立虚引用。
1 Object obj = new Object(); 2 PhantomReference<Object> pf = new PhantomReference<Object>(obj, null); 3 obj = null; // 使obj 只能被虚引用关联
标记清除算法(Mark-Sweep)是最基础的一种垃圾回收算法,它分为2部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,而后把这些垃圾拎出来清理掉。就像上图同样,清理掉的垃圾就变成未使用的内存区域,等待被再次使用。
这逻辑再清晰不过了,而且也很好操做,但它存在一个很大的问题,那就是内存碎片。
上图中等方块的假设是 2M,小一些的是 1M,大一些的是 4M。等咱们回收完,内存就会切成了不少段。咱们知道开辟内存空间时,须要的是连续的内存区域,这时候咱们须要一个 2M的内存区域,其中有2个 1M 是无法用的。这样就致使,其实咱们自己还有这么多的内存的,但却用不了。
不足:
标记整理算法(Mark-Compact)标记过程仍然与标记 --- 清除算法同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,再清理掉端边界之外的内存区域。
标记整理算法一方面在标记-清除算法上作了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但从上图能够看到,它对内存变更更频繁,须要整理全部存活对象的引用地址,在效率上比复制算法要差不少。
优势:
不足:
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另外一块上面,而后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
如今的商业虚拟机都采用这种收集算法回收新生代,可是并非划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象所有复制到另外一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。若是每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时须要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
分代收集算法(Generational Collection)严格来讲并非一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不一样状况所采用不一样算法的一套组合拳。对象存活周期的不一样将内存划分为几块。通常是把 Java 堆分为新生代和老年代,这样就能够根据各个年代的特色采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记——整理算法来进行回收。
通常将堆分为新生代和老年代。
JVM 在进行GC 时,可能针对三个区域进行垃圾回收分别是新生代、老年代、方法区,大部分时候回收的都是新生代。GC类型主要有如下四种类型。
Java 堆(Java Heap)是JVM所管理的内存中最大的一块,堆又是垃圾收集器管理的主要区域。
Java 堆主要分为2个区域-年轻代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2个区。为何要分这么多个区呢?
★ Eden 区
IBM 公司的专业研究代表,有将近98%的对象是朝生夕死,因此针对这一现状,大多数状况下,对象会在新生代 Eden 区中进行分配,当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。
经过 Minor GC 以后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。
★ Survivor 区
Survivor 区至关因而 Eden 区和 Old 区的一个缓冲,相似于咱们交通灯中的黄灯。Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(若是 To 区不够,则直接进入 Old 区)。
★ 为何要分区?
若是没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有不少对象虽然一次 Minor GC 没有消灭,但其实也并不会存活多久,或许第二次,第三次就须要被清除。这时候移入老年区,很明显不是一个明智的决定。
因此,Survivor 的存在乎义就是减小被送到老年代的对象,进而减小 Major GC 的发生。Survivor 的预筛选保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。
★ 为何要分两个Survivor ?
设置两个 Survivor 区最大的好处就是解决内存碎片化。
咱们先假设一下,Survivor 若是只有一个区域会怎样。Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而以前 Survivor 区中的对象,可能也有一些是须要被清除的。问题来了,这时候咱们怎么清除它们?在这种场景下,咱们只能标记清除,而咱们知道标记清除最大的问题就是内存碎片,在新生代这种常常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。由于 Survivor 有2个区域,因此每次 Minor GC,会将以前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。
这种机制最大的好处就是,整个过程当中,永远有一个 Survivor space 是空的,另外一个非空的 Survivor space 是无碎片的。那么,Survivor 为何不分更多块呢?比方说分红三个、四个、五个?显然,若是 Survivor 区再细分下去,每一块的空间就会比较小,容易致使 Survivor 区满,两块 Survivor 区多是通过权衡以后的最佳方案。
★ Old 区
老年代占据着2/3的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,因此内存也不只仅是越大就越好。因为复制算法在对象存活率较高的老年代会进行不少次的复制操做,效率很低,因此老年代这里采用的是标记——整理算法。
★ 两张图了解垃圾回收全流程
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器能够配合使用。
Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工做。
它的优势是简单高效,在单个 CPU 环境下,因为没有线程交互的开销,所以拥有最高的单线程收集效率。
它是 Client 场景下的默认新生代收集器,由于在该场景下内存通常来讲不会很大。它收集一两百兆垃圾的停顿时间能够控制在一百多毫秒之内,只要不是太频繁,这点停顿时间是能够接受的。
它是 Serial 收集器的多线程版本。
它是 Server 场景下默认的新生代收集器,除了性能缘由外,主要是由于除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
与 ParNew 同样是多线程收集器。
其它收集器目标是尽量缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,所以它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
停顿时间越短就越适合须要与用户交互的程序,良好的响应速度能提高用户体验。而高吞吐量则能够高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不须要太多交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,致使吞吐量降低。
能够经过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不须要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行状况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。若是用在 Server 场景下,它有两大用途:
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,均可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为如下四个流程:
在整个过程当中耗时最长的并发标记和并发清除过程当中,收集器线程均可以与用户线程一块儿工做,不须要进行停顿。
具备如下缺点:
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是将来能够替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 能够直接对新生代和老年代一块儿回收。
上图中绿色的永久代在如今的Hotspot 中已被移除。
G1 把堆划分红多个大小相等的独立区域(Region),新生代和老年代再也不物理隔离。
经过引入 Region 的概念,从而将原来的一整块内存空间划分红多个的小空间,使得每一个小空间能够单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。经过记录每一个 Region 垃圾回收时间以及回收所得到的空间(这两个值是经过过去回收的经验得到),并维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的 Region。
每一个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。经过使用 Remembered Set,在作可达性分析的时候就能够避免全堆扫描。
若是不计算维护 Remembered Set 的操做,G1 收集器的运做大体可划分为如下几个步骤:
具有以下特色:
Minor GC:回收新生代,由于新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度通常也会比较快。
Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 不多执行,执行速度会比 Minor GC 慢不少。
大多数状况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
大对象是指须要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
常常出现大对象会提早触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
为对象定义年龄计数器,对象在 Eden 出生并通过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增长 1 岁,增长到必定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
虚拟机并非永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,若是在 Survivor 中相同年龄全部对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象能够直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
在发生 Minor GC 以前,虚拟机先检查老年代最大可用的连续空间是否大于新生代全部对象总空间,若是条件成立的话,那么 Minor GC 能够确认是安全的。
若是不成立的话虚拟机会查看 HandlePromotionFailure 的值是否容许担保失败,若是容许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若是大于,将尝试着进行一次 Minor GC;若是小于,或者 HandlePromotionFailure 的值不容许冒险,那么就要进行一次 Full GC。
对于 Minor GC,其触发条件很是简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有如下条件:
只是建议虚拟机执行 Full GC,可是虚拟机不必定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了不以上缘由引发的 Full GC,应当尽可能不要建立过大的对象以及数组。除此以外,能够经过 -Xmn 虚拟机参数调大新生代的大小,让对象尽可能在新生代被回收掉,不进入老年代。还能够经过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
使用复制算法的 Minor GC 须要老年代的内存空间做担保,若是担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节的空间分配担保。
在 JDK 1.7 及之前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的状况下也会执行 Full GC。若是通过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上缘由引发的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
执行 CMS GC 的过程当中同时有对象要放入老年代,而此时老年代空间不足(多是 GC 过程当中浮动垃圾过多致使暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
类是在运行期间第一次使用时动态加载的,而不是一次性加载全部类。由于若是一次性加载,那么会占用不少的内存。
包括如下 7 个阶段:
加载是类加载的一个阶段,注意不要混淆。
加载过程完成如下三件事:
其中二进制字节流能够从如下方式中获取:
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一块儿被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在全部实例化操做以前,而且类加载只进行一次,实例化能够进行屡次。
初始值通常为 0 值,例以下面的类变量 value 被初始化为 0 而不是 123。
public static int value = 123; // 变量
若是类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例以下面的常量 value 被初始化为 123 而不是 0。
public static final int value = 123; // 常量
将常量池的符号引用替换为直接引用的过程。
其中解析过程在某些状况下能够在初始化阶段以后再开始,这是为了支持 Java 的动态绑定。
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit\>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员经过程序制定的主观计划去初始化类变量和其它资源。
<clinit>() 是由编译器自动收集类中全部类变量的赋值动做和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它以前的类变量,定义在它以后的类变量只能赋值,不能访问。例如如下代码:
1 public class Test { 2 static { 3 i = 0; // 给变量赋值能够正常编译经过
4 System.out.print(i); // 这句编译器会提示“非法向前引用”
5 } 6 static int i = 1; 7 }
因为父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如如下代码:
1 static class Parent { 2 public static int A = 1; 3 static { 4 A = 2; 5 } 6 } 7
8 static class Sub extends Parent { 9 public static int B = A; 10 } 11
12 public static void main(String[] args) { 13 System.out.println(Sub.B); // 2
14 }
接口中不可使用静态语句块,但仍然有类变量初始化的赋值操做,所以接口与类同样都会生成 <clinit>() 方法。但接口与类不一样的是,执行接口的 <clinit>() 方法不须要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也同样不会执行接口的 <clinit>() 方法。
虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,若是多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。若是在一个类的 <clinit>() 方法中有耗时的操做,就可能形成多个线程阻塞,在实际过程当中此种阻塞很隐蔽。
虚拟机规范中并无强制约束什么时候进行加载,可是规范严格规定了有且只有下列五种状况必须对类进行初始化(加载、验证、准备都会随之发生):
遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,若是类没有进行过初始化,则必须先触发其初始化。最多见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
使用 java.lang.reflect 包的方法对类进行反射调用的时候,若是类没有进行初始化,则须要先触发其初始化。
当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化。
当虚拟机启动时,用户须要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
当使用 JDK 1.7 的动态语言支持时,若是一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,而且这个方法句柄所对应的类没有进行过初始化,则须要先触发其初始化;
以上 5 种场景中的行为称为对一个类进行主动引用。除此以外,全部引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
SuperClass[] sca = new SuperClass[10];
System.out.println(ConstClass.HELLOWORLD);
两个类相等,须要类自己相等,而且使用同一个类加载器进行加载。这是由于每个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字作对象所属关系断定结果为 true。
类加载机制参考连接:https://www.cnblogs.com/taojietaoge/p/10269844.html
从 Java 虚拟机的角度来说,只存在如下两种不一样的类加载器:
启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;
全部其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
从 Java 开发人员的角度看,类加载器能够划分得更细致一些:
启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,而且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即便放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器没法被 Java 程序直接引用,用户在编写自定义类加载器时,若是须要把加载请求委派给启动类加载器,直接使用 null 代替便可。
扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的全部类库加载到内存中,开发者能够直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。因为这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以通常称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者能够直接使用这个类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。
应用程序是由三种类加载器互相配合从而实现类加载,除此以外还能够加入本身定义的类加载器。
双亲委托参考连接:https://www.cnblogs.com/taojietaoge/p/10269844.html
下图展现了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有本身的父类加载器。这里的父子关系通常经过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器没法完成时才尝试本身加载。
从上图可用看出ClassLoader的加载序列,委托是从下往上,查找过程则是从上向下的,如下有几个注意事项:
使得 Java 类随着它的类加载器一块儿具备一种带有优先级的层次关系,从而使得基础类获得统一。
.class
。经过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。.class
不能被篡改。经过委托方式,不会去篡改核心.clas
,即便篡改也不会去加载,即便加载也不会是同一个.class
对象了。不一样的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。如下是抽象类 java.lang.ClassLoader 的代码片断,其中的 loadClass() 方法运行过程以下:先检查类是否已经加载过,若是没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试本身去加载。
1 public abstract class ClassLoader { 2 // The parent class loader for delegation
3 private final ClassLoader parent; 4
5 public Class<?> loadClass(String name) throws ClassNotFoundException { 6 return loadClass(name, false); 7 } 8
9 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 10 synchronized (getClassLoadingLock(name)) { 11 // First, check if the class has already been loaded
12 Class<?> c = findLoadedClass(name); 13 if (c == null) { 14 try { 15 if (parent != null) { 16 c = parent.loadClass(name, false); 17 } else { 18 c = findBootstrapClassOrNull(name); 19 } 20 } catch (ClassNotFoundException e) { 21 // ClassNotFoundException thrown if class not found 22 // from the non-null parent class loader
23 } 24
25 if (c == null) { 26 // If still not found, then invoke findClass in order 27 // to find the class.
28 c = findClass(name); 29 } 30 } 31 if (resolve) { 32 resolveClass(c); 33 } 34 return c; 35 } 36 } 37
38 protected Class<?> findClass(String name) throws ClassNotFoundException { 39 throw new ClassNotFoundException(name); 40 } 41 }
在ClassLoader中有四个很重要实用的方法loadClass()、findLoadedClass()、findClass()、defineClass(),能够用来建立属于本身的类的加载方式;好比咱们须要动态加载一些东西,或者从C盘某个特定的文件夹加载一个class 文件,又或者从网络上下载class 主内容而后再进行加载等。分三步搞定:
一、编写一个类继承ClassLoader 抽象类;
二、重写findClass() 方法;
三、在findClass() 方法中调用defineClass() 方法便可实现自定义ClassLoader。
需求:
自定义一个classloader 其默认加载路径为"/TJT/Code"下的jar 包和资源。
实现:
首先建立一个Test.java,而后javac 编译并把生成的Test.class 文件放到"/TJT/Code" 路径下。
而后再编写一个DiskClassLoader 继承ClassLoader。
1 package www.baidu; 2 import java.io.ByteArrayOutputStream; 3 import java.io.File; 4 import java.io.FileInputStream; 5 import java.io.IOException; 6
7 public class DiskClassLoader extends ClassLoader{ 8 //自定义classLoader能将class二进制内容转换成Class对象
9 private String myPath; 10
11 public DiskClassLoader(String path) { 12 myPath = path; 13 } 14
15 //findClass()方法中定义了查找class的方法
16 @Override 17 protected Class<?> findClass(String name) throws ClassNotFoundException{ 18 String fileName = getFileName(name); 19 File file = new File(myPath,fileName); 20 try { 21 FileInputStream is = new FileInputStream(file); 22 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 23 int len = 0; 24 try { 25 while((len = is.read()) != -1) { 26 bos.write(len); 27 } 28 } catch (IOException e) { 29 e.printStackTrace(); 30 } 31 byte[] data = bos.toByteArray(); 32 is.close(); 33 bos.close(); 34 //数据经过defineClass()生成了Class对象
35 return defineClass(name, data,0,data.length ); 36 } catch (Exception e) { 37 e.printStackTrace(); 38 } 39 return super.findClass(name); 40 } 41
42 private String getFileName(String name) { 43 int lastIndexOf = name.lastIndexOf('.'); 44 if (lastIndexOf == -1) { 45 return name + ".class"; 46 }else { 47 return name.substring(lastIndexOf + 1) + ".class"; 48 } 49 } 50 }
最后经过
FindClassLoader 的测试类,调用在Test.class 里面的一个find() 方法。
1 package www.baidu; 2 import java.lang.reflect.Method; 3
4 public class FindClassLoader { 5 public static void main(String[] args) throws ClassNotFoundException { 6 //建立自定义classloader对象
7 DiskClassLoader diskL = new DiskClassLoader("/TJT/Code"); 8 System.out.println("classloader is: "+diskL); 9 try { 10 //加载class文件
11 Class clazz = diskL.loadClass("www.baidu.Test"); 12 if (clazz != null) { 13 Object object = clazz.newInstance(); 14 Method declaredMethod = clazz.getDeclaredMethod("find", null); 15 //经过反射调用Test类的find()方法
16 declaredMethod.invoke(object, null); 17 } 18 } catch (Exception e) { 19 e.printStackTrace(); 20 } 21 } 22 }
验证找到指定路径下的自定义classloader。
试问岭南应很差
却道
此心安处是吾乡