我们都知道java中比较常提到的一个概念就是类。但是在java机制中,类是怎么运行的呢?下面来看看L:


类被加载到虚拟机到使用,最后被卸载,经历了一下几个步骤:


(百度来的)

重点看前面几个:加载,验证,准备,解析,初始化。

加载,验证,准备,初始化顺序是确定的,解析有可能是在初始化之后。

同时这个按顺序有可能是异步进行的,比如验证到一半,就开始准备。

加载

这个阶段虚拟机准备下面这些事情:

1、通过一个类的全限定名来获取其定义的二进制字节流。(获取方式可以是本地class,zip,jar,网络)
 2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
 3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

这个阶段就有一个新的概念引入,那就是   类加载器 ClassLoader 1启动类加载器BOOTSTRAP class loader,这里使用C++/C/汇编实现,就是虚拟机的一部分。
Opened C:\Program Files\Java\jre1.8.0_112\lib\rt.jar]
2
扩展类加载器:Extension ClassLoader,由sun.misc.Launcher$ExtClassLoader实现
C:\Program Files\Java\jre1.8.0_112\lib\ext下面的类
3
应用程序类加载器:Application ClassLoader,由sun.misc.Launcher$AppClassLoader

加载的是我们写的类
4
、自定义类加载器:可以自己实现一个类加载器

写个程序测试下哪些类分别是哪些类加载器加载的:

public class TestClassLoader {
	public static void main(java.lang.String args[]) throws Exception{
		
		//类加载器
		System.out.println("启动类加载器--BOOTSTRAP ClassLoader 加载C:\\Program Files\\Java\\jre1.8.0_112\\lib\\rt.jar: "+String.class.getClassLoader());
		System.out.println("扩展类加载器--Extension ClassLoader 加载C:\\Program Files\\Java\\jre1.8.0_112\\lib\\ext下面的类 :"+com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
		System.out.println("应用程序类加载器--Application ClassLoader 加载所有我们写的类"+ TestClassLoader.class.getClassLoader().getClass().getName());
		
	}
	
}
打印出结果:

启动类加载器--BOOTSTRAP ClassLoader 加载C:\Program Files\Java\jre1.8.0_112\lib\rt.jar: null
扩展类加载器--Extension ClassLoader 加载C:\Program Files\Java\jre1.8.0_112\lib\ext下面的类 :sun.misc.Launcher$ExtClassLoader
应用程序类加载器--Application ClassLoader 加载所有我们写的类sun.misc.Launcher$AppClassLoader


类加载器都继承自java.lang.ClassLoader。里面是有一个引用parent,指向上一层的ClassLoader,使用getParent()方法获取。 

