潍坊市论坛

首页 » 分类 » 问答 » 详解一个java文件的执行过程
TUhjnbcbe - 2021/5/22 23:49:00
有了白癜风怎么办 http://m.39.net/pf/a_4591211.html

由于
   2、防止同一个类被重复加载。4、可见性:子类加载器可以访问父类加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没办法利用类加载器去实现容器的逻辑。解释器和即时编译器(JIT)主要用于将Class数据文件编译成对应的本地机器码执行。解释器传统的编译工具,主要分为字节码解释器和模版解释器。字节码解释器是在执行时通过纯软件代码模拟字节码的执行,效率低下;模版解释器则是主流使用的解释器,原理是将每一条字节码和一个模版函数关联,在Class字节码转成机器码的过程中会通过对应的模版函数生成对应的机器码,这样短期来看效率还不错,但是一旦同一个的字节码被多次执行,那么每次都需要通过模版函数生成机器码,效率十分低下。即时编译器(JIT)JIT的原理是将字节码关联的模版数据直接转成机器码,然后将机器码缓存起来,后面如果再次执行这个字节码时就直接返回缓存中的机器码,省去了二次执行的时间,缺点是第一次的转换消耗比较长,所以以单次执行来看,JIT的效率是不如解释器的,但是一旦执行的字节码重复数多,JIT的作用就体现出来了。HotSpot中有两个JIT编译器,分别是ClientCompiler和ServerCompiler,但大多数情况下我们简称为C1编译器和C2编译器。C1进行简单的优化,耗时短。C2进行耗时长的优化,代码执行效率更高。实际中C1和C2共同协作执行的。实际过程当虚拟机启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且伴随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为有价值的本地机器指令,以换取更高的程序执行效率。热点代码探测的方式1、规定热点阀值。每次方法调用时该方法的调用次数都会+1,当调用次数达到阀值,就会触发JIT编译。热点阀值可通过-XX:CompileThreshold=来设定。如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍不足以让它提交给JIT编译,那这个方法的调用计数器就会减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期。可以使用-XX:-UseCounterDecay来关闭热度衰减,也可以使用-XX:CounterHalfLifeTime设置半衰周期的时间。2、回边计数器。统计一个方法中循环体代码执行的次数。在字节码中遇到控制流向后跳转的指令称为“回边”。显然,建立回边计数器统计的目的是为了触发OSR编译(JIT编译)。运行时数据区域程序计数器线程私有,是当前线程执行的字节码指示器,指示字节码的执行顺序。程序计数器的内存是单独的,不会受到其他变量、对象的影响。所以它不会发生内存溢出。也是JVM唯一一个没有规定任何OOM的区域。也不存在GC。Java虚拟机栈线程私有。先进后出,是代码执行的核心位置,一个方法在执行前会生成这个方法对应的栈帧,栈帧包括局部变量表(保存局部变量)、操作数栈(进行局部变量的操作)、动态链接(其他对象、方法的引用)、方法返回值以及一些附加信息。然后进行压栈操作,开始方法的执行,如果此方法中调用了其他方法,那么会将调用的这个方法对应的栈帧压入栈,等到这个方法执行完之后,如果方法包含返回值,将这个返回值返回给上一个方法,然后这个被调用的栈帧出栈,随后继续执行上一个栈帧。局部变量表基本存储单元是slot(变量槽),用于存储各种类型的数据,其中long和double会占用两个slot,其他基本数据类型以及对象引用变量占用一个slot。这也说明了为什么类方法不能使用this而实例方法可以(实例方法会直接在索引为0的位置创建一个this参数保存,所以在实例方法中使用this就是直接使用这个参数的)同时局部变量表的槽位是可以重用的,当前一个局部变量失效后,下一个变量使用空出来的位置。上面这个方法是实例方法,包含this,应该有四个index槽位,但是因为b是在括号里作用的,出了括号就失效了,所以它的位置(index=3的位置)被新设置的c所占用。操作数栈先进后出结构,是当前方法执行的位置,在方法执行时,会根据编译生成的字节码按顺序将要操作的数据从局部变量表中进入入栈,栈中的数据只能从栈顶向下操作,不能跨数据。比如代码x=x+1,在执行时会将x先压入栈,然后将1压入栈,然后读取到+的指令,将栈顶的两个数相加,再将加的结果存入局部变量表x的位置。如果调用了其他方法并获取了返回值,那么在调用方法执行完毕后,该方法的返回值会被压入栈顶,然后再进行后续的操作。栈顶缓存技术目前只是一种想法,还未实现。因为使用的是栈式架构,所以指令多,又由于操作数是存储在内存中的,所以频繁地读写必然会影响执行速度,所以提出将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。虚方法表因为重写方法都是虚方法,这些方法在编译时期都需要往上寻找直到找到所执行对象的实际类型,然后进行权限验证。这个寻找的过程是比较耗时的,所以每个类会在方法区创建一个虚方法表来保存这些虚方法的实际入口。方法返回值1、正常返回:(boolean、byte、char、short、int)ireturn;lreturn、freturn、dreturn、areturn(String);return(无返回值)。2、异常返回:如果发生异常的方法没有捕获异常而是抛给上一级,那么该异常就会被返回给调用该方法的方法去处理。Java堆线程共享。是Java虚拟机内存最大的一块,主要用于存储创建的对象。根据对象的寿命、大小等因素将对象存储区域划分分为新生代、老年代。在1.7开始引入了字符串常量池。因为对象的创建销毁是非常频繁的,所以堆是JVM中的核心位置之一,也是OOM发生的主要位置之一。本地方法栈与native(本地)方法本地方法栈(也就是最上面图中的本地接口)是JVM与底层交互的接口,用于调用native方法。作用与Java虚拟栈差不多,只不过是为native方法服务的,是由非Java语言编写的。方法区和堆一样是线程共享,用于存储已被虚拟机加载的类型信息、常量、静态变量、即使编译器编译后的代码缓存等。方法区的实现在1.8之前是永久代,使用的是JVM的内存,在1.8开始实现变成元空间,使用的是本地内存。之所以这样改变,是因为原来的方法区很容易发生OOM,因为方法区的类信息被回收的条件非常苛刻,必须满足以下三点:1、该类的所有对象都被回收;2、加载该类的类加载器被回收;3、该类对应的Class对象没有在任何地方被引用(无法在任何地方通过反射访问该类的方法)。关于第三点的Class对象,在一个类被加载时,会在堆中创建一个用于用于访问这个类的类信息Class对象。而在成为元空间后,使用的是本地内存,所以方法区发生OOM的情况会极大改善。运行时常量池当Class文件被类加载器加载到JVM中时,存储的位置就是在方法区,而在Class文件信息中包括着class文件的常量池,当JVM开始执行时,就会将文件常量池中的数据加载到方法区内部的运行时常量池,变成运行时状态,并将符号引用转成直接引用。符号引用和直接引用:当在调用中调用某个类的类方法、类属性、接口方法、接口属性时,因为在执行前,对应的类、接口都还在Class文件常量池中,没有加载到内存中,所以不能确定这些类、接口加载后的具体位置,这时就需要一种方式来确认位置,通常使用类的全名+属性名/方法名来唯一标识要调用的方法/属性,这种标识就是符号引用,等到对应的类加载到内存后,再将这些唯一标识改成在内存中的位置,这种就是直接引用。字符串常量池在JDK1.7开始,字符串常量池就由方法区移入了堆中,字符串常量池是专门存放字符串常量的,至于为什么移入堆中,这是因为字符串的创建和对象一样频繁,销毁也就变得尤其频繁,而方法区的GC是伴随着fullgc的,因为fullgc会造成STW,在fullgc期间其他程序都会停止,所以都会避免fullgc,而字符串常量池放在方法区中就减少了字符串被回收的频率,提高了OOM的概率。类加载过程在Class数据文件被类加载器加载到JVM中到编译执行,中间经历加载、链接、初始化、使用、卸载,其中链接又分为验证、准备、解析。需要注意的是:这些操作阶段不一定要等上个阶段完成后才能进行下一个阶段,解析操作往往在初始化之后再执行。一部分验证和加载同时执行,一部分验证等到解析才会执行。下面就一个个来说明每一步的操作。加载通过类加载器将Class数据文件加载到方法区,并且在堆中创建一个Class对象用于访问方法区的类数据。验证:验证主要用于检验传来的二进制数据格式是否满足加载要求。虽然在java文件的编译阶段编译器已经进行了一次检查,但是JVM是与前面编译器编译的过程隔开的。验证主要包括格式验证、语义验证、字节码验证、符号引用验证。1、格式验证:与加载过程同时进行的。用于检验字节码魔数是否正确、主版本和副版本是否在支持范围内、数据每一项是否有正确的长度等。2、语义验证:校验不同类之间的关系是否正确,例如是否继承了抽象类但没有实现方法,是否继承了final类。3、字节码验证:最复杂的一个验证。从方法层面验证各个操作是否正确,比如是否会跳转到不存在的指令,函数调用是否传递正确类型的值,变量赋值是否给了正确的类型4、符号引用验证:发生在解析操作。将符号验证转化为直接引用时,验证符号引用是否能正确使用。准备为类属性分配内存并设置零值(这里不包括使用staticfinal修饰的属性且赋值的值是一个字符串常量或一个基本数据类型常量或其他不触发方法的情况(也就是过程不会涉及构造器或者其他方法),因为字符串或者基本数据是常量,在编译时期就会分配地址,准备阶段直接就会显式初始化,而如果赋的值包括方法调用就需要在client方法里执行)。如果属性值是常量,那么常量值就会在方法区中分配内存,而如果是对象,那么对象则会在堆中创建;并且实例属性参数也会跟随对象的创建在堆中,只有静态属性和对应的常量值在方法区中分配内存。而设置的零值是当前类型的默认值,比如privateinta=2;那么设的零值就是0,a=2是在后面的client方法中执行的。解析将符号引用转成直接直接引用。符号引用主要包括类或接口、静态属性、类方法和接口方法这四种(都是类数据,在类加载后就能获取的)。初始化执行静态代码块方法以及静态属性的赋值。会将类中所有的关于类属性的赋值语句以及静态代码块中的语句收集起来集中进clinet方法中,然后执行。执行的顺序就是按赋值以及静态代码块的排列顺序执行。虚拟机在在执行client方法时会加锁,使得此方法只会被一个线程加载,所以我们需要考虑类在加载时会不会发生异常死循环导致此类无法被加载使用使用不必多说,就是调用类属性、方法。卸载上面说过一个类卸载所需要的条件:1、该类的所有对象都被回收;2、加载该类的类加载器被回收;3、该类对应的Class对象没有在任何地方被引用(无法在任何地方通过反射访问该类的方法)。那么具体原因是什么?我们知道,对象被回收的条件是这个对象没有被引用,类也是如此,在类被加载到内存后,它会在堆中创建一个Class对象,并且和加载它的加载器互相关联,也就是图中的MyClassLoader,而这个对象也和类对应的实例对象所关联,这种关联是无法切断的,而如果对应的三种变量都没有再引用,那么就相当于这个类信息没有被引用,那么也就可以被回收了。类被加载的场景Java对类的使用方式分为主动使用和被动使用。主动使用会触发类的初始化,被动使用不会(但是还是会触发初始化之前的操作)。主动使用的场景1、创建某个类的对象2、调用某个类的类属性、类方法3、获取某个类的反射对象4、初始化子类,如果父类没有初始化,会先触发父类的初始化(不适用接口)5、如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。6、虚拟机启动,调用主方法的类会被初始化7、初次调用MethodHanlder实例时,初始化该MethodHanlder指向的方法所在的类。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄所在的类)被动使用的场景1、访问的类属性不是当前类的属性,比如从父类继承而来的或者实现接口得到的,比如

