JVM入门
一、什么是虚拟机
所谓虚拟机,就是一台虚拟机器。他是一款软件,用来执行一系列虚拟计算指令,大体上虚拟机可以分为系统虚拟机和程序虚拟机。Visual Box、VMare就属于系统虚拟机。而程序虚拟机典型代表就是java虚拟机,他专门为执行单个计算机程序而设计。
二、认识java虚拟机的基本结构
说到底:操作JVM就是操作Java堆(heap)和垃圾回收机制(GC)
其中:
1.类加载子系统:负责从文件系统或者网络中加载Class信息,加载的信息存放在一块称之为方法区的内存空间。
2.方法区:就是存放类信息、常量信息、常量池信息、包括字符串字量和数字常量等。
3.java堆:在java虚拟机启动时建立java堆,他是java程序最主要的内存工作区域,几乎所有的对象实例都放在java堆中,堆空间是所有线程共享的。
4.直接内存:java的NIO库允许java程序使用直接内存,从而提高性能,通过直接内存速度会优于java堆。读写频繁的场合可能会考虑使用。
5.每个虚拟机线程都有一个私有的栈,一个线程的java栈在线程创建的时候被创建,java栈中保存着局部变量、方法参数、同时java的方法调用、返回值等。
6.本地方法栈和java栈类似,不同是本地方法栈使用本地方法调用。java虚拟机允许java直接调用本地方法。
7.垃圾收集系统(GC)为java核心,java有一套自己进行垃圾清理的机制开发人员无需手工清理。
8.PC寄存器:每个线程启动的时候,都会创建一个PC(Program Counter,程序计数器)寄存器。PC寄存器里保存有当前正在执行的JVM指令的地址。 每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。保存下一条将要执行的指令地址的寄存器是 :PC寄存器。PC寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
三、认清堆、栈、方法去关系
其中:
1.堆:用于存储数据
2.栈:用于解决程序的运行问题
3.方法区:辅助堆栈的一块永久区,解决栈堆信息的产生,此为先决条件。
如图:当创建一个新对象,User类的信息开始都存放在方法区。当User类实例化后,类信息被存储到java堆中的一块内存,使用时,都是使用栈中User对象的引用。
四、关于Java堆
java堆完全自动化管理,通过垃圾回收机制,垃圾对象自动清理,无需显示地释放。
其中:
1.java对有许多不同结构,最常见的就是将整个队分为新生代和老年代。
2.新生代分为eden区、s0区(或from区)、s1区(或to区),s0与s1大小相等且可互换。
3.eden:伊甸园:java对象刚new出来时,这个实例化对象是放在eden区的,经过一系列操作后,执行gc,对象被分配到s0区或者s1区(不可能既存放在s0也存放在s1 区),最后进入老年代。
4.为什么会分为s0和s1区呢?
因为新生代产生对象太频繁,当有一大堆对象同时进行gc操作时(其中有源源不断的对象产生),其中有部分可能会进入老年区,有部分可能还存活,虚拟机 操作就将存活的那部分复制到另一块区(s0或者s1),当前所在的区(s0或者s1)则执行truncat摧毁,也就是将剩余未存活的一次性进入老年区,也就是清理空间。
五、关于虚拟机参数
虚拟机提供一些跟踪系统状态的参数,使用给定的参数执行java虚拟机,就可以在系统运行时打印相关日志,用于分析实际问题。
1、堆配置参数(一)
-XX:+PrintGC #使用该参数,虚拟机启动后,只要遇到GC就会打印日志
-XX:+UseSerialGC #配置串行回收器
-XX:+PrintGCDetails #可查看详细信息,包括各个区的情况
-Xms: #设置Java程序启动时初始堆大小
-Xmx #设置Java程序能获得最大堆大小
-XX:+PrintCommandLineFlags #可以将隐式或者显示传给虚拟机的参数输出
详细测试代码如下:
public class Test01 { public static void main(String[] args) { //-Xms5m -Xmx20m -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+PrintCommandLineFlags //初始化5M 最大20M 启动打印GC详细信息 启动采用串行的垃圾回收机制 打印这行配置参数信息 //查看GC信息(最开始前) System.out.println("max memory:" + Runtime.getRuntime().maxMemory()); System.out.println("free memory:" + Runtime.getRuntime().freeMemory());//比分配值少点,这是根据自身机器配置而定,机器越好,吃内存越少 System.out.println("total memory:" + Runtime.getRuntime().totalMemory()); byte[] b1 = new byte[1*1024*1024]; System.out.println("分配了(使用了)1M"); System.out.println("max memory:" + Runtime.getRuntime().maxMemory()); System.out.println("free memory:" + Runtime.getRuntime().freeMemory()); System.out.println("total memory:" + Runtime.getRuntime().totalMemory()); byte[] b2 = new byte[4*1024*1024]; System.out.println("分配了4M"); System.out.println("max memory:" + Runtime.getRuntime().maxMemory()); System.out.println("free memory:" + Runtime.getRuntime().freeMemory()); System.out.println("total memory:" + Runtime.getRuntime().totalMemory()); } }
配置参数:run-->Run Configurations-->Arguments(其中Java Application一定要指定当前测试的类)
将配置的参数输入到VM arguments
优化:
将初始堆大小和最大堆大小设置相等,可以减少程序运行时的垃圾回收次数,从而提高性能。
2、堆配置参数(二)
-Xmn #可以设置新生代大小,其大小的设置会减少老年代的大小,新生代大小一般设置整个堆空间的1/3到1/4左右
-XX:SurvivorRatio #用于设置新生代中eden空间和from/to空间比列
详细代码如下:
public class Test02 { public static void main(String[] args) { //第一次配置 //-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC //最大20M 最小20M 新生代1M 设置eden/to比列为2 //第二次配置 //-Xms20m -Xmx20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC //第三次配置 //-XX:NewRatio=老年代/新生代 //-Xms20m -Xmx20m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC byte[] b = null; //连续向系统申请10MB空间 for(int i = 0 ; i <10; i ++){ b = new byte[1*1024*1024]; } } }
优化:
新生代的内存尽量设置大点(老年代十分耗内存),从而可以减少老年代的gc操作(如果新生代内存不够,对象会被分配到老年代,这样许多大对象放在老年代,会产生负gc,即java崩溃,内存溢出)
关于新生代内存分配:--XX:SurvivorRatio=eden/from=eden/to=2 表示eden与from或者to的比列是2/1。除此之外,还可以使用(-XX:NewRatio)设置新生代和老年代的比列:-XX:NewRatio=老年代/新生代。
3、堆溢出处理
在程序运行过程中,如果堆空间不足,会抛内存溢出(Out Of Menory)OOM,此能造成业务中断。
-XX:+HeapDumpOnOutOfMemoryError #导出内存溢出时堆整个信息
与之配合的是内存分析工具:Memory Analyzer
详细代码如下:
public class Test03 { public static void main(String[] args) { //-Xms2m -Xmx2m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/Test.dump //此是发生内存溢出,将溢出信息打印到D盘,生成Test03.dum文件,将此文件导入到eclipes并且配合Memory Analyzer使用 //堆内存溢出 Vector v = new Vector(); for(int i=0; i < 5; i ++){ v.add(new Byte[1*1024*1024]); } } }
此代码会发生内存溢出,会在指定的路径下生成Test.dump文件,将文件导入当前工程下,利用Memory Analyzer可分析堆信息情况
4、栈配置
-Xss #指定线程的最大栈空间,配置的参数可以直接决定函数可调用的最大深度
详细代码如下:
public class Test04 { //-Xss1m //-Xss5m //栈调用深度 private static int count; public static void recursion(){ count++; recursion(); } public static void main(String[] args){ try { recursion(); } catch (Throwable t) { System.out.println("调用最大深入:" + count); t.printStackTrace(); } } }
5、方法区参数配置
和java堆一样,方法区是一块所有线程共享的内存区域,它用于保存系统的类信息,方法区可以报错多少信息可以对其设置,默认情况下,-XX:MaxPermSize为64MB
-XX:PermSize=64M -XX:MaxPermSize=64M
六、垃圾回收算法
1、什么是垃圾回收
GC垃圾是指存在于内存、不会再被使用的对象,而回收就是相当于把垃圾清理掉。其中垃圾回收有几种常用的算法:如计数法、标记压缩法、复制算法、分代、分区的思想。
其中:
1)引用计数法
概念:在堆中存储对象时,在对象头处维护一个counter计数器,如果一个对象增加了一个引用与之相连,则将counter++。如果一个引用关系失效则counter–。如果一个对象的counter变为0,则说明该对象已经被废弃,不处于存活状态。 jdk从1.2开始增加了多种引用方式:软引用、弱引用、虚引用
缺点:无法处理循环引用的情况,每次加减操作浪费系统性能。
2)标记清除法
概念:分为标记和清除两个阶段。标记阶段:找到所有可访问的对象,做个标记。清除阶段:遍历堆,把未被标记的对象回收。
缺点:容易产生空间碎片。
3)复制算法
概念:将内存空间分为两块,每次只是用一块,垃圾回收时将正在使用的内存存留的对象复制到未被使用的内存块去,然后去除之前正在是用的内存块中所有的对象,反复交互两个内存角色,完成垃圾回收(from/to采用此算法(优化:此能达到快速运作))
4)标记压缩法
概念:将存活的对象压缩到内存一端,而后进行垃圾清理(可以说是标记清除法升级版,也解决了内存空间碎片问题。老年代采用了此算法(优化:减少空间碎片))
5)分代算法
概念:将内存分为N块,根据每个内存的特点使用不同的算法。针对于新生代和老年代。新生代回收频率高,耗时短,老年代回收频率低,但耗时长,应该尽量减少老年代GC。
6)分区算法
概念:将内存分为N多个小的独立空间,每个小空间都可以独立使用,从而提升了性能,减少了GC的停顿时间。
七、垃圾回收时的停顿现象
垃圾回收器的任务时识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以高效的执行,大部分情况下,会要求系统进入一个停顿的状态。停顿的目的是终止所有应用线程,只有这样系统才不会有新的垃圾产生,同时停顿保证了系统状态下某一个瞬间的一致性,有益于更好地标记垃圾对象。
八、对象如何进入老年代
对象首次创建会被放置到新生代的eden区,若没有GC介入,对象不会离开eden区。只要对象的年龄达到一定的大小,就会自动离开年轻代进入老年代,对象年龄是由对象经历数次GC决定,在新生代每次GC之后如果对象没有被回收则年龄增加1。
-XX:MaxTenuring Threshold #此可控制新生代对象的最大年龄,当超过这个年龄范围就会晋升为老年代
--XX:PretenureSizeThreshold #设置对象的大小超过指定的大小后,可直接晋升到老年代(大对象eden区无法装入)
详细代码如下:
//测试进入老年代的对象 // //参数:-Xmx1024M -Xms1024M -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintGCDetails //-XX:+PrintHeapAtGC Map<Integer, byte[]> m = new HashMap<Integer, byte[]>(); for(int i =0; i <5 ; i++) { byte[] b = new byte[1024*1024]; m.put(i, b); } //此for循环是一直引用着m,执行15次之后的情况 for(int k = 0; k<20; k++) { for(int j = 0; j<300; j++){ //这里循环了6000次,申请了6M内存,但是内存只初始化了1M, 当循环超过15此时,肯定会内存溢出 byte[] b = new byte[1024*1024]; } }
执行情况如下:
由图可知:当循环执行到16次时,新生代执行转为0,直接进入到了老年代
九、关于TLAB
1、概念
TLAB全称Thread Local Allocation Buffer即线程本地分配缓存,是为了加速对象分配而生的。每个线程都会产生一个TLAB,该线程独享工作区域,java虚拟机使用这个TLAB区避免多线程冲突问题,提高了对象分配的效率。TLAB空间一般不会太大,当大对象无法在TLAB分配时,则直接分配到堆上。
-XX:+UseTLAB #使用TLAB
-XX:+TLABSize #设置TLAB大小
-XX:TLABRefillWasteFraction #设置维护进入TLAB空间的单个对象大小,它是个比列值,默认为64,若对象大于整个空间的1/64,则在堆中创建对象
-XX:+PrintTLAB #查看TLAB信息
-XX:ResizeTLAB #自动调整TLABRefillWasteFraction阀值
Jdk默认使用TLAB,如果禁用-XX:-UseTLAB,当然禁用替换配置(TLAB)可以优化打印信息速度
线程执行过程中,优先进入自己有个默认的存储空间TLAB,当对象过大时才存入推。
关于-XX:PretenureSizeThreshold=1000
即当对象上限值为1K,如果超过就直接丢入老年代,不会进入新生代,但是这里排除UseTLAB机制,即虚拟机对于体积不大的对象 会优先把数据分配到TLAB区域中,因此就失去了在老年代分配的机会
代码:
//参数1:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 //这种现象原因为:虚拟机对于体积不大的对象 会优先把数据分配到TLAB区域中,因此就失去了在老年代分配的机会 //参数2:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 -XX:-UseTLAB Map<Integer, byte[]> m = new HashMap<Integer, byte[]>(); for(int i=0; i< 5*1024; i++){ byte[] b = new byte[1024]; m.put(i, b); }
十、关于JVM相关面试题
此面试题来源于网上,答案是自己的一些思路
1、JVM的内存结构。
JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配。
方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。
Java堆(Heap)
Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。几乎所有的对象实例都在这里分配内存。
方法区(Method Area)
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2、JVM的栈中引用如何和堆中的对象产生关联。
如student s=new Student()
S放在栈中,new Student()是放在堆中,这里是通过对象地址引用s(指针)指向new student实列。
3、逃逸分析技术。
1)什么是逃逸分析
通俗一点讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。
而用来分析这种逃逸现象的方法,就称之为逃逸分析。
2)逃逸分析优化JVM原理
我们知道java对象是在堆里分配的,在调用栈中,只保存了对象的指针。
当对象不再使用后,需要依靠GC来遍历引用树并回收内存,如果对象数量较多,将给GC带来较大压力,也间接影响了应用的性能。减少临时对象在堆内分配的数量,无疑是最有效的优化方法。
怎么减少临时对象在堆内的分配数量呢?不可能不实例化对象吧!
场景介绍
其实,在java应用里普遍存在一种场景。一般是在方法体内,声明了一个局部变量,且该变量在方法执行生命周期内未发生逃逸(在方法体内,未将引用暴露给外面)。
按照JVM内存分配机制,首先会在堆里创建变量类的实例,然后将返回的对象指针压入调用栈,继续执行。
这是优化前,JVM的处理方式。
逃逸分析优化 - 栈上分配
优化原理:分析找到未逃逸的变量,将变量类的实例化内存直接在栈里分配(无需进入堆),分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。
这是优化后的处理方式,对比可以看出,主要区别在栈空间直接作为临时对象的存储介质。从而减少了临时对象在堆内的分配数量。
逃逸分析的原理很简单,但JVM在应用过程中,还是有诸多考虑。
比如,逃逸分析不能在静态编译时进行,必须在JIT里完成。原因是,与java的动态性有冲突。因为你可以在运行时,通过动态代理改变一个类的行为,此时,逃逸分析是无法得知类已经变化了。
4、GC的常见算法,CMS以及G1的垃圾回收过程,CMS的各个阶段哪两个是Stop the world的,CMS会不会产生碎片,G1的优势。
1) GC常见的算法:标记清除法、复制算法、标记整理、分代收集算法
2)什么是CMS
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法实现,它的运作过程如下:
1)初始标记
2)并发标记
3)重新标记
4)并发清除
初始标记、从新标记这两个步骤仍然需要“stop the world”,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,熟读很快,并发标记阶段就是进行GC Roots Tracing,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生表动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长点,但远比并发标记的时间短。
CMS是一款优秀的收集器,主要优点:并发收集、低停顿。
缺点:
1)CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
2)CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。
浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现的标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”。
3)CMS是一款“标记--清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
G1收集器
G1是一款面向服务端应用的垃圾收集器。G1具备如下特点:
1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。
3、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
4、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,
5、G1运作步骤:
1、初始标记;2、并发标记;3、最终标记;4、筛选回收
上面几个步骤的运作过程和CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但是耗时很短,并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remenbered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这一阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
5、标记清除和标记整理算法的理解以及优缺点。
1)标记-清除算法:
先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
缺点:回收了被标记的对象后,由于未经过整理,所以导致很多内存碎片
图解:绿色是被标记为可回收的,当回收后,未使用的内存空间非常零碎,产生内存碎片
此图参考csdn的一篇博客
2)标记整理
标记整理算法的“标记”过程和标记-清除算法一致,只是后面并不是直接对可回收对象进行整理,而是让所有存活的对象都向一段移动,然后直接清理掉端边界意外的内存。
图解:由于标记后继续整理,可以很明显的看出未使用的地址空间都是连续的,不会产生内存碎片。
此图参考csdn的一篇博客
6、eden survivor区的比例,为什么是这个比例,eden survivor的工作过程。
1) eden surivor区的比例:
1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,
2) 为什是8:1的比例?
轻代中的对象基本都是朝生夕死的(80%以上),20%是在年老代
3) 工作流程
一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
7、JVM如何判断一个对象是否该被GC,可以视为root的都有哪几种类型。
常说的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象。
1)程序无法再引用到该对象,那么这个对象就肯定可以被回收,这个状态称为不可达。当对象不可达,该对象就可以作为回收对象被垃圾回收器回收。而判断该状态是否可达,是通过GC roots,也就是跟对象。如果从一个对象没有到达根对象的路径,或者说从根对象开始无法引用到该对象,该对象就是不可达的。
2)1.虚拟机栈(JVM stack)中引用的对象(准确的说是虚拟机栈中的栈(frames))
每个方法的执行,jvm都会创建相对应的栈帧,其包含对象的引用。方法执行完毕,栈帧弹出,引用不复存在。也就是没有gc root指向临时对象。
2.方法区中类静态属性引用的对象
Class存在,静态属性引用的对象存在。Class不存在,它也就不存在。
3.本地方法栈(Native Stack)引用的对象
8、强软弱虚引用的区别以及GC对他们执行怎样的操作
强引用(Strong) 就是我们平时使用的方式 A a = new A(),一般new出来的对象,其用一个变量包含其对象的引用,垃圾收集器(GC)是不会回收一个被强引用引用到的对象,即使内存不足报OutOfMemoryError。
弱引用(Weak) :只要GC扫描到它,弱引用的对象是一定会被回收的,不管内存充足还是紧张。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用的对象被垃圾回收的话,Java虚拟机就会把这个弱引用加入相关的引用队列中,以下为相关代码。
String abc = new String("abcde");
WeakReference<String> wf= new WeakReference<String>(str, rq);
补充:软引用:GC扫描到,如果内存充足,不回收,不充足,回收。
9、Java类加载的过程。
1.一个Java文件从编码完成到最终执行,一般主要包括两个过程:编译和运行
编译是指将写好的java文件用javac编译成字节码,即.class文件。
运行即将.class文件交给JVM执行。而类加载就是JVM将.class文件类信息加载到内存并解析成对应的class类对象过程
类加载:
其过程分为:加载、链接、初始化(链接分为:验证、准备和解析过程)
其中:
加载:把class字节码文件从各个来源通过类加载器装载入内存中。来源包含字节码来源和类加载器(由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载)
验证:为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误
准备:主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
解析:将常量池内的符号引用替换为直接引用的过程
初始化:这个阶段主要是对类变量初始化,是执行类构造器的过程。