接上一篇文章:https://segmentfault.com/a/11...通过字节码,我们了解class文件的结构。通过运行数据区,我们了解了jvm内部的内存划分和结构接下来,让我们看看字节码是如何进入jvm的内存空间的,每一个进入那个空间,又是如何运行的。4.1加载4.1.1概述类的加载就是将类文件中的二进制数据读入内存,然后将字节流表示的静态数据结构转换成运行在方法区的数据结构,并存储起来在堆内存中生成一个java.lang.Class对象作为访问方法区数据结构的入口。注意:加载字节码的来源不一定非得是class文件,可以是任何符合字节码规范的地方,甚至是二进制流等。从字节码到内存,由加载器(ClassLoader)完成).下面我们详细看一下加载器相关的内容4.1.2系统加载器jvm提供了3个系统加载器,分别是Bootstrp加载器、ExtClassLoader和AppClassLoader。写的,是在Java虚拟机启动后初始化的。它主要负责加载以下路径下的文件:%JAVA_HOME%/jre/lib/*.jar%JAVA_HOME%/jre/classes/*-Xbootclasspath参数指定System.out的路径。println(System.getProperty("sun.boot.class.path"));2)ExtClassLoaderExtClassLoader是Java写的,具体是sun.misc.Launcher$ExtClassLoaderExtClassLoader主要加载的是:%JAVA_HOME%/jre/lib/ext/*ext下的所有classes目录都在java.ext.dirs指定的类库中systemvariableSystem.getProperty("java.ext.dirs")3)AppClassLoaderAppClassLoader也是Java写的,它的实现类是sun.misc.Launcher$AppClassLoader,我们知道ClassLoader中有一个getSystemClassLoader方法,它是这个方法返回的。负责加载-classpath指定的类或jar文件,也是Java程序默认的类加载器System.getProperty("java.class.path")4)验证很简单,只需要一段代码打印对应属性信息查找当前三个类加载器包com.iheima.jvm.load加载的目录;publicclassSystemLoader{publicstaticvoidmain(String[]args){String[]bootstrap=System.getProperty("sun.boot.class.path").split(":");String[]ext=System.getProperty("java.ext.dirs").split(":");String[]app=System.getProperty("java.class.path").split(":");System.out.println("引导程序:");for(Strings:bootstrap){System.out.println(s);}System.out.println();System.out.println("分机:");for(Strings:ext){System.out.println(s);}System.out.println();//app是默认加载器,注意启动控制platform-classpath选项System.out.println("app:");对于(字符串s:应用程序){System.out.println(s);}}}4.1.3自定义加载器除了上面系统提供的三种加载器,jvm还允许你自己定义类加载器,典型的是在tomcat上:扩展:有兴趣的同学也可以自己写,继承抽象类ClassLoader,并覆盖相应的findClass方法。接下来看一个重点:双亲委托4.1.4双亲委托1)概述一个类加载器加载一个类时,因为有多个Loader甚至可以有各种自定义。它们是父子继承关系,给人的印象是子类的加载会覆盖父类,其实恰恰相反!不同于普通的类继承属性,类加载器会优先加载父类的加载方法。如果父类可以加载,就直接使用父类。否则最后一步尝试自己加载,可以从源码中验证。ClassLoader.loadClass()method:protectedClass>loadClass(Stringname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){//首先检查Class是否已经加载>c=findLoadedClass(name);if(c==null){//如果没有加载,按照以下规则开始执行:longt0=System.nanoTime();try{if(parent!=null){//重要!如果父加载器不为空,则调用父加载器的loadClassc=parent.loadClass(name,false);}else{//如果父加载器为空,调用BootstrapClassloaderc=findBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){}if(c==null){longt1=System.nanoTime();//如果没有找到父加载器,则调用findclass自行查找并加载c=findClass(name);}}如果(解决){resolveClass(c);}返回c;}}2)为什么要这样设计?避免重复加载和核心类篡改。使用双亲委派模型的优点是Java类与其类加载器具有优先级的层次关系。通过这种层次关系,可以避免类的重复加载。当父类已经加载完后,子类加载器就不需要再加载了。其次,考虑到安全因素,不会随意替换javacoreapi中定义的类型。假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托方式传递给启动类加载器,启动类加载器在coreJava中。API找到这个名字的类,发现这个类已经被加载,不会重新加载网络传过来的java.lang.Integer,而是直接返回加载的Integer.class即使父类没有加载,它会优先让父类加载特定系统目录下的类,得到的还是jvm中的核心类,而不是随便重写。这样可以防止核心API库被随意篡改。4.2验证加载完成后,类中定义的类结构进入内存的方法区。接下来,身份验证是连接阶段的第一步。其实验证和上面的加载是交互的(比如class文件格式验证)。之所以将校验放在加载之后,是因为除了基本的类文件格式外,还需要进行很多其他的校验。我们一一来看:4.2.1文件格式校验这个很容易理解,就是校验加载的字节码是否正确。是否符合规范?CAFEBABYE是否以主版本号和次版本号开头?常量池类型是否正确?是否还有其他无法识别的信息?总之,根据我们上一节讲到的字节码分析,我们需要满足合法的字节码约束4.2.2元数据校验到java语法级别。该阶段主要验证属性、字段、类关系、方法等是否合规。有父类吗?除了Object,其他类肯定有不应该继承的继承类。例如,final是抽象类吗?如果是,是不是所有方法都没有字段有问题?是不是重写了父类中的final……等等。总之,经过这个阶段,你的类对象结构就ok了。4.2.3字节码验证最复杂的阶段。等等,之前不是验证过字节码吗?为什么需要验证?上面的验证是一个基本的字节表格式验证。而这里主要是验证类中定义的方法,看方法里面的代码是否合法。类型转换有问题吗?指令是否跳转到方法外的字节码?...经过这个阶段,你就可以保证当你的代码被执行时,不会发生重大事故!一点也不。比如你写了一段代码,jvm在执行的时候只会知道你的方法是符合系统规则的。也不知道你会不会执行太久导致系统死机4.2.4符号引用校验的最后阶段。这个阶段也很容易理解。我们在解读上面的字节码的时候就知道,有些字节码是直接引用的,有些是指向其他字节码地址的。符号引用的校验就是这些引用对应的内容是否合法。utf8记录了某个类的名字,这个类存在吗?方法或字段引用,这些方法是否存在于相应的类中?类、字段、方法等的可见性是否合法……4.3准备工作这个阶段为类中定义的各种类变量分配内存并赋初值。你做的很容易理解,但要注意几点:4.3.1变量类型注意是类变量,即类中的静态变量,不是new的实例变量。下面new的初始化阶段,类变量=静态变量,实例变量=实例化new创建的那些4.3.2存储位置理论上这些值都在方法区,但是注意方法区本身是一个逻辑概念。在1.6,永久代1.8之后,静态类变量如果是对象,其实是在堆中。上面讲方法区的时候验证了这一点。4.3.3初始化值这个值已经进入内存,那么内存中存储的值是多少呢?注意!即使是静态变量,在这个阶段初始化到内存中的还是它的初始值!而不是成为你想要的。看下面两个例子://普通类变量:在准备阶段为它开辟了内存空间,但它的值是int的初始值,即0!//真正的123赋值是在类构造函数中,也就是后面的初始化阶段publicstaticinta=123;//最终修饰的类变量,编译成字节码后,是一个ConstantValue类型//这个类型,在准备中阶段,直接赋值123,后期没有二次初始化,即publicstaticfinalintb=123;4.4分析分析阶段开始分析类之间的关系,加载需要关联的类。这涉及到:类或接口分析:类相关的父子继承,实现的接口有哪些类型?字段解析:字段对应什么类型?方法解析:方法参数、返回值、关联了哪些类型接口方法解析:接口上的类型?解析完成后,就完成了当前类中方法字段父子继承等对象级关系的解析。与这些操作相关的类信息也被加载。4.4初始化4.4.1概述最后一步,在这一步之后,class信息被完整的打入jvm内存,直到被垃圾回收器回收。前面的阶段由虚拟机处理。我们无能为力,只能从代码上按照它的语法要求去做。而这个阶段就是assignment,也就是我们application里面写的占主导地位的地方。在准备阶段,jvm已经初始化了相应的内存空间,final也有自己的值。但是其他类变量在这里赋值。这就是我们所说的:publicstaticinta=123;这行代码中的123实际上是赋值的。4.4.2两次初始化1)类变量和实例变量的区别注意一件事!这里所说的初始化是一个类class加载到内存的过程,所谓初始化值就是类中定义的类变量。那就是静态变量。这种初始化应该与新类区分开来。new是一个实例变量,它是在执行阶段创建的。2)创建实例变量的过程当我们在方法中写了一段代码,在执行过程中,当我们要创建一个新的类时,会发生以下事情:在方法中找到对应类型的类信息area并在当前方法栈帧本地找到在变量表中放一个引用指针在堆中开辟一块空间,放上这个对象的实例并将指针指向该对象在堆中的地址,就可以了完毕!本文由传智教育博学谷教研组发布。如果本文对您有帮助,请关注并点赞;有什么建议也可以留言或私信。您的支持是我坚持创作的动力。转载请注明出处!
