1、Java内存管理
程序计数器、虚拟机栈、本地方法栈是哪个区域随着线程生而生,随线程而灭。
1.1、Java内存区域
1.1.1、程序计数器
1、程序计数器(Program CounterRegister) 是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器. 在虚拟机的概念模型里, 字节码解释器工作时就是通过改变这个计数器的值来选去吓一跳需要执行的字节码指令, 分支, 循环, 跳转, 异常处理, 线程恢复等基础功能都需要依赖这个计数器来完成.
2、由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的, 在任何一个确定的时刻, 一个处理器(对于多核处理器来说是一个内核) 只会执行一条线程中的指令. 因此, 为了线程切换后能恢复到正确的执行位置, 每条线程都需要有一个独立的程序计数器, 各条线程之间的计数器互不影响, 独立存储, 我们称这类内存区域为"线程私有内存".
3、如果线程正在执行的是一个Java方法, 这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是Native方法, 这个计数器值则为空(Undefined). 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域.
##### 1.1.2、**Java虚拟机栈**
每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知了。
1、与程序计数器一样, Java虚拟机栈(Java Virtual Machine Stacks) 也是线程私有的, 它的生命周期与线程相同. 虚拟机栈描述的是Java方法执行的内存模型: 每个方法被执行的时候都会同时创建一个栈帧(Stack Frame) 用于存储局部变量表, 操作栈, 动态链接, 方法出口等信息. 每一个方法被调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程.
2、局部变量表存放了编译期可知的各种基本数据类型(Boolean, byte , char, short, int, float , long , double), 对象引用(reference类型, 它不等同于对象本身, 根据不同的虚拟机实现, 他可能是一个指向对象起始地址的引用指针, 也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址).
3、其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot), 其余的数据类型只占用一个, 局部变量表所需的内存空间在编译期间完成分配, 当进入一个方法时, 这个方法需要在帧中分配多大的局部变量空间是完全确定的, 在方法运行期间不会改变局部变量表的大小.
如果线程请求的栈深度大于虚拟机所允许的深度, 将抛出StackOverflowError异常;
如果虚拟机栈可以动态扩展(当前大部分Java虚拟机都可动态拓展, 只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当拓展时无法申请到足够的内存时会抛出OutOfMemoryEoor异常.
##### 1.1.3、**本地方法栈**
本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用是非常相似的, 其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务, 而本地方法栈则是为虚拟机使用到Native方法服务. 虚拟机规范中对本地方法栈中的方法使用的语言, 使用方式与数据结构并没有强制规定, 因此具体的虚拟机可以自由实现它. 甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一. 与虚拟机栈一样, 本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常.
##### 1.1.4、**Java** **堆**
1、对于大多数应用来说, Java堆(Java Heap) 是Java虚拟机所管理的内存中最大的一块. Java堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建. 此内存区域的唯一目的就是存放对象实例, 几乎所有的对象实例都在这里分配内存. 这一点在Java虚拟机规范中描述的是: 所有的对象实例以及数组都要在堆上分配, 但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟, 栈上分配, 标量替换优化技术将会导致一些微妙的变化发生, 所有的对象都分配在堆上也逐渐变得不是那么"绝对"了.
2、Java 堆是垃圾收集器管理的主要区域, 因此很多时候也被称做"GC堆"(Garbage Collected Heap), 如果从内存回收的角度看, 由于现在收集器基本都是采用的分代收集算法, 所以Java堆中还可以细分为: 新生代和老年代; 在细致一点的有Eden空间, From Survivor空间, To Survivor空间等. 如果从内存分配的角度看, 线程共享的Java对中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB). 不过, 无论如何划分, 都与存放内容无关, 无论哪个区域, 存储的都仍然是对象实例, 进一步划分的目的是为了更好的回收内存, 或者更快的分配内存.
3、根据Java虚拟机规范的规定, Java堆上可以处于物理上不连续的内存空间中, 只要逻辑上是连续的即可, 就像我们的磁盘空间一样. 在实现时, 既可以实现成固定大小的, 也可以是可拓展的, 不过当前主流的虚拟机都是按照可拓展来实现的( 通过-Xms 初始化堆, -Xmx 最大堆空间), 如果在堆中没有内存完成实例分配, 并且堆也无法在拓展时, 将会抛出OutOfMemoryError异常.
##### 1.1.5、**方法区**
1、方法区(Method Area) 与Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类信息, 常量, 静态变量, 即时编译器编译后的代码等数据. 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分, 但是它却有一个别名叫做Non-Heap非堆, 目的应该是与Java Heap 区分开来.
2、方法区也称为”永久代“,仅仅是因为HotSpot虚拟机设计团队把GC分代收集扩展至方法区,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存。
3、如何实现方法区属于虚拟机实现细节,不受虚拟机规范约束,但使用永久代来实现方法区,容易遇到内存溢出的问题(永久代有-XX:MaxPermSize的上限),而且有极少数方法(如String.intern())会因为这个原因导致不同的虚拟机下不同的表现,目前已经发布的jdk1.7的HotSpot中,已经把原本放在永久代的字符串常量池移出。
4、Java虚拟机对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集,这个区域的内存回收主要目标是针对常量池的回收和类型的卸载。
##### 1.1.6**运行时常量池**
1、运行时常量池(Runtime Constant Pool) 是方法区的一部分. Class文件中除了有类的版本, 字段,方法, 接口等描述信息外, 还有一项信息是常量池(Constant Pool Table), 用于存放编译期生成的各种字面量和符号引用, 这部分内容将在类加载后存放到方法区的运行时常量池中.
2、运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性, Java语言并不要求常量一定只能在编译期产生, 也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池, 运行期间也可能将新的常量放入翅中, 这种特性被开发人员利用的比较多的便是String类的intern() 方法.
##### **1.1.7、直接内存**
直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分, 也不是Java虚拟机规范中定义的内存区域, 但是这部分内存也被频繁地使用, 而且也可能导致OutOfMemoryError异常出现. 显然, 本机直接内存的分配不会受到Java堆大小的限制, 但是, 既然是内存, 则肯定还是会受到本机总内存的大小及处理器寻址空间的限制. 服务器管理员配置虚拟机参数时, 一般会根据实际内存-Xmx等参数信息, 但经常会忽略到直接内存, 使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制), 从而导致动态扩展时出现OutOfMemoryError异常.
1.2、对象的创建
1.2.1、加载类
当虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有先执行相应的类加载过程。
1.2.2、为新生对象分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可以确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
分配内存的方式:
1、指针碰撞
这种方式是假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那么分配内存就是把指针向空闲的那边挪动一段与对象大小相等的距离。
2、空闲列表
假设Java堆中的内存是不规整的,已使用的内存和空闲的内存相互交错,虚拟机必须维护一个列表,记录上那块内存块是可用,在分配的时候从列表中找到一块足够大小的空间划分给对象,并更新列表上的记录。
1.选择哪种分配方式有Java堆是否规整来决定,而Java堆是否规整由所采用的垃圾收集器是否带有压缩整理的功能决定。
- 因此:在使用Serial、ParNew等带有Compact过程的收集器时,系统采用的方法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。
1.2.3、处理内存分配的并发情况
有两种方案解决:
1、对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
2、把内存分配的动作按照线程划分在不同的空间上进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thead Local Allocation Buffer,TLAB)。那个线程要分配内存,就在那个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
1.2.4、对象初始化
内存分配完成之后,需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一动作可以提前至TLAB分配时进行。
1.2.5、对对象进行必要的设置
虚拟机要对这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息进行设置。这些信息存放在对象头中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁,对象头会有不同的设置方式。
1.2.6、执行init方法
执行new指令,分配好了内存空间之后,接着执行init方法,把对象按照程序员的意愿进行初始化,这样对象才算完全产生出来。
1.3、对象的内存布局
在HotSpot虚拟机中,对象在内存中的布局分为三个区域:对象头(Header)、实例数据(Instance)、对齐填充(Padding)。
1.3.1、对象头
对象头包括两部分:
1、第一部分用于存储对象自身的运行时数据,如哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别是32bit和64bit,官方称为“Mark Word”。
对象需要存储的运行时数据很多,已经超过32位和64位,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到效率,Mark Word被设计成非固定的数据结构以便在极小的空间内存储尽量多的信息。
2、对象头另一部分存储的是类型信息,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
并不是所有的虚拟机实现都必须在对象数据上保留类型指针,也就是说,查找对象的元数据信息并不一定经过对象本身。
另外,如果对象是一个数组,那对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通的Java对象的元数据信息确定Java对象的大小,但是数组对象是例外。
1.3.2、实例数据
这部分存储的是对象真正的有效信息,就是在程序中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。
这部分存储顺序受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略是:long/doubles、ints、shorts/chars、bytes/booleans、oop(Ordinary Object Pointers),相同宽度的字段分配到一起,在满足这个前提下,父类中定义的变量会出现在子类之前。
1.3.3、对齐填充
这部分没有特别的含义,仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也即对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的整数倍(一倍或两倍),因此,当对象实例数据部分没有对齐时,就需要填充。
1.4、对象的访问定位
1.4.1、句柄
Java堆会划分出一块内存来作为句柄池,对象引用存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
1.4.2、直接指针
直接指针中引用中存储的直接就是对象地址,对象头中包含对象类型数据的地址。
区别:
1、使用句柄来访问的最大好处就是引用存储的就是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而引用本身不需要修
改。
2、使用直接指针访问好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁。HotSpot VM使用第二种方式进行对象访问。
2、对象判定策略
哪些内存需要回收?
什么时候回收?
如何回收?
2.1、引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1。当引用失效时,计数器就减1。任何时刻计数器为0的对象就是不可能再被使用的。
缺点:
它很难解决对象之间的相互循环引用的问题,因此JVM没有选用引用计数算法来管理内存。
2.2、可达性分析算法
基本思想是通过一系列的称为“GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象当GC Roots 没有任何引用链(对象到GC Roots不可达)时,则证明此对象是不可用的。
在Java中可作为GC Roots的对象包括:
1、虚拟机栈(栈帧中的本地变量表)中引用的对象
2、方法区类静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中Native方法引用的对象
2.3、引用分类
Java中引用的定义是 如果reference类型的数据中存储的数值代表的是另一块内存地址的起始地址,就称这块内存代表的是一个引用。
引用分为四种:强引用、软引用、弱引用、虚引用,他们的强度一次减弱
2.3.1、强引用
程序中类似:Object obj = new Object();这种引用,只要强引用还在,垃圾收集器就永远不会回收它们。
2.3.2、软引用(SoftReference)
用来描述一些还有用但并非必需的对象,对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,则抛出内存溢出异常。
2.3.4、弱引用(WeakReference)
用来描述一些还有用但并非必需的对象,但是她的强度比软引用更弱一点,被弱引用关联的对象只能生存到下一次垃圾收集之前,当垃圾收集器开始工作是,都会回收其关联的对象。
2.3.5、虚引用(PhantomReference)
没有多大的作用,完全不会对对象生存时间构成影响。唯一目的就是这个对象被回收之前会收到系统的一个通知。
2.4、对象的生死
即使可达性分析算法中不可达的对象,也并非是非死不可,真正宣告一个对象死亡,要经过两次标记的过程。
第一次标记:
如果对象在进行可达性分析后发现与GC Roots没有相连接的引用链,将会进行第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,此时都视为没有必要执行finalize()。
第二次标记:
如果这个对象被判定有必要执行finalize()方法,那么这个对象会被放置在一个叫做F-Queue的队列中,并由虚拟机自动建立的低优先级的Finalizer线程去执行它,执行表示虚拟机会触发这个方法,但不会等待它运行结束,原因是如果一个对象在finalize()方法中执行缓慢,或发生死循环导致F-Queue队列中其他对象处于等待,导致回收系统崩溃。
finalize()方法是对象逃脱死亡 的最后一次机会,稍后GC会对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()方法中重新与引用链的任何一个对象关联上即可(如把this赋值给某个类变量或者对象的成员变量),那么就不会被回收。
如果对象这时候还没有逃脱那么就真的被回收了。
2.5、回收方法区(永久代)
JVM可以不要求在方法区实现垃圾收集,在堆中尤其是新生代中,常规进行一次收集可以回收70%-95%的空间,而永久代的垃圾收集效率远低于此
永久代的垃圾回收主要有两部分:
2.5.1、废弃常量
回收废弃常量与Java堆中的对象非常相似,以常量池中字面量的回收为例:假如一个字符串“abc”,没有被任何对象引用,那么就会被回收。常量池中的其他类(接口)、方法、字段的符号引用也是如此。
2.5.2、无用的类
判定一个常量是否是废弃常量非常简单,但是判定一个类是无用的类条件非常苛刻。必须满足一下:
1、该类的所有实例都已经被回收
2、加载该类的ClassLoader已经被回收
3、该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
2.6、分配策略
对象主要在新生代的Eden上分配,如果启动TLAB,则在TLAB分配
新生代GC(Minor Gc)
老生代GC(Major GC/Full GC)
2.6.1、对象优先在Eden分配
大多数情况在新生代的Eden上分配,当Eden空间不足时,将触发Minor GC
2.6.2、大对象直接进入老年代
大对象是指需要大量连续的内存空间的Java对象,如:很长的字符串以及数组
2.6.3、长期存活的对象直接进入老年代
使用分代思想,就必须识别那些对象放在新生代,哪些放在老年代。因此给每个对象顶一个对象年龄计数器
如果对象在Eden出生并经过第一次MinorGC还存活,并能被Survivor容纳,将对象年龄设置为1,对象在Survivor中每经过一次Minor GC 年龄就增加1,当年龄到15(默认15,通过-XX:MaxTenuringThreshold)时,会晋升到老年代中。
2.6.4、动态对象年龄判断
虚拟机并不是永远要求对象的年龄达到15才能进入老年代,如果Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
2.6.5、空间分配担保
在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于,那么MinorGC可用确保是安全的,如果不成立,虚拟机会看HandlePromotionFailure设置值是否允许担保失败,如果允许,继续检查检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,进行MinorGC,如果小于或者HandlePromotionFailure设置为不允许(false),改为FullGC
3、垃圾收集算法
3.1、标记-清除算法
首先标记出所有需要回收的对象,在标记完成之后统一清除被标记的对象。
缺点:
1、效率低:标记和清除两个过程效率都不高
2、空间问题:清除之后会产生大量不连续的内存碎片,会导致下次需要分配较大的对象时,没有足够的连续的内存而不得不提前触发另一次垃圾收集动作。
3.2、复制算法
为解决效率问题,复制算法将内存分为大小相等的两块,每次只使用其中的一块,当这一块使用完了,就将还存活对象复制到另外一块上面,然后再把已使用过的内存空间一次性清理掉。这样使得每次都是对整个半区进行内存回收,就不用考虑内存碎片的复杂情况。
新生代都采用这种算法,由于新生代中的对象98%都是朝生夕死,所以不需要按照1:1划分空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,按照 8:1:1划分。
进行垃圾回时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。就是说每次新生代中可用内存为整个新生代空间的90%,只有10%的内存会被浪费。
如果另一块Survivor没有足够的空间存放上一次新生代收集下来的存活对象时,会直接通过分配担保机制进入老年代。
3.3、标记-整理算法
复制算法适合对象存活率较低的情况,对于存活率高的情况(老年代)复制算法就需要进行较多的复制操作,效率会变低。
老年代一般选用标记-整理算法
标记-整理过程是先标记,后续不是直接对可回收的对象进行整理,而是让所有存活的对象都向一端移动,然后清理掉边界以外的内存。
3.4、分代收集算法
根据对象存活周期的不同将内存划分为几块,一般Java堆分为新生代和老年代,这样可以根据对象不同的特点使用不同的收集算法。
新生代每次垃圾收集都有大批对象死亡,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代对象存活率高,没有额外的空间进行担保,必须使用标记-整理算法或者标记-清除算法
3.5、HotSpot算法实现
3.5.1、枚举根节点
可达性行分析中从GC Roots开始查找引用链
痛点:
1、由于仅仅方法区有数百兆,逐个检查会消耗很多时间。
2、查找过程中必须保证一致性,即这个过程中引对象引用关系不能变化,因此导致了GC停顿,(Stop The World)停止所有的Java执行线程。
当系统停下来后,并不需要一个一个地去检查引用,HotSpot实现中使用一组OOPMap的数据结构,存储引用所在的位置、在类加载完成之后,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,这样GC扫描的时候可以直接得到这些信息了。
3.5.2、安全点(safe Point)
系统只在特定的位置上,或者说特定的时间点,开始计算OopMap,计算这个时刻对象引用处于一种什么关联中。确定好了对象引用关系(OopMap)再开始GC。
3.5.3、安全区域(safe Region)
安全点在实际情况中会有问题
如果程序进入到安全点,但是此时程序处于sleep或者Blocked状态,或者此时没有获得CPU时间片,此时JVM不能等待这个线程重新获得CPU时间片。
安全区域是指在一段代码中,引用关系不会发生变化,这个区域GC都是安全的。
实现:
在程序执行到安全区域时,首先标识自己进入到安全区域了,那么这段时间JVM要发起GC就不用管标识自己是safe region状态的线程了。
在线程要离开safe region 时,要检查系统是否已经完成了根节点枚举(或者整个GC过程),如果完成了那么就继续执行,否则等待直到收到可以离开safe region的信号为止。
4、垃圾收集器
4.1、Serial收集器
是一个单线程的收集器,它在工作的时候,必须暂停其他所有的线程,直到它收集结束。是Client 模式下新生代收集器,
特点:简单高效,没有线程交互的开销,
4.2、ParNew 收集器
是Serial的多线程版本
是许多运行在Server模式下的虚拟机中首选的新生代收集器,目前只有ParNew能与CMS收集器配合工作。
4.3、Parallel Scavenge收集器
是一个新生代收集器,使用复制算法,又是并行的多线程收集器,
CMS收集器关注点是尽可能地缩短垃圾收集时用户线程停顿的时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时候 / (运行用户代码时候+垃圾收集时间))
4.4、Serial Old
是Serial的老年代版本
4.5、Parallel Old
是Parallel Scavenge老年代版本,使用多线程和标记-整理算法
4.6、CMS收集器
CMS(Concurrent Mark Sweep)是一种获取最短回收停顿时间为目标的收集器。基于Mark-Sweep
过程:
1、初始标记
2、并发标记
3、重新标记
4、并发清除
1、2步骤需要停顿
4.7、G1
是面向服务端应用的(jdk1.7发布的)(CMS是jdk1.5发布)
特点:
1、并行与并发:充分利用CPU、多核环境下的优势,缩短停顿时间。
2、分代收集
3、空间整合:从整体上看是标记-整理,从局部(两个region)上看是复制算法,收集之后不会产生内存碎片
4、可预测的停顿:
5、JVM类加载器
虚拟机只加载执行时所需的类文件,如果遇到调用的方法需要使用到别的类,那么接下来会加载这些类。
每个Java程序至少包括三个类加载器:
1、引导类加载器
2、扩展类加载器
3、系统类加载器
每个线程都有一个对应的类加载器的引用,称为上下文类加载器,主线程的上下文类加载器是系统类加载器,
5.1、引导类加载器
负责加载系统类(通常在JAR文件rt.jar中进行加载)。它是虚拟机不可分割的一部分,而且通常是C语言实现的,引导类加载器没有对应的ClassLoader对象。例如:
String.class.getClassLoader(); 该方法将返回null。
5.2、扩展类加载器
用于从jre/lib/ext目录加载“标准的扩展”,可以将jar放入该目录,这样即使没有任何类路径,扩展类加载器也可以找到其中的各个类。
5.3、系统类加载器
用于加载应用类,他在有ClassPath环境变量,或者-ClassPath命令行选项设置的类路径中的目录里或者是JAR、ZIP文件里找这些类
扩展、系统类加载器都是Java实现都是URLClassLoader类的实例。
5.4、类加载器的层次结构
加载器有一种父子关系,除了引导类加载器之外,根据规定,类加载器在加载时,会首先抛给父类加载器进行加载,在父类加载失败时,才会自己加载,
类加载器的继承关系:
Plug类加载器(plug.jar)-> System类加载器(CLASSPATH) -> Extension类加载器(jre/lib/ext) -> Bootstrap类加载器(rt.jar)
5.5、编写自己的类加载器
只需要继承ClassLoader类,然后覆盖findClass(String className)