记录正在执行的虚拟机字节码指令的地址(若是正在执行的是本地方法则为空)。html
每一个 Java 方法在执行的同时会建立一个栈帧用于存储局部变量表、操做数栈、常量池引用等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。java
能够经过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小:android
java -Xss=512M HackTheJava
该区域可能抛出如下异常:git
本地方法不是用 Java 实现,对待这些方法须要特别处理。程序员
与 Java 虚拟机栈相似,它们之间的区别只不过是本地方法栈为本地方法服务。github
全部对象实例都在这里分配内存。算法
是垃圾收集的主要区域("GC 堆"),现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不一样的对象采起不一样的垃圾回收算法,所以虚拟机把 Java 堆分红如下三块:数据库
当一个对象被建立时,它首先进入新生代,以后有可能被转移到老年代中。新生代存放着大量的生命很短的对象,所以新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分红如下三个空间:数组
Java 堆不须要连续内存,而且能够动态增长其内存,增长失败会抛出 OutOfMemoryError 异常。缓存
能够经过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms=1M -Xmx=2M HackTheJava
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和 Java 堆同样不须要连续的内存,而且能够动态扩展,动态扩展失败同样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,可是通常比较难实现,HotSpot 虚拟机把它当成永久代来进行垃圾回收。
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的各类字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还容许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池。
在 JDK 1.4 中新加入了 NIO 类,它可使用 Native 函数库直接分配堆外内存,而后经过一个存储在 Java 堆里的 DirectByteBuffer 对象做为这块内存的引用进行操做。这样能在一些场景中显著提升性能,由于避免了在 Java 堆和 Native 堆中来回复制数据。
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束以后也会消失,所以不须要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。
给对象添加一个引用计数器,当对象增长一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
两个对象出现循环引用的状况下,此时引用计数器永远不为 0,致使没法对它们进行回收。
objA.instance = objB; objB.instance = objA;
经过 GC Roots 做为起始点进行搜索,可以到达到的对象都是都是可用的,不可达的对象可被回收。
GC Roots 通常包含如下内容:
不管是经过引用计算算法判断对象的引用数量,仍是经过可达性分析算法判断对象的引用链是否可达,断定对象是否存活都与引用有关。
Java 对引用的概念进行了扩充,引入四种强度不一样的引用类型。
(一)强引用
只要强引用存在,垃圾回收器永远不会回收调掉被引用的对象。
使用 new 一个新对象的方式来建立强引用。
Object obj = new Object();
(二)软引用
用来描述一些还有用可是并不是必需的对象。
在系统将要发生内存溢出异常以前,将会对这些对象列进回收范围之中进行第二次回收。
软引用主要用来实现相似缓存的功能,在内存足够的状况下直接经过软引用取值,无需从繁忙的真实来源获取数据,提高速度;当内存不足时,自动删除这部分缓存数据,从真正的来源获取这些数据。
使用 SoftReference 类来实现软引用。
Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj);
(三)弱引用
只能生存到下一次垃圾收集发生以前,当垃圾收集器工做时,不管当前内存是否足够,都会被回收。
使用 WeakReference 类来实现弱引用。
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj);
(四)虚引用
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用取得一个对象实例。
为一个对象设置虚引用关联的惟一目的就是能在这个对象被收集器回收时收到一个系统通知。
使用 PhantomReference 来实现虚引用。
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj);
由于方法区主要存放永久代对象,而永久代对象的回收率比新生代差不少,所以在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
类的卸载条件不少,须要知足如下三个条件,而且知足了也不必定会被卸载:
能够经过 -Xnoclassgc 参数来控制是否对类进行卸载。
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGo 这类频繁自定义 ClassLoader 的场景都须要虚拟机具有类卸载功能,以保证不会出现内存溢出。
finalize() 相似 C++ 的析构函数,用来作关闭外部资源等工做。可是 try-finally 等方式能够作的更好,而且该方法运行代价高昂,不肯定性大,没法保证各个对象的调用顺序,所以最好不要使用。
当一个对象可被回收时,若是须要执行该对象的 finalize() 方法,那么就有可能经过在该方法中让对象从新被引用,从而实现自救。
将须要回收的对象进行标记,而后清除。
不足:
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另外一块上面,而后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
如今的商业虚拟机都采用这种收集算法来回收新生代,可是并非将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另外一块 Survivor 空间上,最后清理 Eden 和 使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。若是每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时须要依赖于老年代进行分配担保,也就是借用老年代的空间。
让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。
如今的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不一样块采用适当的收集算法。
通常将 Java 堆分为新生代和老年代。
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器能够配合使用。
它是单线程的收集器,不只意味着只会使用一个线程进行垃圾收集工做,更重要的是它在进行垃圾收集时,必须暂停全部其余工做线程,每每形成过长的等待时间。
它的优势是简单高效,对于单个 CPU 环境来讲,因为没有线程交互的开销,所以拥有最高的单线程收集效率。
在 Client 应用场景中,分配给虚拟机管理的内存通常来讲不会很大,该收集器收集几十兆甚至一两百兆的新生代停顿时间能够控制在一百多毫秒之内,只要不是太频繁,这点停顿是能够接受的。
它是 Serial 收集器的多线程版本。
是 Server 模式下的虚拟机首选新生代收集器,除了性能缘由外,主要是由于除了 Serial 收集器,只有它能与 CMS 收集器配合工做。
默认开始的线程数量与 CPU 数量相同,可使用 -XX:ParallelGCThreads 参数来设置线程数。
是并行的多线程收集器。
其它收集器关注点是尽量缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
停顿时间越短就越适合须要与用户交互的程序,良好的响应速度能提高用户体验。而高吞吐量则能够高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不须要太多交互的任务。
提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,致使吞吐量降低。
还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不须要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行状况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。自适应调节策略也是它与 ParNew 收集器的一个重要区别。
Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。若是用在 Server 模式下,它有两大用途:
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,均可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
CMS(Concurrent Mark Sweep),从 Mark Sweep 能够知道它是基于标记 - 清除算法实现的。
特色:并发收集、低停顿。
分为如下四个流程:
在整个过程当中耗时最长的并发标记和并发清除过程当中,收集器线程均可以与用户线程一块儿工做,不须要进行停顿。
具备如下缺点:
对 CPU 资源敏感。CMS 默认启动的回收线程数是 (CPU 数量 + 3) / 4,当 CPU 不足 4 个时,CMS 对用户程序的影响就可能变得很大,若是原本 CPU 负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能致使用户程序的执行速度突然下降了 50%,其实也让人没法接受。而且低停顿时间是以牺牲吞吐量为代价的,致使 CPU 利用率变低。
没法处理浮动垃圾。因为并发清理阶段用户线程还在运行着,伴随程序运行天然就还会有新的垃圾不断产生。这一部分垃圾出如今标记过程以后,CMS 没法在当次收集中处理掉它们,只好留到下一次 GC 时再清理掉,这一部分垃圾就被称为“浮动垃圾”。也是因为在垃圾收集阶段用户线程还须要运行,那也就还须要预留有足够的内存空间给用户线程使用,所以它不能像其余收集器那样等到老年代几乎彻底被填满了再进行收集,须要预留一部分空间提供并发收集时的程序运做使用。可使用 -XX:CMSInitiatingOccupancyFraction 的值来改变触发收集器工做的内存占用百分比,JDK 1.5 默认设置下该值为 68,也就是当老年代使用了 68% 的空间以后会触发收集器工做。若是该值设置的过高,致使浮动垃圾没法保存,那么就会出现 Concurrent Mode Failure,此时虚拟机将启动后备预案:临时启用 Serial Old 收集器来从新进行老年代的垃圾收集。
标记 - 清除算法致使的空间碎片,给大对象分配带来很大麻烦,每每出现老年代空间剩余,但没法找到足够大连续空间来分配当前对象,不得不提早触发一次 Full GC。
G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot 开发团队赋予它的使命是(在比较长期的)将来能够替换掉 JDK 1.5 中发布的 CMS 收集器。
具有以下特色:
在 G1 以前的其余收集器进行收集的范围都是整个新生代或者老生代,而 G1 再也不是这样,Java 堆的内存布局与其余收集器有很大区别,将整个 Java 堆划分为多个大小相等的独立区域(Region)。虽然还保留新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,而都是一部分 Region(不须要连续)的集合。
之因此能创建可预测的停顿时间模型,是由于它能够有计划地避免在整个 Java 堆中进行全区域的垃圾收集。它跟踪各个 Region 里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的 Region(这也就是 Garbage-First 名称的来由)。这种使用 Region 划份内存空间以及有优先级的区域回收方式,保证了它在有限的时间内能够获取尽量高的收集效率。
Region 不多是孤立的,一个对象分配在某个 Region 中,能够与整个 Java 堆任意的对象发生引用关系。在作可达性分析肯定对象是否存活的时候,须要扫描整个 Java 堆才能保证准确性,这显然是对 GC 效率的极大伤害。为了不全堆扫描的发生,每一个 Region 都维护了一个与之对应的 Remembered Set。虚拟机发现程序在对 Reference 类型的数据进行写操做时,会产生一个 Write Barrier 暂时中断写操做,检查 Reference 引用的对象是否处于不一样的 Region 之中,若是是,便经过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 便可保证不对全堆扫描也不会有遗漏。
若是不计算维护 Remembered Set 的操做,G1 收集器的运做大体可划分为如下几个步骤:
收集器 | 串行、并行 or 并发 | 新生代 / 老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单 CPU 环境下的 Client 模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不须要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不须要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 |
G1 | 并发 | both | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,未来替换 CMS |
对象的内存分配,也就是在堆上分配。主要分配在新生代的 Eden 区上,少数状况下也可能直接分配在老年代中。
大多数状况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
关于 Minor GC 和 Full GC:
大对象是指须要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。常常出现大对象会提早触发垃圾收集以获取足够的连续空间分配给大对象。
提供 -XX:PretenureSizeThreshold 参数,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
JVM 为对象定义年龄计数器,通过 Minor GC 依然存活,而且能被 Survivor 区容纳的,移被移到 Survivor 区,年龄就增长 1 岁,增长到必定年龄则移动到老年代中(默认 15 岁,经过 -XX:MaxTenuringThreshold 设置)。
JVM 并非永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,若是在 Survivor 区中相同年龄全部对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象能够直接进入老年代,无需等待 MaxTenuringThreshold 中要求的年龄。
在发生 Minor GC 以前,JVM 先检查老年代最大可用的连续空间是否大于新生代全部对象总空间,若是条件成立的话,那么 Minor GC 能够确认是安全的;若是不成立的话 JVM 会查看 HandlePromotionFailure 设置值是否容许担保失败,若是容许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若是大于,将尝试着进行一次 Minor GC,尽管此次 Minor GC 是有风险的;若是小于,或者 HandlePromotionFailure 设置不容许冒险,那这时也要改成进行一次 Full GC。
对于 Minor GC,其触发条件很是简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有如下条件:
此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非必定,但不少状况下它会触发 Full GC,从而增长 Full GC 的频率,也即增长了间歇性停顿的次数。所以强烈建议能不使用此方法就不要使用,让虚拟机本身去管理它的内存。可经过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()。
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上缘由引发的 Full GC,调优时应尽可能作到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要建立过大的对象及数组。
使用复制算法的 Minor GC 须要老年代的内存空间做担保,若是出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。
在 JDK 1.7 及之前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的状况下也会执行 Full GC。若是通过 Full GC 仍然回收不了,那么 JVM 会抛出 java.lang.OutOfMemoryError,为避免以上缘由引发的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
执行 CMS GC 的过程当中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多致使暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
类是在运行期间动态加载的。
包括如下 7 个阶段:
其中解析过程在某些状况下能够在初始化阶段以后再开始,这是为了支持 Java 的动态绑定。
虚拟机规范中并无强制约束什么时候进行加载,可是规范严格规定了有且只有下列五种状况必须对类进行初始化(加载、验证、准备都会随着发生):
遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,若是类没有进行过初始化,则必须先触发其初始化。最多见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
使用 java.lang.reflect 包的方法对类进行反射调用的时候,若是类没有进行初始化,则须要先触发其初始化。
当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化。
当虚拟机启动时,用户须要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
当使用 JDK.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);
包含了加载、验证、准备、解析和初始化这 5 个阶段。
加载是类加载的一个阶段,注意不要混淆。
加载过程完成如下三件事:
其中二进制字节流能够从如下方式中获取:
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
主要有如下 4 个阶段:
(一)文件格式验证
验证字节流是否符合 Class 文件格式的规范,而且能被当前版本的虚拟机处理。
(二)元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
(三)字节码验证
经过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
(四)符号引用验证
发生在虚拟机将符号引用转换为直接引用的时候,对类自身之外(常量池中的各类符号引用)的信息进行匹配性校验。
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一块儿分配在 Java 堆中。
初始值通常为 0 值,例以下面的类变量 value 被初始化为 0 而不是 123。
public static int value = 123;
若是类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
public static final int value = 123;
将常量池的符号引用替换为直接引用的过程。
初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 <clinit>() 方法的过程。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员经过程序制定的主观计划去初始化类变量和其它资源。
<clinit>() 方法具备如下特色:
public class Test { static { i = 0; // 给变量赋值能够正常编译经过 System.out.print(i); // 这句编译器会提示“非法向前引用” } static int i = 1; }
与类的构造函数(或者说实例构造器 <init>())不一样,不须要显式的调用父类的构造器。虚拟机会自动保证在子类的 <clinit>() 方法运行以前,父类的 <clinit>() 方法已经执行结束。所以虚拟机中第一个执行 <clinit>() 方法的类确定为 java.lang.Object。
因为父类的 <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); // 输出结果是父类中的静态变量 A 的值 ,也就是 2。 }
<clinit>() 方法对于类或接口不是必须的,若是一个类中不包含静态语句块,也没有对类变量的赋值操做,编译器能够不为该类生成 <clinit>() 方法。
接口中不可使用静态语句块,但仍然有类变量初始化的赋值操做,所以接口与类同样都会生成 <clinit>() 方法。但接口与类不一样的是,执行接口的 <clinit>() 方法不须要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也同样不会执行接口的 <clinit>() 方法。
虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,若是多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。若是在一个类的 <clinit>() 方法中有耗时的操做,就可能形成多个进程阻塞,在实际过程当中此种阻塞很隐蔽。
虚拟机设计团队把类加载阶段中的“经过一个类的全限定名来获取描述此类的二进制字节流 ( 即字节码 )”这个动做放到 Java 虚拟机外部去实现,以便让应用程序本身决定如何去获取所须要的类。实现这个动做的代码模块称为“类加载器”。
对于任意一个类,都须要由加载它的类加载器和这个类自己一同确立其在 Java 虚拟机中的惟一性,每个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof() 关键字作对象所属关系断定等状况),只有在这两个类是由同一个类加载器加载的前提下才有意义,不然,即便这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不一样,那这两个类就一定不相等。
从 Java 虚拟机的角度来说,只存在如下两种不一样的类加载器:
启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;
全部其余类的加载器,这些类由 Java 实现,独立于虚拟机外部,而且全都继承自抽象类 java.lang.ClassLoader。
从 Java 开发人员的角度看,类加载器能够划分得更细致一些:
启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JAVA_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)上所指定的类库,开发者能够直接使用这个类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。
应用程序都是由三种类加载器相互配合进行加载的,若是有必要,还能够加入本身定义的类加载器。下图展现的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其他的类加载器都应有本身的父类加载器,这里类加载器之间的父子关系通常经过组合(Composition)关系来实现,而不是经过继承(Inheritance)的关系实现。
(一)工做过程
若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载,而是把这个请求委派给父类加载器,每个层次的加载器都是如此,依次递归。所以全部的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈本身没法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试本身加载。
(二)好处
使用双亲委派模型来组织类加载器之间的关系,使得 Java 类随着它的类加载器一块儿具有了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 rt.jar 中,不管哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,所以 Object 类在程序的各类类加载器环境中都是同一个类。相反,若是没有双亲委派模型,由各个类加载器自行加载的话,若是用户编写了一个称为java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不一样的 Object 类,程序将变得一片混乱。若是开发者尝试编写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现能够正常编译,可是永远没法被加载运行。
(三)实现
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ // 先检查请求的类是否已经被加载过了 Class c = findLoadedClass(name); if(c == null) { try{ if(parent != null) { c = parent.loadClass(name, false); } else{ c = findBootstrapClassOrNull(name); } } catch(ClassNotFoundException e) { // 若是父类加载器抛出 ClassNotFoundException,说明父类加载器没法完成加载请求 } if(c == null) { // 若是父类加载器没法完成加载请求,再调用自身的 findClass() 来进行加载 c = findClass(name); } } if(resolve) { resolveClass(c); } return c; }
配置 | 描述 |
---|---|
-Xms | 初始化堆内存大小 |
-Xmx | 堆内存最大值 |
-Xmn | 新生代大小 |
-XX:PermSize | 初始化永久代大小 |
-XX:MaxPermSize | 永久代最大容量 |
配置 | 描述 |
---|---|
-XX:+UseSerialGC | 串行垃圾回收器 |
-XX:+UseParallelGC | 并行垃圾回收器 |
-XX:+UseConcMarkSweepGC | 并发标记扫描垃圾回收器 |
-XX:ParallelCMSThreads= | 并发标记扫描垃圾回收器 = 为使用的线程数量 |
-XX:+UseG1GC | G1 垃圾回收器 |
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar