一、类加载
1.加载
将class字节码加载到内存中,同时在方法区形成改类运行时数据结构。
同时在堆中产生一个Class对象,反射就是获取这个对象并对其进行操作。
2.链接
2.1验证:验证加载的类信息是否是否符合JVM规范。
2.2准备:分配内存,赋默认值。(此处的赋默认值不是赋初值,例如int i = 3,这个是赋初值)。
2.3解析:将虚拟机中的符号引用替换为直接引用。
3.初始化
3.1执行类构造器,由编译器自动收集类变量的赋值动作和静态语句块中的语句合并而成。
3.2如果父类没有初始化,则先初始化父类。
3.3类构造器会在多线程环境中被正确的加锁和同步。
3.4访问一个java类的静态域时,只有真正声明这个域的类才会被初始化。
这里主要看初始化部分,结合上述初始化中的几点我们来看下列代码:
public class Demo01 { public static void main(String[] args){ A a = new A(); System.out.println(a.weight); } } class A{ public static int weight = 100; static{ System.out.println("初始化A"); weight = 300; } public A(){ System.out.println("构造对象A"); } }
运行结果:
初始化A
构造对象A
300
首先我们看上述的 -->3.1执行类构造器,由编译器自动收集类变量的赋值动作和静态语句块中的语句合并而成。
一开始先由编译器收集变量的赋值动作,和静态语句块中的语句进行类构造。所以最先打印出初始化A,然后在main
方法中实例化了一个对象a,这时是调用A类的无参构造,所以打印出构造对象A,由于执行类构造器时,代码时顺序执行的
所以一开始weigth=100,后来执行静态块后weight是300。
public class Demo01 { public static void main(String[] args){ A_Child ac = new A_Child(); System.out.println(ac.weight); } } class A_Child extends A{ static{ System.out.println("初始化A_Child"); } public A_Child(){ System.out.println("构造对象A_Child"); } } class A{ public static int weight = 100; static{ System.out.println("初始化A"); weight = 300; } public A(){ System.out.println("构造对象A"); } }
运行结果:
初始化A
初始化A_Child
构造对象A
构造对象A_Child
300
3.2如果父类没有初始化,则先初始化父类。
我们可以看到,实例化子类A_Child是初始化父类A,然后在初始化A_Child,当创建A_Child对象并且调用继承自父类中的属性时,
也是先构造父类A对象,然后构造A_Child对象,最后调用weigth。
public class Demo01 { public static void main(String[] args){ System.out.println(A_Child.weight); } } class A_Child extends A{ static{ System.out.println("初始化A_Child"); } public A_Child(){ System.out.println("构造对象A_Child"); } } class A{ public static int weight = 100; static{ System.out.println("初始化A"); weight = 300; } public A(){ System.out.println("构造对象A"); } }
运行结果:
初始化A
300
上述3.4访问一个java类的静态域(属性)时,只有真正声明这个域(属性)的类才会被初始化。
例如这里是A_Child调用weigth属性,但代码运行后只初始化了A,只有真正声明这个域(属性)的类才会被初始化。
接下来我们具体看下什么情况会发生初始化,什么情况不会发生初始化:
类的主动引用(会发生初始化):
1.new一个对象。(new A())
2.调用类的静态成员(final常量除外)和静态方法。(A.weigth)
3.初始化一个类时,当其父类没有被初始化则会先初始化父类。(new A_Child();会先初始化父类)
4.通过反射引用也会初始化。()
附:类被加载后会维持一段时间的缓存,在缓存存在期间不会重复加载。
例如执行上述1中的new A()后A类被加载了,接着执行2中的A.weigth此时由于有缓存,所以不会再次加载改类。
类的被动引用(不会发生初始化):
1.引用常量(final修饰的)不会触发此类的初始化。
2.通过数组定义类引用不会发送初始化(A[] arr = new A[10];)
3.访问一个静态域是只有真正声明这个域的类才会被初始化。(int a = A_Child.weight;)
例如子类继承父类,调用子类中继承的属性,子类不会发生初始化,只会初始化父类
基本和上述初始化中的几点差不多,只不过这里对其更加具体化了。
二、类加载器
2.1类加载器作用
将class字节码加载到内存中,同时在方法区形成改类运行时数据结构。
同时在堆中产生一个Class对象,反射就是获取这个对象并对其进行操作。
2.2类加载器层次
2.2.1引导类加载器(bootstrap class loader):主要用于加载java核心库,由原生代码编写(比如C/C++)并不继承java.lang.java.lang.ClassLoder。
2.2.2扩展类加载器(extensions class loader):加载java的扩展库,由sun.misc.Launcher$ExtClassLoader实现。
2.2.3应用类加载器(application calss loader):根据java应用路径(java.class.path),加载java应用的类。由sun.misc.Launcher$AppClassLoader实现。
2.2.4自定义类加载器:通过继承java.lang.ClassLoder类来实现自定义类加载。
类加载器采取的是双亲委托机制,简单来说就是当前加载器不对其直接进行加载,而是向上传递直到顶层,再来判断是否可以加载。可以加载则加载,
不能加载则给下一级判断能否加载。所有加载器都无法加载就报错。
例如这里有个类只能由自定义加载器加载,那么首先会将其一级级上传到引导类加载器,接着引导类加载器判断是否可以加载,假设这里是不能加载,
则下传给扩展类加载器,仍然不嗯呢加载,继续下传到应用类加载器,还是不能加载。接着下传到自定义加载器,发现可以加载就将其加载。
可能感觉这样做是不是有些费事费力,为什么不首先判断当前加载器能否加载,如果能加载则直接加载,不能再上传判断能否加载这样做呢?
主要是为了核心库、扩展库等的安全。例如我们自己定义一个java.lang.String。如果采用双亲委托,加载的依然是核心库中的String。如果采用
我们假设的方法,那么假如第一个类加载器可以加载,那么核心库中的String就变成了我们自定义的String,这样显然是不安全。
2.3java.lang.ClassLoder
java.lang.ClassLoderClassLoader:根据类名找到或生成对应的字节代码,然后从这些字节代码中定义出一个java类,即java.lang.Class的实例。
除此之外,ClassLocal还负责加载java应用所需要的资源,如图片、配置文件等。
主要方法:
public final ClassLoader getParent();//获取上一级(父)构造器
protected final Class<?> findLoadedClass(String name);//如果该类已被加载则返回对应class对象,反之返回null
public Class<?> loadClass(String name);//加载指定类,返回对应class对象
protected final Class<?> defineClass(String name, byte[] b, int off, int len)//将字节数组转换为指定类,返回class对象。
public class Demo01 { public static void main(String[] args){ ClassLoader cl = ClassLoader.getSystemClassLoader(); System.out.println(cl);//返回当前委派的类加载器(应用类加载器) System.out.println(cl.getParent());//获取当前类加载器的父级(扩展类加载器) System.out.println(cl.getParent().getParent());//父级的父级(引导类加载器) //获取当前java.class.path即应用类加载器加载的路径 System.out.println(System.getProperty("java.class.path")); } }
运行结果: sun.misc.Launcher$AppClassLoader@73d16e93 sun.misc.Launcher$ExtClassLoader@15db9742 null(由于引导类加载器由原生代码编写,所以无法获取到)
E:eclipseClassLoaderin
下面我们就要结合java.lang.ClassLoder中的方法编写一个自定义类加载器。
具体步骤:
1.自定义MyLoader需要继承ClassLoader
2.当前类是否已经被加载,如果已经被加载则返回对应Class对象。
如果没有被加载则交给父构造器加载。
3.父构造器尝试对其加载,如果加载成功则返回对应class对象。
如果父加载器无法加载,则使用自定义加载器加载。
(这里只是简单的模拟下,所以只向上寻找了一层,不同于之前说的寻找到顶层为止)
4.读取对应.class文件并用字节数组返回,将字节数组转换为class对象
注:加载的类要有.class文件,即是已经完成编译了的。
package TestClassLoader; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; public class MyClassLoader extends ClassLoader{ private String root; public MyClassLoader(String root){ this.root = root; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // TODO Auto-generated method stub Class c = findLoadedClass(name);//如果改类已被加载返回对应类,反之返回null; if(c != null){//如果已加载,直接返回对应class对象 return c; }else{ //没有加载的话先调用父加载器加载 ClassLoader parent = this.getParent();//获取父加载器 //System.out.println(parent); //当前的父加载器,即应用加载器 try{ c = parent.loadClass(name);//加载指定类并返回加载的类 }catch(Exception e){ //父类可能无法加载指定类这里会弹出异常信息, //因为这里的父加载器是应用用加载器,而引用加载器是加载class.path路径下的类。 //而这个class.path代表当前项目地址(E:eclipseClassLoaderin) //但是我们要加载的类的地址是F:/TestJava/com/TestSsist/TestUser.class // e.printStackTrace();//这里出现异常是不影响的。 //因为即使出现异常,最终也会执行转换。 }finally{ if(c != null){//如果父加载器加载成功,直接返回。 return c; }else{ //如果父加载器没有加载成功 //获取指定class文件数据,并以字节数组返回,然后转换成对应类 byte[] classDate = getClassDate(name); if(classDate != null){ //将字节数组转换为class对象 c = defineClass(name,classDate,0,classDate.length); }else{ throw new ClassNotFoundException(); } } } } return c; } private byte[] getClassDate(String className){ int temp = 0; byte[] buffer = new byte[1024]; //com.TestSsist.TestUser --> root(F:/TestJava/) + com/TestSsist/TestUser.class String path = root + className.replace('.', '/') + ".class"; InputStream bi = null; ByteArrayOutputStream baos = null; try { bi = new FileInputStream(path); baos = new ByteArrayOutputStream(); while((temp = bi.read(buffer)) != -1){ baos.write(buffer, 0, temp); } } catch (IOException e) { // TODO Auto-generated catch block //e.printStackTrace(); } return baos.toByteArray(); } }
public class Demo01 { public static void main(String[] args){ //自定义类加载 MyClassLoader loader = new MyClassLoader("F:/TestJava/"); try { Class c = loader.findClass("com.TestSsist.TestUser"); System.out.println(c.getClassLoader());//打印出该类的类加载器 } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } //*/ } }
运行结果:
TestClassLoader.MyClassLoader@6d06d69c
2.4线程上下文加载器
java本身提供的双亲委托机制的加载有其优点,但也有其缺点。例如某些接口在java的扩展库中,通过扩展类加载器可以加载。
但其实现是由第三方厂家实现的,也就是接口声明和实现不在一起,这时就会出现无法加载的情况。
为了避免这一情况,java中还提供了线程上下文加载器,简单的说改变当前线程的加载器,让其可以根据我们实际需要调整。
例如我们一般是进入main方法中,这里打开一个main线程,默认一般使用应用类加载器加载,首先加载A(这里当然是用的当前线程默认的应用类加载器)。
然后我们修改当前main线程的加载器为其他的加载,然后通过获取的其他加载器进行加载,这样就可以解决接口实现分离问题。
自定义类加载器及加载类参照上述2.3内容。
public class Demo01 { public static void main(String[] args) throws ClassNotFoundException{ //获取当前线程加载器并打印(此时为应用类加载器) System.out.println(Thread.currentThread().getContextClassLoader()); //将当前线程的加载器设置为MyClassLoader Thread.currentThread().setContextClassLoader(new MyClassLoader("F:/TestJava/")); //获取当前线程加载器并打印(此时已变成MyClassLoader) System.out.println(Thread.currentThread().getContextClassLoader()); //获取当前线程类加载器,并加载指定类。 Class c = Thread.currentThread().getContextClassLoader().loadClass("com.TestSsist.TestUser"); //获取加载改类的类加载器 System.out.println(c.getClassLoader()); } }
运行结果:
sun.misc.Launcher$AppClassLoader@73d16e93
TestClassLoader.MyClassLoader@6d06d69c
TestClassLoader.MyClassLoader@6d06d69c