阿里云服务器上做淘宝客网站,电大形考任在哪个网站做,网站建设销售是做什么的,wordpress seo h1标签#x1f431;#x1f453; 一、JVM
1.1 JVM基本定义
定义#xff1a;Java Virtual Machine-Java 程序的运行环境#xff08;Java二进制字节码的运行环境#xff09;
好处#xff1a; 一次编写后#xff0c;任意环境都可运行 自动内存管理、垃圾回收功能 数组下标… 一、JVM1.1 JVM基本定义定义Java Virtual Machine-Java 程序的运行环境Java二进制字节码的运行环境好处一次编写后任意环境都可运行自动内存管理、垃圾回收功能数组下标越界越界检查其他语言无此功能会造成内存地址覆盖多态比较JVM、JRE、JDK学习路线 二 、JVM内存结构JVM内存结构线程私有的程序计数器、虚拟机栈、本地方法栈线程共享的堆、方法区、直接内存 非运行时数据区的⼀部分)1.8之前1.8之后2.1 程序计数器(1) 定义程序计数器是⼀块较小的内存空间可以看作是当前线程所执行的字节码的行号指示器。字节码解释器⼯作时通过改变这个计数器的值来选取下⼀条需要执行的字节码指令分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。为了线程切换后能恢复到正确的执行位置每条线程都需要有⼀个独立的程序计数器各线程之间计数器互不影响独立存储我们称这类内存区域为“线程私有”的内存。作用记住下一条jvm要执行的指令地址字节码解释器通过改变程序计数器来依次读取指令实现代码的流程控制如顺序执行、选择Java源代码-二进制字节码-解释器寻找程序计数器记录-机器码-CPU在多线程的情况下程序计数器用于记录当前线程执行的位置从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。特点线程私有不会内存溢出程序计数器是唯⼀⼀个不会出现OutOfMemoryError的内存区域它的生命周期随着线程的创建而创建随着线程的结束而死亡。2.2 虚拟机栈1定义每个线程只能有一个活动栈帧对应着当前正在执行的那个方法。Java栈中保存的主要内容是栈帧每一次函数调用都会有一个对应的栈帧被压入Java栈每一个函数调用结束后都会有一个栈帧被弹出。与程序计数器一样Java虚拟机栈也是线程私有的它的生命周期和线程相同描述的是Java方法执行的内存模型每次方法调用的数据都是通过栈传递的。Java方法有两种返回方式return语句、抛出异常。不管哪种返回方式都会导致栈帧被弹出。是线程运行时需要的内存空间将每个栈帧执行按顺序压入栈内每个栈帧的内存占用即为每个方法的参数、局部变量、返回地址。(2) 问题辨析垃圾回收是否涉及栈内存不涉及每次出栈后即被释放栈内存空间越大越好吗不物理内存大小固定栈内存越大只能进行更多次的方法调用线程数会变小方法内局部变量是否安全如果方法内局部变量没有处于方法的作用外它是线程安全的。如果局部变量引用了对象并且对象处于方法的作用范围需要考虑线程安全。(3) 栈内存溢出java.lang.StackOverFlowError1.当方法递归调用过多导致栈内存溢出2.栈内存过大导致栈内存溢出3.循环引用两个类属性循环调用在JSON数据转换时出现错误。Java虚拟机栈会出现两种错误StackOverFlowError和OutOfMemoryError。StackOverFlowError若 Java 虚拟机栈的内存大小不允许动态扩展那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候就抛出 StackOverFlowError 错误。OutOfMemoryError若 Java 虚拟机堆中没有空闲内存并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。对于非固定大小的栈在其扩展时扩容如果没有办法获取到足够大小的内存报OutOfMemoryError(4) 设置栈内存大小在idea中edit configuration编辑-Xss256k(5)线程运行诊断案例一CPU占用过多用top定位哪个进程对cpu的占用过高ps H -eo pid,tid,%cpu | grep 进程id 用ps命令进一步定位是哪个线程引起的cpu占用过高jstack 进程id根据线程id 找到有问题的线程进一步定位到问题代码的源码行号案例2程序运行很长时间没有结果死锁2.3 本地方法栈(1)定义和虚拟机栈所发挥的作用非常相似区别是︰虚拟机栈为虚拟机执行Java方法也就是字节码)服务而本地方法栈则为虚拟机使用到的Native方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一。本地方法被执行的时候在本地方法栈也会创建一个栈帧用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。方法执行完后相应的栈帧也会出栈并释放内存空间也会出现StackOverFlowError和OutOfMemoryError两种错误。本地方法栈给本地方法调用分配内存空间调用的本地方法接口其他语言编写间接操作操作系统的方法。// 被native关键字修饰的方法调用本地方法接口例如: protected native Object clone() throws CloneNotSupportedException;2.4 堆(1)定义Java 虚拟机所管理的内存中最大的一块Java 堆是所有线程共享的一块内存区域在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例几乎所有的对象实例以及数组都在这里分配内存。通过 new 关键字创建对象都会使用堆内存它。是线程共享的堆中对象都需要考虑线程安全的问题有垃圾回收机制。Java堆是垃圾收集器管理的主要区域因此也被称作GC堆(Garbage Collected Heap) 。从垃圾回收的角度由于现在收集器基本都采用分代垃圾收集算法所以Java堆还可以细分为︰新生代和老年代∶再细致一点有: Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存或者更快地分配内存。堆最容易出现的就是 OutOfMemoryError 错误。设置堆内存大小-Xmx10m2.5 方法区 方法区是各个线程共享的内存区域它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分但是它却有一个别名叫做Non-Heap(非堆)目的应该是与Java堆区分开来。2.5.1 永久代、元空间永久代和元空间就是方法区的实现在JDK1.8之前方法区的实现是永久代在JDK1.8之后是元空间。方法区就是一个定义规范永久代和元空间就是它的实现。为什么要将永久代(PermGen)替换为元空间(MetaSpace)1整个永久代有一个JVM本身设置固定大小上限无法进行调整而元空间使用的是直接内存受本机可用内存的限制虽然元空间仍旧可能溢出但是比原来出现的几率会更小。当元空间溢出时会得到如下错误 java.lang.OutOfMemoryError: MetaSpace-XX: MaxMetaSpaceSize标志设置最大元空间大小默认值为unlimited这意味着它只受系统内存的限制。-XX: MetaSpaceSize调整标志定义元空间的初始大小如果未指定此标志则Metaspace 将根据运行时的应用程序需求动态地重新调整大小。2元空间里面存放的是类的元数据这样加载多少类的元数据就不由-XX: MaxPermSize永久代内存大小控制了而由系统的实际可用空间来控制这样能加载的类就更多了。3.在JDK8合并HotSpot和JRockit 的代码时, JRockit 从来没有一个叫永久代的东西,合并之后就没有必要额外的设置这么一个永久代的地方了。2.5.2 常量池常量池就是一张表虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息运行时常量池。常量池在方法区中运行时常量池在堆中。常量池是*.class文件中的当该类被加载它的常量池信息就会放入运行时常量池并把里面的符号地址变为真实地址。运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外还有常量池表用于存放编译期生成的各种字面量和符号引用)。2.5.2 StringTableString table又称为String pool字符串常量池当字符串常量未被引用时都只是一个符号当被引用时才会将它创建为对象放在StringTable中是一个懒惰的可以使用intern方法主动放入StringTable数据结构是hash表数组链表1.字符串变量拼接public class Test { public static void main(String[] args) throws InterruptedException { // 基本类型在常量池中 StringTable[a,b,ab] String a a; // 存放在常量池中 String b b; String ab ab; //实际调用new StringBuilder().append(a).append(b).toString,返回的是封装的String对象 new String(ab); String ab1 ab; //变量拼接 //javac 在编译期的优化因为是a,b是常量在编译期已经确定为ab所以不用StirngBuilder方式拼接 String ab2 ab; //常量拼接 System.out.println(ab1 ab); //false } }字节码文件反编译 String ab1 ab; 后指令字节码反编译指令javap -v Test.class2.字符串延迟加载intern()方法主动将常量放入常量池public class Test1 { public static void main(String[] args) { // 常量池 [a,b,ab] 堆 new String(a) 、new String(b) 、new String(ab) String ab new String(a)new String(b); // intern() 方法尝试将字符串对象放入常量池有则不放无则放入返回池中对象 String intern ab.intern(); System.out.println(internab); //true System.out.println(abab); //就是将ab放入的池所以也是true 1.8之前是false } }3.StringTable特性1、stringTable数据结构为一个hash表数组链表不可扩容存字符串常量唯一不重复。 2、常量池中的字符串仅是符号第一次用到才变为对象 3、其创建方式为懒创建用到时才创建常量池中的字符串仅是符号第一次用到时才变为对象。利用串池的机制来避免重复创建字符串对象。字符串变量拼接的原理是stringBuilder (1.8)。字符串常量拼接的原理是编译期优化。·可以使用intern方法主动将串池中还没有的字符串对象放入串池。4.StringTable位置JDK1.6存放在永久代时只会被FullGC即是在老年代满后进行回收导致回收效率过低所以1.8后将方法区放到堆里此时在MinorGC时就会进行回收。5.StringTable垃圾回收打印字符串实例的个数占用的大小信息-XX:PrintStringTableStatistics打印垃圾回收的详细信息回收次数、时间...-XX:PrintGCDetails -verbose:gc6.StringTable性能调优(1) 设置 如字符串常量较多如读取大文件减少串池hash冲突-XXStringTableSize桶个数(2) 考虑将字符串对象是否入池未入池的字符串对象每个都要再堆中分配内存而在串池中重复的字符串引用同一个字符串对象。intern()方法入池list.add( new String(a).intern() ) 放入池中list存入返回的池中信息提高性能。2.6 直接内存普通调用需要走系统缓存区和Java缓冲区两次空间使用直接内存系统本地和Java都可使用。 三、垃圾回收3.1 判断垃圾可回收3.1.1 引用计数法若A、B循环引用虽然A、B都没被引用但是计数已经1故不会被回收。3.1.2 可达性分析、GCRoots(TODO三色标记法)Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象扫描堆中的对象看是否能够沿着GC Root对象为起点的引用链找到该对象找不到表示可以回收GCRoots包括以下几类元素虚拟机栈中引用的对象比如:各个线程被调用的方法中使用到的参数、局部变量等。本地方法栈内JNI(通常说的本地方法)引用的对象方法区中类静态属性引用的对象比如: Java类的引用类型静态变量方法区中常量引用的对象比如:字符串常量池(string Table里的引用所有被同步锁synchronized持有的对象Java虚拟机内部的引用。基本数据类型对应的class对象一些常驻的异常对象如:NullPointerException、OutOfMemoryError)系统类加载器。反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等3.1.3 五种引用1.强引用只有所有GC Roots对象都不通过【强引用】引用该对象该对象才能被垃圾回收。2.软引用(SoftReference)仅有软引用引用该对象时在垃圾回收后内存仍不足时会再次出发垃圾回收回收软引用对象。可以配合引用队列来释放软引用自身。3.弱引用(WeakReference)仅有弱引用引用该对象时在垃圾回收时无论内存是否充足都会回收弱引用对象。可以配合引用队列来释放弱引用自身。4.虚引用(PhantomReference)必须配合引用队列使用主要配合ByteBuffer使用被引用对象回收时会将虚引用入队由referenceHandler线程调用虚引用相关方法释放直接内存。5.终结器引用(FinalReference)无需手动编码但其内部配合引用队列使用在垃圾回收时终结器引用入队(被引用对象暂时没有被回收)再由Finalizer线程优先级较低执行概率小通过终结器引用找到被引用对象并调用它的finalize方法第二次GC时才能回收被引用对象软引用和弱引用对象被垃圾回收时虚引用对象被垃圾回收终结器引用被垃圾回收时3.1.4 实例1.软引用在设置堆内存-Xmax40m后创建5个Byte[4 * 1024 * 1024]对象会内存溢出。public class Test3 { static int _4MB 2*1024 * 1024; public static void main(String[] args) { ArrayListByte[] bytes new ArrayList(); for (int i 0;i5;i){ bytes.add(new Byte[_4MB]); System.out.println(i); } System.out.println(bytes); } } java.lang.OutOfMemoryError: Java heap space使用软引用对Byte数组引用在垃圾回收后若内存不足将释放软引用的对象public class Test3 { static int _4MB 2*1024 * 1024; public static void main(String[] args) { ArrayListSoftReferenceByte[] bytes new ArrayList(); // 引用队列 ReferenceQueueByte[] referenceQueue new ReferenceQueue(); for (int i 0;i5;i){ // 将byte数组进行软引用 将软引用对象放入引用队列 SoftReferenceByte[] softReference new SoftReference(new Byte[_4MB],referenceQueue); bytes.add(softReference); System.out.println(i); } // 清除队列中的软引用 Reference? extends Byte[] poll referenceQueue.poll(); while (poll ! null) { bytes.remove(poll); poll referenceQueue.poll(); } for (SoftReferenceByte[] item : bytes) { System.out.println(item.get()); } } }2.弱引用static void weakReference(){ ArrayListWeakReferenceByte[] bytes new ArrayList(); // 引用队列 ReferenceQueueByte[] referenceQueue new ReferenceQueue(); for (int i 0;i7;i){ // 将byte数组进行软引用 将软引用对象放入引用队列 WeakReferenceByte[] softReference new WeakReference(new Byte[_4MB],referenceQueue); bytes.add(softReference); System.out.println(i); } // 清除队列中的软引用 Reference? extends Byte[] poll referenceQueue.poll(); while (poll ! null) { bytes.remove(poll); poll referenceQueue.poll(); } for (WeakReferenceByte[] item : bytes) { System.out.println(item.get()); } }3.2 垃圾回收算法 3.2.1 标记清除标记清除回收算法分为两步1、第一次扫描记录可被回收的地址 2、第二次根据地址进行清除优点速度快缺点回收内存后不会进行合并会造成内存碎片3.2.2 标记整理算法标记整理分为两步1、标记存活的内存地址 2、整理存活的内存到一起优点不会造成内存碎片的问题。缺点因为在整理过程中会移动内存地址效率会降低。3.2.3 复制算法复制算法将内存分为from区和to区将from区中未引用的内存标记将被引用的内存转到to区将from区全部释放然后from区和to区对换。优点不会产生内存碎片。缺点会占用两倍内存空间。3.2.4 分代回收对象首先分配在伊甸园区新生代空间不够时触发MinorGC清理整个新生代空间采用复制算法将伊甸园和from区的对象复制到to区中对象存活时间1然后from区和to区交换保证to区为空交换数据内存MinorGC会引发stop the world暂停其他线程等垃圾回收结束后用户线程才恢复运行当幸存区对象寿命超过阈值会晋升到老年代最大寿命是154bit当老年代空间不足会先尝试触发MinorGC如果空间仍不足触发FullGCSTW时间更长在Eden区内存够的情况下创建的对象会优先选择放到Eden区。如果在存入对象时Eden区的内存不够存的那就会进行一次minor gc垃圾回收整个新生代然后将Eden区survivor from中然后再清空Eden区将新对象存入Eden区若Eden区存不下将直接放入老年代。若survivor区存不下则将部分对象存入老年区。之后再进来的对象还是会选择放到Eden区如果Eden区又存放不下了这时就会将Eden区和survivor from中存活的对象都复制到survivor to中然后清空Eden区和survivor from并且将survivor from和survivor to进行位置交换目的就是为了保证survivor to中不存放对象。如果这个时候新生代还是放不下这个对象那该对象就会被放到老年代中。若survivor区存不下则将部分对象存入老年区。原文链接3.3 相关VM参数 堆初始大小-Xms堆最大大小-Xmx或-XX:MaxHeapSizesize新生代大小-Xmn或(XX:NewSizesize -XX:MaxNewSizesize )幸存区比例(动态)-Xx:InitialSurvivorRatioratio和-XX:UseAdaptiveSizePolicy幸存区比例-Xx:SurvivorRatioratio晋升阈值-XX:MaxTenuringThresholdthreshold晋升详情-XX:PrintTenuringDistributionGc详情-XX:PrintGCDetails -verbose:gcFullGC前MinorGC-XX:ScavengeBeforeFullGC3.4 垃圾回收器3.4.1 串行单线程堆内存小适合个人电脑-XX:-UserSerialGC Serial(新生代复制算法)SerialOld(老年代标记整理算法)3.4.2 吞吐量优先多线程堆内存较大多核CPU让单位时间内STW时间最短-XX:UseParallelGC~ -XX :UseParalle10ldGc 新生代复制算法)(老年代标记整理算法 -XX:UseAdaptivesizePolicy 采用自适应调整新生代大小策略 -XX:GCTimeRatioratio 垃圾回收时间与总时间的占比 -XX:MaxGCPauseMillisms 垃圾回收暂停时间 默认200ms -XX:Paralle1GCThreadsn GC时线程数3.4.3 响应时间优先 CMS多线程堆内存较大多核CPU让单次STW时间最短-XX :UseConcMarkSweepGC~老年代GC并发出现问题时由于标记清除算法造成的内存碎片问题老年代GC会退回到SerialOld进行一次标记整理-XX :UseConcMarkSweepGC~ -XX:UseParNewGC ~ SerialOld 复制算法)(标记清除算法)-XX: ParallelGCThreadsn ~ -XX: ConcGCThreadsthreads 并行垃圾回收线程数并发垃圾回收线程数-xX:CMSInitiatingOccupancyFractionpercent 当内存占比到percent的时候进行并发标记-XX :CNSScavengeBeforeRemark 1,0 重新标记会标记所有引用对象在并发高时可能新生代对象较多且是垃圾导致在重新标记花费长时间在重写标记发生之前对新生代垃圾进行清理减少重新标记的时间。当老年代发生内存不足其中一个线程进行初始标记仅标记GCRoot对象到阈值后就可进行并发标记标记所有初始标记在这个阶段需要虚拟机停顿正在执行的任务官方的叫法STW(Stop The Word)。这个过程从垃圾回收的根对象开始只扫描到能够和根对象直接关联的对象并作标记。所以这个过程虽然暂停了整个JVM但是很快就完成了。并发标记这个阶段紧随初始标记阶段在初始标记的基础上继续向下追溯标记。并发标记阶段应用程序的线程和并发标记的线程并发执行所以用户不会感受到停顿。并发预清理并发预清理阶段仍然是并发的。在这个阶段虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代 或者有一些对象被分配到老年代)。通过重新扫描减少下一个阶段重新标记的工作因为下一个阶段会Stop The World。重新标记这个阶段会暂停虚拟机收集器线程扫描在CMS堆中剩余的对象。扫描从跟对象开始向下追溯并处理对象关联。并发清理清理垃圾对象这个阶段收集器线程和应用程序线程并发执行。3.4.4 G1博客注重吞吐量和低延迟默认暂停目标是200ms超大堆内存会将堆划分为多个大小相等的Region整体上是标记整理算法两个区域之间是复制算法-XX: UserG1GC JDK9以后默认使用-XX:G1HeapRegionSizesize 设置region区域大小-XX:MaxGCPauseMillisms1G1垃圾回收阶段年轻代GC-年轻代GC并发标记-混合回收2Young Collection会STW3Young Collection CM在Young GC是会进行GC Root的初始标记老年代占用堆空间比例达到阈值时进行并发标记STW-XX:InitiatingHeapOccupancyPercentpercent 默认45%4Mixed Collection会对E、S、O进行全面垃圾回收最终标记Remark会STW拷贝存活Evacuation会STW-XX:MaxGCPauseMillisms在回收老年代Region时会根据最大暂停时间选择部分回收价值最高的即能回收的垃圾最多的Region进行回收。5Full GCSerialGC新生代内存不足发生的垃圾收集 - minor gc老年代内存不足发生的垃圾收集 - full gcParallelGC新生代内存不足发生的垃圾收集 - minor gc老年代内存不足发生的垃圾收集 - full gcCMS新生代内存不足发生的垃圾收集 - minor gc老年代内存不足G1新生代内存不足发生的垃圾收集 - minor gc老年代内存不足6Young Collection跨代引用新生代垃圾回收的过程首先要找到根对象然后对根对象进行可达性分析找到存活对象对存活对象进行复制复制到幸存区产生问题找到新生代对象的根对象根对象有一部分是来自老年代的而老年代存活的对象一般都特别多如果去遍历整个老年代效率非常低。采用了cart table的方式对老年代进行细分分成了许多个card,每个card大约是512K。如果老年代某个对象引用了新生代的对象我们把这个老年代的对象标记为脏card。这样找老年代的根对象时就不用遍历整个老年代了只需要关注脏card减小搜索范围提高效率。如下图粉色为脏card,绿色为伊甸园区蓝色为幸存者区橙色为老年代。老年代有脏卡标记而新生代则有remembered Set记录外部对它的引用记录都有哪些脏卡。将来对新生代进行垃圾回收时先通过remembered Set 知道有哪些脏卡然后通过脏卡区域遍历GC Root。在引用变更时通过post-write barrier dirty card queue,在每次的引用变更时都要更新标记脏卡异步操作把更新的指令放到一个队列dirty card queue之中将来由一个线程执行更新操作。7remark在并发标记阶段当对象引用改变时JVM会加入一个写屏障,引用改变,写屏障指令就会被执行,会将该对象加入一个队列中,将对象变为为处理状态,等到整个并发标记结束,进入重新标记阶段会STW, 对象出队列,进行标记。8字符串去重9JDK 8u40并发标记类卸载所有对象都经过并发标记后就能知道哪些类不再被使用当一个类加载器的所有类都不再使用则卸载它所加载的所有类 -XX:ClassUnloadingWithConcurrentMark 默认启用10回收巨型对象一个对象大于region的一半时称之为巨型对象.G1不会对巨型对象进行拷贝回收时被优先考虑。 G1会跟踪老年代所有incoming 引用这样老年代 incoming 引用为0的巨型对象可以在新生代垃圾回收。11JDK9并发标记起始时间的调整并发标记必须在堆空间占满前完成否则退化为FullGCJDK9之前需要使用-Xx:InitiatingHeapOccupancyPercent- JDK 9可以动态调整-XX:InitiatingHeapOccupancyPercent 用来设置初始值进行数据采样并动态调整总会添加一个安全的空档空间3.5 垃圾回收调优 查看虚拟机运行参数java -XX:PrintFlagsFinal -version | findstr GC3.5.1 调优领域内存、锁竞争、CPU占用、IO、GC3.5.2 确定目标【低延迟】还是【高吞吐量】选择合适的回收器CMSG1ZGC 低延迟响应时间优先ParallelGCZing3.5.3 最快的 GC最快的GC是不发生GC查看Full GC前后的内存占用考虑以下几个问题数据是不是太多resultSet statement.executeQuery(select * from 大表)数据表示是否太臃肿 对象图 对象大小是否存在内存泄漏3.5.4 新生代调优新生代的特点所有的new操作分配内存都是非常廉价的TLAB thread-local allocation buffer可防止多个线程创建对象时的干扰死亡对象回收零代价大部分对象用过即死朝生夕死Minor GC 所用时间远小于Full GC新生代内存越大越好么不是新生代内存太小频繁触发Minor GC会STW会使得吞吐量下降新生代内存太大老年代内存占比有所降低会频繁地触发Full GC。而且触发Minor GC时清理新生代花费的时间更长新生代内存设置为能容纳[并发量*(请求-响应)]的数据为宜幸存区大到能保留【当前活跃对象需要晋升对象】晋升阈值配置得当让长时间存活对象尽快晋升3.5.5 老年代调优以 CMS 为例 CMS 的老年代内存越大越好先尝试不做调优如果没有 Full GC 那么已经…否则先尝试调优新生代观察发生 Full GC 时老年代内存占用将老年代内存预设调大 1/4 ~ 1/3-XX:CMSInitiatingOccupancyFractionpercent 四、类加载4.1 类加载过程4.1.1加载将类的字节码载入方法区中内部采用 C 的 instanceKlass 描述 java 类它的重要 field 有_java_mirror 即 java 的类镜像例如对 String 来说就是 String.class作用是把 klass 暴露给 java 使用_super 即父类_fields 即成员变量_methods 即方法_constants 即常量池_class_loader 即类加载器_vtable 虚方法表_itable 接口方法表如果这个类还有父类没有加载先加载父类加载和链接可能是交替运行的结果加载完成之后会在堆中实例化一个Java类的原型模板—–类模板对象Class对象该对象用来访问方法区中的类信息方法信息域filed信息使用new关键字创建对象的时候首先会去这个类对应的Class对象获取到该类的信息然后再创建对象。因此将Class对象看做是类的模板类创建的对象可以有很多但是模板只有一份也就是说每个类对应的Class对象只有一个。注意:instanceKlass 这样的【元数据】是存储在方法区1.8 后的元空间内 _java_mirror是存储在堆中。可以通过前面介绍的 HSDB 工具查看。4.1.2链接1验证验证类是否符合 JVM规范安全性检查。这一步骤是确保Class文件的字节流中包含的信息要符合虚拟机规范中的要求保证这些信息被当做代码运行后不会危害虚拟机自身的安全。2 准备这个阶段做的事情就是为静态变量分配内存然后赋值普通静态变量赋默认值加上final的静态变量直接赋值。为 static 变量分配空间设置默认值static 变量在 JDK 7 之前存储于 instanceKlass 末尾从 JDK 7 开始存储于 _java_mirror 末尾static 变量分配空间和赋值是两个步骤分配空间在准备阶段完成赋值在初始化阶段完成如果 static 变量是 final 的基本类型以及字符串常量那么编译阶段值就确定了赋值在准备阶段完成如果 static 变量是 final 的但属于引用类型那么赋值也会在初始化阶段完成3解析将常量池中的符号引用解析为直接引用public class Load2 { public static void main(String[] args) throws ClassNotFoundException,IOException { ClassLoader classloader Load2.class.getClassLoader(); // loadClass 方法不会导致类的解析和初始化 Class? c classloader.loadClass(cn.itcast.jvm.t3.load.C); // new C(); System.in.read(); } } class C { D d new D(); } class D { }4.1.3 初始化4.3初始化clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的初始化即调用 clinit() 虚拟机会保证这个类的『构造方法』的线程安全发生的时机概括得说类初始化是【懒惰的】main 方法所在的类总会被首先初始化首次访问这个类的静态变量或静态方法时子类初始化如果父类还没初始化会引发子类访问父类的静态变量只会触发父类的初始化Class.forNamenew 会导致初始化不会导致类初始化的情况访问类的 static final 静态常量基本类型和字符串不会触发初始化类对象.class 不会触发初始化创建该类的数组不会触发初始化实验class A { static int a 0; static { System.out.println(a init); } } class B extends A { final static double b 5.0; static boolean c false; static { System.out.println(b init); } }验证实验时请先全部注释每次只执行其中一个public class Load3 { static { System.out.println(main init); } public static void main(String[] args) throws ClassNotFoundException { // 1. 静态常量基本类型和字符串不会触发初始化 System.out.println(B.b); // 2. 类对象.class 不会触发初始化 System.out.println(B.class); // 3. 创建该类的数组不会触发初始化 System.out.println(new B[0]); // 4. 不会初始化类 B但会加载 B、A ClassLoader cl Thread.currentThread().getContextClassLoader(); cl.loadClass(cn.itcast.jvm.t3.B); // 5. 不会初始化类 B但会加载 B、A ClassLoader c2 Thread.currentThread().getContextClassLoader(); Class.forName(cn.itcast.jvm.t3.B, false, c2); // 1. 首次访问这个类的静态变量或静态方法时 System.out.println(A.a); // 2. 子类初始化如果父类还没初始化会引发 System.out.println(B.c); // 3. 子类访问父类静态变量只触发父类初始化 System.out.println(B.a); // 4. 会初始化类 B并先初始化类 A Class.forName(cn.itcast.jvm.t3.B); } }4.2类加载器1.引导类加载器BootstrapClassLoaderc/c语言实现嵌套在jvm内部用来加载Java的核心类库JAVA_HOME/jre/lib/rt.jar或者sum.boot.class.path不继承java.lang.ClassLoader,没有父加载器出于安全考虑只加载包名为java、javax、sum等开头的类加载扩展类加载器和系统类加载器并指定为他们的父加载器。无法被获取2.拓展类加载器ExtensionClassLoaderJava语言编写由sum.misc.Launcher$ExtClassLoader实现继承于ClassLoader类父类加载器为引导类加载器从java.ext.dirs系统属性所指定的目录中加载类库或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户常见的JAR放在此目录下也会有拓展类加载器加载3.系统类加载器AppClassLoaderJava语言编写由sum.misc.Launcher$AppClassLoader实现继承于ClassLoader类父类加载器为引导类加载器负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库应用程序中的类加载器默认是系统类加载器它是用户自定义类加载器的默认父加载器通过ClassLoader的getSystemClassLoader方法可以获取到系统类加载器4.用户自定义类加载器通过自定义类加载器可以实现插件机制通过自定义类加载器能够实现应用隔离自定义类加载器要继承与ClassLoader类加载分类显示加载指的是在代码中通过调用ClassLoader加载class对象例如直接使用Class.forName(name)或者this.getClass().getClassLoader().loadClass(name)加载对象隐式加载不在代码中调用ClassLoder的方法加载class对象而是通过虚拟机自动加载到内存中例如在某个类的class文件中引用了另一个类的对象额外引用的类会通过jvm自动加载到内存4.2.1 启动类加载器用 Bootstrap 类加载器加载类package cn.itcast.jvm.t3.load; public class F { static { System.out.println(bootstrap F init); } }执行package cn.itcast.jvm.t3.load; public class Load5_1 { public static void main(String[] args) throws ClassNotFoundException { Class? aClass Class.forName(cn.itcast.jvm.t3.load.F); System.out.println(aClass.getClassLoader()); } }输出E:\git\jvm\out\production\jvmjava -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5 bootstrap F init null-Xbootclasspath 表示设置 bootclasspath其中 /a:. 表示将当前目录追加至 bootclasspath 之后可以用这个办法替换核心类java -Xbootclasspath:new bootclasspathjava -Xbootclasspath/a:追加路径 后追加java -Xbootclasspath/p:追加路径 前追加4.3 双亲委派模式所谓的双亲委派就是指调用类加载器的 loadClass 方法时查找类的规则。protected Class? loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1. 检查该类是否已经加载 Class? c findLoadedClass(name); if (c null) { long t0 System.nanoTime(); try { if (parent ! null) { // 2. 有上级的话委派上级 loadClass c parent.loadClass(name, false); } else { // 3. 如果没有上级了ExtClassLoader则委派 BootstrapClassLoader c findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c null) { long t1 System.nanoTime(); // 4. 每一层找不到调用 findClass 方法每个类加载器自己扩展来加载 c findClass(name); // 5. 记录耗时 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }public class Load5_3 { public static void main(String[] args) throws ClassNotFoundException { Class? aClass Load5_3.class.getClassLoader() .loadClass(cn.itcast.jvm.t3.load.H); System.out.println(aClass.getClassLoader()); } }执行流程为sun.misc.Launcher$AppClassLoader //1 处 开始查看已加载的类结果没有sun.misc.Launcher$AppClassLoader // 2 处委派上级sun.misc.Launcher$ExtClassLoader.loadClass()sun.misc.Launcher$ExtClassLoader // 1 处查看已加载的类结果没有委派上级BootstrapClassLoaderBootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类显然没有sun.misc.Launcher$ExtClassLoader // 4 处调用自己的 findClass 方法是在JAVA_HOME/jre/lib/ext 下找 H 这个类显然没有回到 sun.misc.Launcher$AppClassLoader的 // 2 处继续执行到 sun.misc.Launcher$AppClassLoader // 4 处调用它自己的 findClass 方法在classpath 下查找找到了4.4 自定义类加载器什么时候需要自定义类加载器1想加载非 classpath 随意路径中的类文件2都是通过接口来使用实现希望解耦时常用在框架设计3这些类希望予以隔离不同应用的同名类都可以加载不冲突常见于 tomcat 容器步骤继承 ClassLoader 父类要遵从双亲委派机制重写 findClass 方法注意不是重写 loadClass 方法否则不会走双亲委派机制读取类文件的字节码调用父类的 defifineClass 方法来加载类使用者调用该类加载器的 loadClass 方法