JVM详解.docx

上传人:b****0 文档编号:12581886 上传时间:2023-04-20 格式:DOCX 页数:60 大小:541.39KB
下载 相关 举报
JVM详解.docx_第1页
第1页 / 共60页
JVM详解.docx_第2页
第2页 / 共60页
JVM详解.docx_第3页
第3页 / 共60页
JVM详解.docx_第4页
第4页 / 共60页
JVM详解.docx_第5页
第5页 / 共60页
点击查看更多>>
下载资源
资源描述

JVM详解.docx

《JVM详解.docx》由会员分享,可在线阅读,更多相关《JVM详解.docx(60页珍藏版)》请在冰豆网上搜索。

JVM详解.docx

JVM详解

JVM详解

Java程序运行在JVM之上,JVM的运行状况对于Java程序而言会产生很大的影响,因此掌握JVM中的关键机制对于编写稳定、高性能的JAVA程序至关重要。

JVM制定了Java类的加载、编译、执行、对象内存的分配和回收、线程以及锁机制,这些机制对Java程序的运行效果起到了重要的影响,当然,JVM涉及的不仅仅是上面这些机制,但在本章节中并不打算介绍所有JVM的机制,而是仅仅深入介绍其中的一些关键机制。

1JVM类加载机制

JVM类加载机制是指将jar或目录中的.class文件加载到JVM,并形成Class对象的机制,应用可对Class对象进行实例化从而进行调用,JVM的类加载机制是动态的,因此可在运行时动态的加载外部的类、远程网络下载过来的字节码等,除了这个动态化的优点外,也可通过JVM的类加载机制来达到合理的类隔离的效果,例如ApplicationServer中要避免两个应用的类互相干扰。

JVM将整个类加载过程划分为了三个步骤:

装载、链接和初始化,其中初始化过程不是必须的,装载和链接过程完成后即以完成将二进制的字节码转为了Class对象,初始化只是将Class对象实例化,从而得到Object的过程,过程图示如下:

●装载

装载过程负责找到二进制字节码并加载至JVM中,JVM通过类名、类所在的包名通过ClassLoader来完成类的加载,同样,也采用以上三个元素来标识一个被加载了的类:

类名+包名+ClassLoader实例ID,类名为如下格式:

对于接口或非数组型的类,其名称即为类名,此种类型的类由所在的ClassLoader负责加载;