publicclassInitTest{publicstaticvoidmain(String[]args){inta=son.a;}}classparent{publicstaticinta=0;static{System.out.println("12");}}classsonextendsparent{publicstaticintb=0;static{System.out.println("1ss2");}}这里只会触发parent的初始化,而不会触发son类的初始化,而如果son重写了属性a或者调用的是son的另一个属性b,那么就会触发son类的初始化,并且因为son继承了parent类,所以在son初始化前还会先初始化parent。2、通过数组定义类引用,不会触发此类的初始化(如果数组类型是基本数据类型,那么不需要加载;如果是引用数据类型,那么就进行类的加载,但不会进行初始化操作)3、调用staticfinal修饰的且是常量或者是字符串或是其他没有方法触发的情况,也不会触发初始化操作。4、调用ClassLoader的loadClass()方法加载一个类,只会触发加载操作,而不会触发初始化操作。类加载器的拓展类的唯一性每个类加载器都有其自己的命名空间,命名空间由该加载器及其所有的父加载器所加载的类组成。在同一个命名空间中,不会出现类的完整名字(包名+类名)相同的两个类。但在不同的命名空间中,有可能出现完整名字相同的两个类。所以,在比较两个类是否是同一个类的前提是这两个类由同一个类加载器加载,如果这两个类是由两个类加载器加载的,那么这两个类必然不是同一个类。一个类只能被一个类加载器加载一次,但是可以被多个类加载器加载。类加载器的主要方法1、getParent()返回该类加载器的父类加载器。2、loadClass(name)加载name类,如果找不到该类,就抛出异常。内部的实现是父类委托机制。3、findClass(name)查找二进制的name类,返回该类的实例,这个类是loadClass内部调用的一个方法,JDK维护了一个推荐的重写方法,鼓励我们去重写这个方法来实现对功能的拓展。JDK1.2之前还未引入父类委托机制,所以要拓展就需要去重写loadClass方法,1.2引入父类委托机制后通过重写findClass方法来拓展,并且也没有破坏父类委托机制。4、defineClass(Stringname,byte[]b,intoff,intlen)将字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度。其中b是ClassLoader从外部获取的。这是受保护的方法,只有在自定义的ClassLoader子类中使用。一般在findClass方法中被调用,在findClass方法中先类的字节码数组,然后调用defineClass获取类实例返回。ClassLoader一些实现类的继承关系SecureClassLoader扩展了ClassLoader,增加一些方法,但是一般我们使用的是其子类URLClassLoader,URLClassLoader实现了ClassLoader很多抽象方法,如findClass()、findResource()。我们在编写自定义类加载器时,如果没有特别复杂的实现,可以直接继承URLClassLoader,这样可以避免自己编写findClass以及获取字节流的方式,使自定义类加载更加简洁。而拓展类加载器与系统类加载器也是继承URLClassLoader。Class.forName与ClassLoader.loadClass的区别ClassLoader.loadClass是一个实例方法,该方法将Class文件加载到内存中后,只会执行类加载过程的加载、验证、准备、解析。初始化等到类的第一次使用时才会执行。Class.forName是静态方法,该方法在将Class文件加载到内存的同时,还会执行类的初始化。破坏双亲委派机制的三次场景1、由于双亲委派机制是在JDK1.2之后才引入的,而在Java的第一个版本就有类加载器的概念以及抽象类ClassLoader,所以此时是没有双亲委派机制的,用户自定义类加载器就是直接重写loadClass方法,这也就是破坏了双亲委托机制。2、第二次是为了弥补双亲委托机制的缺陷,因为双亲委托机制使得父类加载器无法使用子类加载器的类资源,这样对于父类需要调用子类加载器加载的类资源时就无法实现。为了解决这个问题,引入了线程上下文类加载器(默认为系统类加载器),当需要调用系统类加载器就可以使用这个属性进行加载。3、IBM公司设计的代码热部署,使得传统简单的树状继承关系,改成了更为复杂的网状结构,让每个模块都有自己自定义的类加载器。自定义类加载器好处1、隔离加载类,创建多个模块空间,确保相互间加载的类不会冲突。2、修改类加载的方式。某些非必要导入的类可以自定义类加载器在某个事件按需导入。3、扩展加载器,加载不同位置位置的资源。4、防止源码外泄。在编译时加密。注意1、因为同一个类被两个类加载器加载会生成不同的类对象,所以如果两个继承关系的类被两个类加载器加载,那么强制转换类型会报错。所以使用自定义类加载器需要结合场景,不能一味使用。2、实现时推荐重写findClass方法,不破坏双亲委托机制。沙箱安全机制Java沙箱是将Java代码限定在JVM特定的运行范围中,并且严格限制代码对本地系统资源的访问。防止对本地系统造成破坏。演变1、JDK1.0时期将执行的Java代码分为本地和远程两种,本地代码默认视为可信赖的,而远程代码则看作不受信赖的。对于信赖的代码,可以访问一切本地资源。而不受信赖的代码,则会受到沙箱的限制,不能访问本地资源。2、JDK1.1时期由于1.0中对远程代码限制太过激进,导致一些需要访问本地资源的远程代码无法访问,极大影响了程序的可用性,所以在1.1中进行了优化,在前者基础上,增加了安全策略。允许用户指定代码对本地资源的访问权限。3、JDK1.2时期1.1中无法解决的是本地代码权限问题,因为本地都是可以访问本地资源的,所以在1.2中又引入了代码签名。无论是本地代码还是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。4、JDK1.6时期也是当前最新的安全策略,相比于前代引入了域的概念。主要升级是将资源的访问进一步划分。虚拟机会把所有代码加载到系统域或应用域中。系统域是与关键资源交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。JDK9的新特性1、扩展类加载器改名为平台类加载器(platformclassloader)。可以通过ClassLoader的新方法getPlatformClassLoader()来获取。2、原来的rt.jar(启动类加载器加载的核心源码)和tool.jar(Java程序启动所需的class目录下的类)被拆分成数十个JMOD文件,Java类库也被改成可扩展的模式,所以拓展目录也就无需存在了。3、平台类加载器和应用程序类加载器不再继承URLClassLoader。现在三大加载器全部继承于jdk.internal.loader.BuiltinClassLoader4、类加载器拥有了name属性,可以通过getName()获取,平台类加载器的name是platform。应用类加载器的名称是app。类加载器的名称在调试与类加载器相关的问题时会非常有用。5、启动类加载器现在是jvm内部和java类库共同协作的实现的(c++和java,过去只是c++),但是为了与之前的代码兼容,在获取启动类加载器的场景中仍然为null。6、委派机制变化。在加载器受到加载请求后,会先判断该类是否属于某个系统模块,如果属于直接将这个请求发给这个模块的类加载器。

往期推荐:

这个高仿小米商城项目,拿来学习再好不过了!

SpringBoot库存管理系统,拿来学习真香

代码中大量的if/else,你有什么优化方案?

IDEA快捷键终极大全,速收藏

再见了!公司的烂系统~网友:好想给大神当小弟...

预览时标签不可点收录于话题#个上一篇下一篇
1
查看完整版本: 详解一个java文件的执行过程