zoukankan      html  css  js  c++  java
  • 类加载机制

    • JVM和类的关系
    当我们通过java指令来运行某个java从程序时,该命令将启动一个java虚拟机进程。不管程序有多么复杂,不管启动了多少个线程,他们都处于java的虚拟机中。值得注意的是:2次运行java程序处于2个不同的JVM进程中,2个JVM之间并不会共享数据。比如我2次运行一个程序来修改一个类的一个静态属性,彼此之间是没有关系的。


    • 类加载和类初始化
    其实上面的2个名词我们经常说,但是准确的来讲,并不是很清楚它到底是啥意思,做些了什么事情。可以这样子理解:当我们在使用一个类的时候,如果这个类还没有被加载到内存中,那么系统会通过加载,连接,初始化3个步骤来对该类进行初始化。那么这3个步骤就是我们说类的初始化。
    1,类的加载:将类的class文件读入内存,并为之创建一个java.lang.Class对象。也就是说,当程序中使用任何类的时候,系统都会为之建立一个java.lang.Class对象。通过使用不同的类加载器,可以从不同来源加载类的2进制数据,比如说本地系统,jar包中,或者网络中。
    注意一点:程序不一定是说非要等到使用这个类的时候,才会去加载这个该类,也可以T前加载进去的。
    2,类的连接:把类的2进制数据合并到jar中。当类被加载之后,系统会为之生成一个对应的Class对象,这时开始进入连接阶段。类的连接又可以分为3个阶段:验证,准备,解析。

    3,类的初始化:对类的静态属性进行初始化。这里包含有2个钟方式: 第一种是申明一个属性的时候就指定初始值,第二种是静态初始化块为静态属性指定初始值。关于这一点在前面的另外一篇博客(类初始化)里面有详细介绍,这里简单总结下:要是2次都没有赋值的话,静态属性有默认值的,从上到下依次执行,每次都要先走父类,然后才能到自己,最终静态初始化块只执行一次,这个要和普通初始化块区分开来的。


    • ClassLoader
    类装载器是用来把类(class)装载进 JVM 的。java为ClassLoader提供了一个URLClassLoader的实现类(功能比较强大,可以从本地文件系统或者是远程主机上获取2机制文件来加载类)。一旦一个类被加载到JVM中,那么同一个类就不会再次载入了呢。这是一个抽象类,每一个Class对象都包含一个对定义他的ClassLoader的引用。现在问题来了:上面说了同一个类,那么怎么才算是“同一个类”?正如一个对象有一个唯一的标示一样,java中,一个对象使用全限定类名,也就是包名和类名作为标示,在JVM中,一个类用其权限定类名和其类的类加载器作为唯一标示。JVM 规范定义了两种类型的类装载器:启动类装载器(bootstrap)和用户自定义装载器(user-defined class loader)。 JVM在运行时会产生3个类加载器组成的初始化加载器层次结构 ,如下图所示:



     

    package tz.web.main;
    
    import java.util.*;
    import java.net.*;
    import java.io.*;
    
    public class ClassLoaderPropTest
    {
    	public static void main(String[] args) throws IOException
    	{
    		// 获取系统类加载器
    		ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
    		System.out.println("系统类加载器:" + systemLoader);
    		/*
    		 * 获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定 如果操作系统没有指定CLASSPATH环境变量,默认以当前路径作为
    		 * 系统类加载器的加载路径
    		 */ 
    		Enumeration<URL> em1 = systemLoader.getResources("");
    		while (em1.hasMoreElements())
    		{
    			//系统类加载器的加载路径:程序运行的当前路径
    			System.out.println(em1.nextElement());
    		}
    		// 获取系统类加载器的父类加载器:得到扩展类加载器
    		ClassLoader extensionLader = systemLoader.getParent();
    		//扩展类的加载器路径:java_homeinetc
    		System.out.println("扩展类加载器:" + extensionLader);
    		System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
    		//这里会输出null,因为根类加载器并没有继承ClassLoader这个抽象类,所以返回是null。它是用C++写的,无法直接获取
    		System.out.println("扩展类加载器的parent:" + extensionLader.getParent());
    	}
    }
    

     



    • 创建并使用自定义的类加载器
      如果需要定义自己的ClassLoader,可以重写loadClass和findClass2个方法。推荐重写findClass,load的那个方法里面也调用了find这个方法了呢。  下面程序开发了一个自定义的ClassLoader,通过重写findClass()方法来自定义的类加载机制。这个ClassLoader可以在加载类之前先编译该类的源文件,从而实现运行java之前就先编译该程序的目标,可以通过这个类直接运行java源文件。

    import java.io.*;
    import java.lang.reflect.*;
    
    public class CompileClassLoader extends ClassLoader
    { 
    	// 读取一个文件的内容
    	private byte[] getBytes(String filename)
    		throws IOException
    	{
    		File file = new File(filename);
    		long len = file.length();
    		byte[] raw = new byte[(int)len];
    		try(
    			FileInputStream fin = new FileInputStream(file))
    		{
    			// 一次读取class文件的全部二进制数据
    			int r = fin.read(raw); 
    			if(r != len)
    			throw new IOException("无法读取全部文件:"
    				+ r + " != " + len);
    			return raw;
    		}
    	} 
    	// 定义编译指定Java文件的方法
    	private boolean compile(String javaFile)
    		throws IOException
    	{
    		System.out.println("CompileClassLoader:正在编译 "
    			+ javaFile + "..."); 
    		// 调用系统的javac命令
    		Process p = Runtime.getRuntime().exec("javac " + javaFile);
    		try
    		{   
    			// 其他线程都等待这个线程完成
    			p.waitFor();
    		}
    		catch(InterruptedException ie)
    		{   	
    			System.out.println(ie);
    		}
    		// 获取javac线程的退出值
    		int ret = p.exitValue();
    		// 返回编译是否成功
    		return ret == 0;
    	} 
    	// 重写ClassLoader的findClass方法
    	protected Class<?> findClass(String name)
    		throws ClassNotFoundException
    	{
    		Class clazz = null;
    		// 将包路径中的点(.)替换成斜线(/)。
    		String fileStub = name.replace("." , "/");
    		String javaFilename = fileStub + ".java";
    		String classFilename = fileStub + ".class";
    		File javaFile = new File(javaFilename);
    		File classFile = new File(classFilename);
    		// 当指定Java源文件存在,且class文件不存在、或者Java源文件
    		// 的修改时间比class文件修改时间更晚,重新编译
    		if(javaFile.exists() && (!classFile.exists()
    			|| javaFile.lastModified() > classFile.lastModified()))
    		{
    			try
    			{
    				// 如果编译失败,或者该Class文件不存在
    				if(!compile(javaFilename) || !classFile.exists())
    				{
    					throw new ClassNotFoundException(
    						"ClassNotFoundExcetpion:" + javaFilename);
    				}
    			}
    			catch (IOException ex)
    			{
    				ex.printStackTrace();
    			}
    		}
    		// 如果class文件存在,系统负责将该文件转换成Class对象
    		if (classFile.exists())
    		{
    			try
    			{
    				// 将class文件的二进制数据读入数组
    				byte[] raw = getBytes(classFilename);
    				// 调用ClassLoader的defineClass方法将二进制数据转换成Class对象
    				clazz = defineClass(name,raw,0,raw.length);
    			}
    			catch(IOException ie)
    			{
    				ie.printStackTrace();
    			}
    		}
    		// 如果clazz为null,表明加载失败,则抛出异常
    		if(clazz == null)
    		{
    			throw new ClassNotFoundException(name);
    		}
    		return clazz;
    	}
    	// 定义一个主方法
    	public static void main(String[] args) throws Exception
    	{
    		// 如果运行该程序时没有参数,即没有目标类
    		if (args.length < 1)
    		{
    			System.out.println("缺少目标类,请按如下格式运行Java源文件:");
    			System.out.println("java CompileClassLoader ClassName");
    		}
    		// 第一个参数是需要运行的类
    		String progClass = args[0];
    		// 剩下的参数将作为运行目标类时的参数,
    		// 将这些参数复制到一个新数组中
    		String[] progArgs = new String[args.length-1];
    		System.arraycopy(args , 1 , progArgs 
    			, 0 , progArgs.length);
    		CompileClassLoader ccl = new CompileClassLoader();
    		// 加载需要运行的类
    		Class<?> clazz = ccl.loadClass(progClass);
    		// 获取需要运行的类的主方法
    		Method main = clazz.getMethod("main" , (new String[0]).getClass());
    		Object[] argsArray = {progArgs};
    		main.invoke(null,argsArray);
    	}
    }
    

    • 用类加载器的方式管理资源和配置文件
    关于java项目来读取配置文件的几种方式:
    1,读取输入流:配置文件要放在java项目的内部,读取的时候直接写路径就可以了。这个是系统默认的,他会自己计算出位置的,所以写的时候直接写就可以了。比如:Inputstream ips = new FileInputStream("Linkin.properties")。JDK的原话是这样子说的,注意理解:通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的路径名 name 指定。创建一个新 FileDescriptor 对象来表示此文件连接。
    2,读取资源:ClassLoader的getResourceAsStream()方法,或者Class的getResourceAsStream()方法,里面的参数是从classpath类路径下去寻找的,可以写绝对路径,也可以写相对路径,在框架中使用很多。注意理解:这里只是在查找一个普通的资源文件,不想上面那样子给对应的系统建立一个连接,而是从classPath路径里面去找。所以这里要不写相对路径(直接写),要不写绝对路径(/开头)

  • 相关阅读:
    Docker之Linux UnionFS
    Docker之Linux Cgroups
    Docker之Linux Namespace
    理解Docker容器的进程管理
    Docker命令详解
    协同过滤和基于内容推荐有什么区别?
    Docker 有什么优势?
    kubernetes
    Docker如何为企业产生价值?
    关于网页的几种高度说明
  • 原文地址:https://www.cnblogs.com/LinkinPark/p/5233122.html
Copyright © 2011-2022 走看看