对于数组型的类,其名称为”[”+(基本类型或L+引用类型类名),例如byte[]bytes=newbyte[512],这个bytes的类名为:

[B;Object[]objects=newObject[10],objects的类名则为:

[Ljava.lang.Object;,数组型的类其中的元素由所在的ClassLoader负责加载,但数组类则由JVM直接创建。

●链接

链接过程负责对二进制字节码的格式进行校验、初始化装载类中的静态变量以及解析类中调用的接口、类。

二进制字节码的格式校验遵循JavaClassFileFormat(具体请参见JVM规范)规范,如格式不符合则抛出VerifyError,校验过程中如碰到需要引用到其他的接口和类,也会进行加载,如加载过程失败,则会抛出NoClassDefFoundError。

在完成了校验后,JVM初始化类中的静态变量,并将其值赋为默认值。

最后一步为对类中的所有属性、方法进行验证,以确保其需要调用的属性、方法存在,以及具备相应的权限(例如public、private域权限等),这个阶段失败可能会造成NoSuchMethodError、NoSuchFieldError等错误信息。

●初始化

初始化过程即为执行类中的静态初始化代码、构造器代码以及静态属性的初始化,在四种情况下初始化过程会被触发执行:

调用了new;

反射调用了类中的方法;

子类调用了初始化;

JVM启动过程中指定的初始化类。

在执行初始化过程之前,必须首先完成链接过程中的校验和准备阶段,解析阶段则不强制。

JVM的类加载通过ClassLoader以及其子类来完成,分为了BootstrapClassLoader、ExtensionClassLoader、SystemClassLoader以及User-DefinedClassLoader,这四种ClassLoader的关系图示如下:

●BootstrapClassLoader

这是JVM的根ClassLoader,它是用C++实现的,JVM启动时初始化此ClassLoader,并由此ClassLoader完成$JAVA_HOME中jre/lib/rt.jar(SunJDK的实现)中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现。

●ExtensionClassLoader

JVM用此classloader来加载扩展功能的一些jar包,例如SunJDK中这个目录下有dns工具jar包等,在SunJDK中此ClassLoader对应的类名为ExtClassLoader。

●SystemClassLoader

JVM用此classloader来加载启动参数中指定的Classpath中的jar包以及目录,在SunJDK中此ClassLoader对应的类名为AppClassLoader。

●User-DefinedClassLoader

User-DefinedClassLoader是Java开发人员继承ClassLoader抽象类自行实现的ClassLoader,基于自定义的ClassLoader可用于加载非Classpath中(例如从网络上下载的jar或二进制)的jar以及目录、还可以在加载之前对class文件做一些动作,例如解密等。

JVM的ClassLoader采用的为树形结构,除了BootstrapClassLoader外,其他的ClassLoader都会有parentClassLoader,User-DefinedClassLoader的默认parentClassLoader为SystemClassLoader,在加载类时也必须按照树形结构的原则来进行加载,也就是说首先应从parentClassLoader中尝试进行加载,当parent中无法加载时,应再尝试从SystemClassLoader中进行加载,SystemClassLoader同样遵循此原则,在找不到的情况下会自动从其parentClassLoader中进行加载,值得注意的是,由于JVM是采用类名加上Classloader的实例来作为Class加载的判断的,因此在加载的时候不采用上面的顺序也是可以的,例如在加载时不去parentClassLoader中寻找,而是只在当前的ClassLoader中寻找,但会造成的现象是树上多个不同的ClassLoader中都加载了某Class,并且这些Class的实例对象都不相同,JVM会保证同一个ClassLoader实例对象中只能加载一次同样名称的Class,,这有些时候是满足需求的,例如隔离的需求,但有些时候可能会带来困惑,例如ClassCastException,因此在加载类的顺序上要根据需求合理把握,尽量还是要保证从根到最下层的ClassLoader上的Class是只加载了一次的。

ClassLoader抽象类提供了几个关键的方法:

●loadClass

此方法负责加载指定名字的类,ClassLoader的实现方法为先从已经加载的类中寻找,如没有则继续从parentClassLoader中寻找,如仍然没找到,则从SystemClassLoader中寻找,最后再调用findClass方法来寻找,如要改变类的加载顺序,则可覆盖此方法,如加载顺序相同,则可通过覆盖findClass来做特殊的处理,例如解密、固定路径寻找等,当通过整个寻找类的过程仍然未获取到Class对象时,则抛出ClassNotFoundException。

如类需要resolve,则调用resolveClass进行链接。

●findLoadedClass

此方法负责从当前ClassLoader实例对象的缓存中寻找已加载的类,调用的为native的方法。

●findClass

此方法直接抛出ClassNotFoundException,因此需要通过覆盖loadClass或此方法来以自定义的方式加载相应的类。

●findSystemClass

此方法负责从SystemClassLoader中寻找类,如未找到,则继续从BootstrapClassLoader中寻找,如仍然为找到,则返回null。

●defineClass

此方法负责将二进制的字节码转换为Class对象,这个方法对于自定义加载类而言非常重要,如二进制的字节码的格式不符合JVMClass文件的格式,抛出ClassFormatError;如需要生成的类名和二进制字节码中的不同,则抛出NoClassDefFoundError;如需要加载的class是受保护的、采用不同签名的或类名是以java.开头的,则抛出SecurityException;如需加载的class在此ClassLoader中已加载,则抛出LinkageError。

●resolveClass

此方法负责完成Class对象的链接,如已链接过,则会直接返回。

当Java开发人员调用Class.forName来获取一个对应名称的Class对象时,JVM会首先获取到调用Class.forName的对象所在的ClassLoader,并使用此ClassLoader来加载此名称的类,JVM为了保护加载、执行的类的安全,并不允许ClassLoader直接卸载加载了的类,只有等待JVM来卸载,在SunJDK中,只有当ClassLoader对象没有引用时,此ClassLoader对象加载的类才会被卸载。

根据上面的描述,加上实际的应用中,JVM的类加载过程中会抛出这样那样的异常,在这些情况下对于各种异常产生的原因的掌握是最为重要的,下面来看看类加载方面的常见异常:

●ClassNotFoundException

这是最常见的异常,产生这个异常的原因为在当前的ClassLoader中加载类时未找到类文件,对于位于SystemClassLoader的类很容易判断,就是需要加载的类不在Classpath中,对于,对于位于User-DefinedClassLoader的类则麻烦些,需要具体查看下这个ClassLoader加载类的过程,从而判断此ClassLoader需要从什么位置加载到此类。

●NoClassDefFoundError

这个异常较之ClassNotFoundException更难处理一些,造成此异常主要的原因是加载的类中引用到的另外的类不存在,例如需要加载A,然后A中调用了B,B不存在或当前ClassLoader没法加载B,就会抛出这个异常。

因此对于这个异常,需要先查看是加载哪个类时候报出的,然后需要确认这个类中引用的类是否存在于当前ClassLoader能加载到的位置。

●LinkageError

这个异常在自定义ClassLoader的情况下更容易出现,主要原因是此类已经在ClassLoader加载过了,重复的加载会造成这个异常,要注意避免在并发的情况下出现这样的问题。

由于JVM的这个保护机制,使得在JVM中是没办法直接更新一个已经load了的Class的,只能是创建一个新的ClassLoader来加载更新了的Class,然后将新的请求转入到这个ClassLoader中来获取类,这也是JVM中不好实现动态更新的原因之一,而其他更多的原因是对象状态的复制、依赖的设置等等。

●ClassCastException

这个异常有很多种原因,在JDK5支持泛型后,相对少了一些,在这些原因中比较难查的一种是两个A对象是由不同的ClassLoader加载的情况,这个时候如果将其中的某个A对象造型成另外一个A对象,也会报出ClassCastException。

2JVM执行机制

JVM加载class文件后,已经将class文件中的常量信息、类信息、方法代码等放入方法区中了,但JVM具体是如何调用class中的方法的呢,本章节就具体来讲解下JVM的执行机制。

JVM通过执行引擎来完成字节码的执行,在执行过程中JVM采用的是自己的一套指令系统,每个线程在创建后,都会产生一个程序计数器(pc)和栈(Stack),其中程序计数器中存放了下一条将要执行的指令,Stack中存放StackFrame,表示的为当前正在执行的方法,每个方法的执行都会产生StackFrame,StackFrame中存放了传递给方法的参数、方法内的局部变量以及操作数栈,操作数栈用于存放指令运算的中间结果,指令负责从操作数栈中弹出参与运算的操作数,指令执行完毕后再将计算结果压回到操作数栈,当方法执行完毕后则从Stack中弹出,继续其他方法的执行。

在执行方法时JVM提供了invokestatic、invokevirtual、invokeinterface和invokespecial四种指令来执行,对应的在Java程序中就是如下四种方式,结合实际例子来讲述下JVM的具体执行机制。

●调用类的static方法

代码片段示例如下:

publicclassA{

publicstaticvoidexecute(Stringmessage){

//执行代码

}

}

publicclassB{

publicstaticvoidmain(String[]args){

A.execute(“HelloWorld”);

}

}

编译后B中的代码转变为了如下字节码(通过调用javap–cB查看):

publicstaticvoidmain(java.lang.String[]);

Code:

0:

ldc#16;//StringHelloWorld

2:

invokestatic#18;//Methodsample/A.execute:

(Ljava/lang/String;)V

5:

return

当main线程启动后,JVM为此线程创建一个PC和Stack,执行ldc#16;//StringHelloWorld,并同时将invokestatic放入PC,接着将HelloWorld的引用压入Stack的参数变量中,继续执行invokestatic指令,该指令后跟随的//Method部分的内容称为内容,此内容的规范为:

包名/类名.方法名:

(参数类型)返回值类型,invokestatic指令根据此method-spec找到此Class的方法,根据method-spec中的参数信息找到参数的个数,从stackframe中弹出这些参数,然后创建一个新的stackframe,将参数信息压入,然后重复上述过程,完成整个方法的执行。

●调用对象实例的方法

代码片段示例如下:

publicclassA{

publicvoidexecute(Stringmessage){

//执行

}

}

publicclassB{

publicstaticvoidmain(String[]args){

Aa=newA();

a.execute(“HelloWorld”);

}

}

对于实例的调用,不同点在于调用a时采用的指令为invokevirtual,在执行Aa=newA()时,JVM会负责将a对象的引用压入栈中,当执行invokevirtual指令时,JVM从栈中将a对象的引用弹出,找到a对象引用所属的Class,通过此Class找到符合method-spec中描述的方法,如此Class中没有此方法,则寻找父类,在找到方法后弹出方法所需的参数,创建stackframe,压入参数,继续并完成方法的执行。

在调用实例的方法时,还有一种常用的方式为将属性定义为接口来进行调用,例如:

publicinterfaceIA{

publicvoidexecute(Stringmessage);

}

publicclassAimplementsIA{

publicvoidexecute(Stringmessage){

//执行

}

}

publicclassB{

publicstaticvoidmain(String[]args){

IAa=newA();

a.execute(“HelloWorld”);

}

}

这种方式在JVM的执行中其实和invokevirtual调用是不一样的,一方面是指令改为了采用invokeinterface,改为了包名/接口名.方法名<参数类型>返回值类型;另一方面是调用时寻找方法的方式不一样,在invokevirtual方式时,所调用的对象的方法其在方法表中的位置是固定的,在实现时可直接采用固定偏移的方式来找到方法,而对于接口来说则不行,毕竟每个实现接口的类的方法在方法表中的位置并不一定是一样的,因此接口调用时JVM必须每次都去搜索查找类的方法表,当然,这其实只会造成很小的性能影响,相对于接口编程带来的好处而言,基本可以忽略。

JVM对于初始化对象(Java构造器的方法为:

)以及调用对象实例中的私有方法时,采用的是invokespecial指令,其不同点在于其查找方法时不需要根据运行时的对象引用查找类的方法表的方式,而是直接在编译期就已经指定了,因此相对而言invokespecial指令的执行是快过invokevritual和invokeinterface的。

●反射执行

反射机制是Java的亮点之一,基于反射可动态调用某对象实例中对应的方法、访问查看对象的属性等,而无需在编写代码时就确定需要创建的对象,这使得Java可以实现很灵活的实现对象的调用,例如MVC框架中通常要调用实现类中的execute方法,但框架在编写时是无法知道实现类,在Java中则可以通过反射机制直接去调用应用实现类中的execute方法,代码示例如下:

ClassactionClass=Class.forName(外部实现类);

Methodmethod=actionClass.getMethod(“execute”,null);

Objectaction=actionClass.newInstance();

method.invoke(action,null);

这种方式对于框架之类的代码而言非常的重要,但反射和直接new实例,然后调用方法的最大不同之处在于实创建的过程、方法调用的过程是动态的,这也就使得采用反射生成执行方法调用的代码并不像直接调用实例对象代码样,在编译后就可直接生成对对象方法调用的字节码,而是只能生成调用jvm反射实现的字节码了,各种jvm的反射实现会有些不同,在此来阐述下SunJDK中反射的实现。

要实现动态的调用,最明显的方法就是动态的生成字节码,加载到JVM中并执行,SunJDK也是采用的这种方法,来看看在SunJDK中以上反射代码的关键执行过程。

●ClassactionClass=Class.forName(外部实现类);

调用本地方法,使用调用者所在的ClassLoader来加载创建出Class对象;

●Methodmethod=actionClass.getMethod(“execute”,null);

校验此Class是否为public类型的,以确定类的执行权限,如不是public类型的,则直接抛出SecurityException;

调用privateGetDeclaredMethods来获取到此Class中所有的方法,在privateGetDeclaredMethods对此Class中所有的方法的集合做了缓存,在第一次时会调用本地方法去获取;

扫描方法集合列表中是否有相同方法名以及参数类型的方法,如有则复制生成一个新的Method对象返回;

如没有则继续扫描父类、父接口中是否有此方法,如仍然没找到方法则抛出NoSuchMethodException;

●Objectaction=actionClass.newInstance();

校验此Class是否为public类型,如权限不足则直接抛出SecurityException;

如没有缓存的构造器对象,则调用本地方法获取到构造器,并复制生成一个新的构造器对象,放入缓存,如没有空构造器则抛出InstantiationException;

校验构造器对象的权限;

执行构造器对象的newInstance方法;

构造器对象的newInstance方法判断是否有缓存的ConstructorAccessor对象,如果没有则调用sun.reflect.ReflectionFactory生成新的ConstructorAccessor对象;

sun.reflect.ReflectionFactory判断是否需要调用本地代码,可通过sun.reflect.noInflation=true来设置为不调用本地代码,在不调用本地代码的情况下,就转交给MethodAccessorGenerator来处理了,本地代码调用的情况在此则不进行阐述;

MethodAccessorGenerator中的generate方法根据JavaClass格式规范生成字节码,字节码中包括了ConstructorAccessor对象需要的newInstance方法,此newInstance方法对应的指令为invokespecial,所需的参数则从外部压入,生成的Constructor类的名字以:

sun/reflect/GeneratedSerializationConstructorAccessor或sun/reflect/GeneratedConstructorAccessor开头,后面跟随一个累计创建的对象的次数;

在生成了字节码后将其加载到当前的ClassLoader中,并实例化,完成ConstructorAccessor对象的创建过程,并将此对象放入构造器对象的缓存中;

执行获取的constructorAccessor.newInstance,这步和标准的方法调用没有任何区别。

●method.invoke(action,null);

这步执行的过程和上一步基本类似,只是在生成字节码时生成的方法改为了invoke,其调用的目标改为了传入的对象的方法,同时生成的类名改为了:

sun/reflect/GeneratedMethodAccessor。

按照上面的阐述,执行一段反射执行的代码后在debug中查看Method对象中的MethodAccessor对象引用(参数为-Dsun.reflect.noInflation=true,否则默认要执行15次反射调用后才会是动态生成字节码):

SunJDK采用以上方式提供了反射的实现,大大提升了代码编写的灵活性,但也可以看出,由于其整个过程比直接已经编译成字节码的调用复杂很多,因此性能比直接执行的慢了一些,但其实也慢不到太多,SunJDK中的反射执行的性能也是随着JDK版本的提升越来越好,到了JDK6后差距已经非常小了,但要注意的是getMethod是非常耗性能的,一方面是权限的校验,另外一方面所有方法的扫描以及Method对象的复制,因此在使用反射调用多的系统中应缓存getMethod返回的Method对象,至于method.invoke,其性能仅比直接调用低一点点,差不多是纳秒级的,一段典型的对比直接执行、反射执行性能的程序:

Objectobject=newObject();

intloop=1000000;

longbeginTime=System.currentTimeMillis();

for(inti=0;i

object.toString();

}

longendTime=System.currentTimeMillis();

System.out.println("直接调用消耗的时间为:

"+(endTime-b

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 外语学习 > 英语学习

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1