		private final ClassLoader parent;		
		@CallerSensitive
		public final ClassLoader getParent() {
			if (parent == null)
				return null;
			SecurityManager sm = System.getSecurityManager();
			if (sm != null) {
				// Check access to the parent class loader
				// If the caller's class loader is same as this class loader,
				// permission check is performed.
				checkClassLoaderPermission(parent, Reflection.getCallerClass());
			}
			return parent;
		}

这里就牵扯到对于某个一样的类,该是由谁来加载的问题:



加载的顺序是从上头加载到下头,先使用上头的类加载器。如果发现上头加载过就不会加载。
假设现在没有这种机制。然后你自己写一个String类,上面说过,如果是自己写的类,应该是使用Application ClassLoader加载,而不会用BOOTSTRAP ClassLoader加载(JDK封装了一个String类,这个类应该是有BOOTSTRAP ClassLoader加载)。String类,一般项目里面都会用的到,随便往里面写点什么破坏性的东西,让你酸爽一下。
但是有了这个机制之后,就是上头先加载,上头写把JDK里的String加载了,你这个小的就不能去碰了。
就好比 你的上头给了你一份名单,那个名单里面有他想要的美女,你碰见了,就别想着搞个大新闻,乖乖的送上去就好。
上头好过了,你也好过了,你就有机会给你的下头一份名单了。

但是他们不是继承关系!!!他们都继承自java.lang.ClassLoader。他们几个之间本没有父子关系

验证

这个过程就是验证下你的文件是不是符合jvm的标准,会不会有问题。说不定你往里面写点什么格式化的代码。。。

1.类文件的结构检查
	确保类文件遵从Java类文件的固定格式。
  2.语义检查
  确保类本身符合Java语言的语法规定,比如验证final类型的类没有子类,以及final类型的方法没有被覆盖。
  注意,语义检查的错误在编译器编译阶段就会通不过,但是如果有程序员通过非编译的手段生成了类文件,其中有可能会含有语义错误,此时的语义检查主要是防止这种没有编译而生成的class文件引入的错误。
  3.字节码验证
  确保字节码流可以被Java虚拟机安全地执行。
  字节码流代表Java方法(包括静态方法和实例方法),它是由被称作操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。
  字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。
  4.二级制兼容性的验证
  确保相互引用的类之间的协调一致。
  例如,在Worker类的gotoWork()方法中会调用Car类的run()方法,Java虚拟机在验证Worker类时,会检查在方法区内是否存在Car类的run()方法,假如不存在(当Worker类和Car类的版本不兼容就会出现这种问题),就会抛出NoSuchMethodError错误
	来源http://www.cnblogs.com/mengdd/p/3561952.html

准备

这个过程就是给那些类的变量赋初始值。

记住一点,除非是final static同时使用,如public final static int value = 3,这个时候值是3。

否则任何值都是初始值,

类型   默认值

boolean false 
char \u0000(null) 
byte (byte)0 
short (short)0 
int 0 
long 0L 
float 0.0f 
double 0.0d 

注意一下:这个是针对类的变量初始化。写在方法里面的值是没有被初始化的。

解析
这个过程之中,会将符号引用转化成直接引用。

比如说我们写个方法 ,这个时候是符号引用,在二进制文件下面,jvm解析时候就会找到这个方法的地址,将这个方法指向那个地址。


初始化

这个过程之中,就会真正的去执行java程序的代码了,比如说刚刚有个static int value=3

在这个阶段才会赋值给他3

其实这个阶段是执行类的<clinit>()方法的过程。


	这里简单说明下<clinit>()方法的执行规则:
    1、<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
    2、<clinit>()方法与实例构造器<init>()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此,在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
    3、<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
    4、接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成<clinit>()方法。但是接口鱼类不同的是:执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
    5、虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
	出处:http://blog.csdn.net/ns_code/article/details/17881581


最后用eclipse运行下:

右键项目--run as--run configuration--arguments下面的VMarguments  输入 "-verbose:class"这样就能看出类被加载的时候,发生了什么

public class TestDynamicLoading {
	public static void main(String args[]){
		new A();
		System.out.println("_-----");
		new B();
		new C();
		new C();
		new D();
		new D();
	}
}
class A{}
class B{}
class C{
	static {
		System.out.println("C loading");
	}
}
class D{
	{
		System.out.println("D loading");
	}
}

运行结果:

[Loaded A from file:/D:/studyDir/codeDir/java/web/TestDynamicLoading/bin/]
_-----
[Loaded B from file:/D:/studyDir/codeDir/java/web/TestDynamicLoading/bin/]
[Loaded C from file:/D:/studyDir/codeDir/java/web/TestDynamicLoading/bin/]
C loading
[Loaded D from file:/D:/studyDir/codeDir/java/web/TestDynamicLoading/bin/]
D loading
D loading
[Loaded java.lang.Shutdown from C:\Program Files\Java\jdk1.8.0_112\jre\lib\rt.jar]
[

可以看出,B被new 的时候,才显示load B 类都是使用的时候才被加载进来的

C的静态代码段,只会被load一次

D的动态代码段会被load2次,相当于,你这么写的话,在所有的构造函数里面都会先调用这一块的代码



Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