深入理解JAVA虚拟机 虚拟机执行子系统
Posted 张小贱1987
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解JAVA虚拟机 虚拟机执行子系统相关的知识,希望对你有一定的参考价值。
class类文件的结构
java的class类文件中存在两种结构:无符号数和表。最小的存储单元是8个字节。
无符号数是基本的数据类型,用来描述数字,UTF-8编码的字符串,索引引用。
表示多个无符号数构成的复杂数据结构。
其中:
- magic 表示魔数,并且魔数占用了4个字节,魔数到底是做什么的呢?它其实就是表示一下这个文件的类型是一个Class文件,而不是一张JPG图片,或者AVI的电影。而Class文件对应的魔数是0xCAFEBABE.
- minor_version 表示Class文件的次版本号。
- major_version 表示Class文件的主版本号。
- constant_pool 常量池 主要存储字面量和符号引用。其中字面量包含static字符串,static final常量等。符号引用包含类或者接口的全限定名、字段和方法的名称和描述符等。
还有各种描述类的信息,如:实现的接口列表,超类,方法列表,属性列表等。
一定程度上说,class文件中存储的信息在被加载后,大部分都是存放在方法区的。
其中method_info(方法表)包含:
访问标志-public private等
名称索引-方法名称(之所以叫索引是因为内容是放在常量池中的 这里只是一个索引)
描述符索引-参数列表+返回值(之所以叫索引是因为内容是放在常量池中的 这里只是一个索引)
属性表集合(attribute-info):一个方法中,除了上面三个之外的其他信息,包括方法的代码,都是存储在属性表中的。注意:这里说的是方法表里面的属性表,一个Class有自己的属性表,一个字段表(field_info)也有自己的属性表。
类文件-方法表-属性表-Code属性表:这基本对应运行时的一个栈帧。包含方法代码的字节码,操作数栈的最大深度值,局部变量表所需要的存储空间。
虚拟机类加载机制
类的加载和连接是在程序运行期间完成的。
类的生命周期
1、加载:查找并加载类的二进制数据 ,类的加载指的是将类的.class文件中的二进制数据读入到内存中(这个部分是类加载器做的),将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
2、连接
–验证:确保被加载的类的正确性
–准备:为类的静态变量分配内存,并将其初始化为默认值
–解析:把类中的符号引用转换为直接引用 (直接引用时直接指向内存地址,符号引用是描述目标的一种方法,需要在运行时转换为直接引用,为了实现多态,Java在编译的时候不能确定直接引用,所以设计了符号引用,然后一部分符号引用在解析阶段被转换,另外一部分在运行的时候才被转换。符号引用包含类符号引用,类字段符号引用,类方法符号引用)(符号引用包含对方法来说,只能把非虚方法的符号引用转换为直接引用,虚方法的直接饮用是在运行时转换的)
3、初始化:为类的静态变量赋予正确的初始值
初始化:
初始化是执行静态变量初始化代码和初始化块的过程。编译的时候会生成一个类构造器,clinit()方法,如果没有需要初始化的内容,就没有这个方法。所以初始化这个步骤是可有可无的。
clinit是将静态变量初始化代码和初始化块按照代码中编写的顺序糅合成的一个方法。
静态初始化块只能访问定义在它之前的变量,但是可以对定义在它之后的变量赋值。
整个运行期间,clinit方法在会在加载的过程中初始化一遍。java虚拟机会保证多线程环境中的对这段代码的加锁同步。所以如果clinit不要加载时间太长。
超类的clinit如果也没有执行,会先执行它。(这点对接口不适用,接口不需要加载超类的clinit)。
初始化的时间
虚拟机规范严格规定了有且只有5种情况必须立即对类进行初始化。
1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的),虚拟机会优先初始化这个主类。
5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic等时,这个方法的类还没有进行过初始化,则需要先触发其初始化。
其中getstatic、putstatic或invokestatic是指读取或者设置一个类的静态字段(被final修饰的字段、已经在编译器把结果存入常量池的静态字段除外)。
-
package jvm;
-
-
public class StaticTest {
-
public static void main(String[] args) {
-
System.out.println(Static.n);
-
System.out.println(Static.s);
-
System.out.println(Static.p);
-
}
-
}
-
-
class Static{
-
public static final int n = 10;
-
public static final String s = "10";
-
public static final Person p = new Person();
-
-
static
-
{
-
System.out.println("static");
-
}
-
}
-
class Person{
-
-
}
输出结果:
10
10
static
也就是说,访问static final类型的基本类型和Sting类型(经过测试,不包含Integer类型)的访问不会触发初始化操作,这部分是在编译阶段存储在常量池中的。
-
package jvm;
-
-
public class StaticTest {
-
public static void main(String[] args) {
-
System.out.println(StaticSub.n);
-
}
-
}
-
-
class StaticSuper{
-
public static int n = 10;
-
-
static
-
{
-
System.out.println("StaticSuper");
-
}
-
}
-
class StaticSub extends StaticSuper{
-
-
static
-
{
-
System.out.println("StaticSub");
-
}
-
}
结果:
StaticSuper
10
注意,这里不是输出StaticSub。
类加载器
定义:通过一个类的全限定名来获取一个二进制的字节流的过程。
java中的一些相等逻辑,包括Class对象的equals方法,isInstance方法,还有instanceof关键字,都是依赖于加载class的类加载器,不同类加载器加载的类,使用上面的比较总是返回false。
(1).BootStrap ClassLoader:启动类加载器,负责加载存放在%JAVA_HOME%\\lib目录中的,或者通被-Xbootclasspath参数所指定的路径中的,并且被java虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库,即使放在指定路径中也不会被加载)类库到虚拟机的内存中,启动类加载器无法被java程序直接引用。
(2).Extension ClassLoader:扩展类加载器,由sun.misc.Launcher$ExtClassLoader实现,负责加载%JAVA_HOME%\\lib\\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
(3).Application ClassLoader:应用程序类加载器,由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径classpath上所指定的类库,是类加载器ClassLoader中的getSystemClassLoader()方法的返回值,开发者可以直接使用应用程序类加载器,如果程序中没有自定义过类加载器,该加载器就是程序中默认的类加载器。
这里需要注意的是上述三个JDK提供的类加载器虽然是父子类加载器关系,但是没有使用继承,而是使用了组合关系。
从JDK1.2开始,java虚拟机规范推荐开发者使用双亲委派模式(ParentsDelegation Model)进行类加载,其加载过程如下:
(1).如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成。
(2).每一层的类加载器都把类加载请求委派给父类加载器,直到所有的类加载请求都应该传递给顶层的启动类加载器。
(3).如果顶层的启动类加载器无法完成加载请求,子类加载器尝试去加载,如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载。
双亲委派模式的类加载机制的优点是java类它的类加载器一起具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,保证了java程序的稳定运行。双亲委派模式的实现:
使用双亲委派模型自定义类加载器:
-
package jvm;
-
-
import java.io.FileInputStream;
-
import java.io.FileNotFoundException;
-
import java.io.IOException;
-
import java.io.InputStream;
-
-
public class MyClassLoader extends ClassLoader {
-
-
//使用应用程序类加载器作为父类加载器
-
private ClassLoader parent = ClassLoader.getSystemClassLoader();
-
-
@Override
-
/**
-
* loadClass是覆盖ClassLoader的方法 它是类加载器的加载类的入口 可以自己在整个方法中定义全部的加载逻辑
-
* 但是java建议 这个方法用来处理双亲委派的流程 加载的方法教给findClass去做
-
* 这里是一个参考方法 实际使用中不需要写这个方法
-
* 因为java已经在ClassLoader超类中用双亲加载模型定义好了loadClass的逻辑 前提是要定义好setparent getparent方法
-
*/
-
protected Class<?> loadClass(String name, boolean resolve)
-
throws ClassNotFoundException {
-
//首先测试是否已经加载过这个类
-
Class c = findLoadedClass(name);
-
if(c == null)
-
{
-
try {
-
//委派给超类去加载
-
parent.loadClass(name);
-
} catch (ClassNotFoundException e) {
-
//表示超类找不到这个类
-
}
-
-
if(c == null)
-
{
-
//调用自身的findClass方法来加载类
-
findClass(name);
-
}
-
}
-
//连接这个类
-
if(resolve)
-
{
-
resolveClass(c);
-
}
-
return c;
-
}
-
-
@Override
-
/**
-
* 加载方法
-
* 根据name得到class流
-
*/
-
protected Class<?> findClass(String name) throws ClassNotFoundException {
-
String path = name.substring(name.lastIndexOf(".") + 1);
-
path = "C:\\\\classes\\\\" + path;
-
try {
-
//获取文件流
-
InputStream is = new FileInputStream(path);
-
//获取字节数组
-
byte[] b = new byte[is.available()];
-
is.read(b, 0, b.length);
-
//调用超类的defineClass方法 根据name和字节数组返回Class
-
return defineClass(name,b,0,b.length);
-
} catch (FileNotFoundException e) {
-
e.printStackTrace();
-
} catch (IOException e) {
-
e.printStackTrace();
-
}
-
return super.findClass(name);
-
}
-
-
-
}
线程上下文类加载器
线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 Java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器(也就是应用程序类加载器)。在线程中运行的代码可以通过此类加载器来加载类和资源。
前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的(要给接口赋予实现的时候,引导类加载器在自己的内部找不到SPI的实现类在哪里,也不知道去哪里找),因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。
使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。
hot swap类加载器实现
hot swap即热插拔的意思,我们知道Java缺省的加载器对相同全名的类只会加载一次,以后直接从缓存中取这个Class object。因此要实现hot swap,必须在加载的那一刻进行拦截,先判断是否已经加载,若是则重新加载一次,否则直接首次加载它。
-
package classloader;
-
-
import java.net.URL;
-
import java.net.URLClassLoader;
-
-
/**
-
* 可以重新载入同名类的类加载器实现
-
* 放弃了双亲委派的加载链模式,需要外部维护重载后的类的成员变量状态
-
*/
-
public class HotSwapClassLoader extends URLClassLoader {
-
-
public HotSwapClassLoader(URL[] urls) {
-
super(urls);
-
}
-
-
public HotSwapClassLoader(URL[] urls, ClassLoader parent) {
-
super(urls, parent);
-
}
-
-
// 下面的两个重载load方法实现类的加载,仿照ClassLoader中的两个loadClass()
-
// 具体的加载过程代理给父类中的相应方法来完成
-
public Class<?> load(String name) throws ClassNotFoundException {
-
return load(name, false);
-
}
-
-
public Class<?> load(String name, boolean resolve) throws ClassNotFoundException {
-
// 若类已经被加载,则重新再加载一次
-
if (null != super.findLoadedClass(name)) {
-
return reload(name, resolve);
-
}
-
// 否则用findClass()首次加载它
-
Class<?> clazz = super.findClass(name);
-
if (resolve) {
-
super.resolveClass(clazz);
-
}
-
return clazz;
-
}
-
-
public Class<?> reload(String name, boolean resolve) throws ClassNotFoundException {
-
return new HotSwapClassLoader(super.getURLs(), super.getParent()).load(
-
name, resolve);
-
}
-
}
使用方法:
-
HotSwapClassLoader c1 = new HotSwapClassLoader(urls,a.getClass().getClassLoader());
-
Class clazz = c1.load("classloader.A"); // 用hot swap重新加载类A
-
Object aInstance = clazz.newInstance(); // 创建A类对象
在J2SE中还包括以下的功能使用不同的类加载器:
- JNDI使用线程上下文类加载器。
- Class.getResource()和Class.forName()使用当前类加载器。
- java.util.ResourceBundle使用调用者的当前类加载器。
- Java序列化API缺省使用调用者当前的类加载器。
类加载器与OSGi
OSGi是 Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGi 已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse就是基于OSGi 技术来构建的。
OSGi 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载。只需要设置系统属性 org.osgi.framework.bootdelegation的值即可。
假设有两个模块 bundleA 和 bundleB,它们都有自己对应的类加载器 classLoaderA 和 classLoaderB。在 bundleA 中包含类 com.bundleA.Sample,并且该类被声明为导出的,也就是说可以被其它模块所使用的。bundleB 声明了导入 bundleA 提供的类 com.bundleA.Sample,并包含一个类 com.bundleB.NewSample继承自 com.bundleA.Sample。在 bundleB 启动的时候,其类加载器 classLoaderB 需要加载类 com.bundleB.NewSample,进而需要加载类 com.bundleA.Sample。由于 bundleB 声明了类 com.bundleA.Sample是导入的,classLoaderB 把加载类 com.bundleA.Sample的工作代理给导出该类的 bundleA 的类加载器 classLoaderA。classLoaderA 在其模块内部查找类 com.bundleA.Sample并定义它,所得到的类 com.bundleA.Sample实例就可以被所有声明导入了此类的模块使用。对于以 java开头的类,都是由父类加载器来加载的。如果声明了系统属性 org.osgi.framework.bootdelegation=com.example.core.*,那么对于包 com.example.core中的类,都是由父类加载器来完成的。
OSGi 模块的这种类加载器结构,使得一个类的不同版本可以共存在 Java 虚拟机中,带来了很大的灵活性。不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:
(1)如果一个类库只有一个模块使用,把该类库的 jar 包放在模块中,在 Bundle-ClassPath中指明即可。
(2)如果一个类库被多个模块共用,可以为这个类库单独的创建一个模块,把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。
(3)如果类库提供了 SPI 接口,并且利用线程上下文类加载器来加载 SPI 实现的 Java 类,有可能会找不到 Java 类。如果出现了 NoClassDefFoundError异常,首先检查当前线程的上下文类加载器是否正确。通过 Thread.currentThread().getContextClassLoader()就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的话,可以首先通过 class.getClassLoader()来得到模块对应的类加载器,再通过 Thread.currentThread().setContextClassLoader()来设置当前线程的上下文类加载器。
osgi扩展:
http://blog.csdn.net/eddle/article/details/7089490
http://www.2cto.com/kf/201608/536691.html
https://www.ibm.com/developerworks/cn/opensource/os-cn-osgi-spring/
以上是关于深入理解JAVA虚拟机 虚拟机执行子系统的主要内容,如果未能解决你的问题,请参考以下文章
JVM | 第2部分:虚拟机执行子系统《深入理解 Java 虚拟机》