精华内容
下载资源
问答
  • JAVA类之间方法的调用

    万次阅读 多人点赞 2018-11-02 23:39:15
    JAVA类方法的调用一、静态方法调用其他方法:1. 静态方法调用非静态方法2.静态方法调用静态方法二、非静态方法调用其他方法1.非静态方法在同一类内调用其他方法2.非静态方法在不同类之间调用其他方法 注:调用方法...

    注:调用方法——调用另一方法的方法
    被调用方法——被调用的方法

    一、静态方法调用其他方法:

    1. 静态方法调用非静态方法

    无论是否在同一类内,均需要通过对象调用

    //Test类
    package main;
    
    public class Test {
    
        public void IsSon1(){//非静态方法
            System.out.println("java大爷,你成功调用了你非静态方法里的儿子1");
        }
    
        public static void main(String[] args) {
            Test son1=new Test();
            son1.IsSon1();//静态方法通过对象调用此类中的非静态方法
            Son son=new Son();//静态方法通过对象调用Son类中的非静态方法
            son.IsSon();
        }
    }
    
    //Son类
    package main;
    
    public class Son {
        public  void IsSon(){
            System.out.println("java大爷,你成功的调用了你另一个类里的儿子");
        }//Son类里的非静态方法
    }
    

    输出结果

    2.静态方法调用静态方法

    同一类内直接调用,不同类内直接通过类名.方法名(参数表)调用

    package main;
    
    public class Test {
    
        public static void IsSon2(){//静态方法
            System.out.println("java大爷,你成功调用了你静态方法里的儿子2");
        }
    
        public static void main(String[] args) {
            IsSon2();//静态方法直接调用类内的静态方法
            Son.IsSon3();//静态方法通过类名直接调用Son类中的静态方法
        }
    }
    
    package main;
    
    public class Son {
        public  static void IsSon3(){
            System.out.println("java大爷,你成功的调用了你另一个类里的静态儿子3");
        }//Son类里的静态方法
    }
    
    

    输出结果

    二、非静态方法调用其他方法

    1.非静态方法在同一类内调用其他方法

    在同一类内,非静态方法可以直接调用静态方法和非静态方法

    package main;
    
    public class Test {
    
        public void Son1(){//非静态方法
            System.out.println("java大爷,你成功调用了你非静态方法里的儿子1");
        }
        public static void Son2(){//静态方法
            System.out.println("java大爷,你成功调用了你静态方法里的儿子2");
        }
        public void alloutput(){
            Son1();//非静态方法直接调用类内的非静态方法△
            Son2();//非静态方法直接调用类内的静态方法△
        }
        public static void main(String[] args) {
            Test test = new Test();
            test.alloutput();//前边已学静态方法通过对象调用非静态方法
        }
    }
    

    输出结果

    2.非静态方法在不同类之间调用其他方法

    在不同类之间,非静态方法需要通过对象才能调用非静态方法。
    非静态方法既可以通过对象调用静态方法又可以通过类名直接调用(由于对象的调用方式属于非静态调用方式,所以建议使用类名直接调用静态方法)

    package main;
    
    public class Test {
    
        public void output(){
            Son.IsSon1();//通过类名直接调用Son类中的静态方法,不建议使用对象调用静态方法
            Son son = new Son();
            son.IsSon2();//通过对象调用Son类中的非静态方法
        }
    
        public static void main(String[] args) {
            //同一类中的静态方法调用非静态方法output,前边已经涉及到
            Test test = new Test();
            test.output();
        }
    }
    
    package main;
    
    public class Son {
    
        public  static void IsSon1(){//Son类里的静态方法
            System.out.println("java大爷,你成功的调用了你另一个类里的静态儿子1");
        }
    
        public  void IsSon2(){//Son类里的非静态方法
            System.out.println("java大爷,你成功的调用了你另一个类里的非静态儿子2");
        }
    }
    
    

    输出结果

    借鉴前人的经验1

    借鉴前人的经验2
    小白上路,如有问题希望各路神手指教 /抱拳

    展开全文
  • 深入理解Java类加载器(一):Java类加载原理解析

    万次阅读 多人点赞 2017-05-15 20:47:44
    每个开发人员对java.lang...本文简述了JVM三种预定义类加载器,即启动类加载器、扩展类加载器和系统类加载器,并介绍和分析它们之间的关系和类加载所采用的双亲委派机制,给出并分析了与Java类加载原理相关的若干问题。

    摘要:

    每个开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这个异常背后涉及到的是Java技术体系中的类加载机制。本文简述了JVM三种预定义类加载器,即启动类加载器、扩展类加载器和系统类加载器,并介绍和分析它们之间的关系和类加载所采用的双亲委派机制,给出并分析了与Java类加载原理相关的若干问题。


    版权声明:

    本文作者:书呆子Rico
    作者博客地址:http://blog.csdn.net/justloveyou_/


    一、引子

    每个开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,其实,这个异常背后涉及到的是Java技术体系中的类加载。Java类加载机制虽然和大部分开发人员直接打交道的机会不多,但是对其机理的理解有助于排查程序出现的类加载失败等技术问题,对理解Java虚拟机的连接模型和Java语言的动态性都有很大帮助。


    二. Java 虚拟机类加载器结构简述

    1、JVM三种预定义类型类加载器

    当JVM启动的时候,Java开始使用如下三种类型的类加载器:

    启动(Bootstrap)类加载器:启动类加载器是用本地代码实现的类加载器,它负责将JAVA_HOME/lib下面的核心类库或-Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用。具体可由启动类加载器加载到的路径可通过System.getProperty(“sun.boot.class.path”)查看。

    扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将JAVA_HOME /lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器,具体可由扩展类加载器加载到的路径可通过System.getProperty("java.ext.dirs")查看。

    系统(System)类加载器:系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径,如第四节中的问题6所述)下的类库加载到内存中。开发者可以直接使用系统类加载器,具体可由系统类加载器加载到的路径可通过System.getProperty("java.class.path")查看。

    Ps: 除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载器,这个将在《深入理解Java类加载器(二):线程上下文类加载器》一文中进行单独介绍。


    2、类加载双亲委派机制介绍和分析

    JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归 (本质上就是loadClass函数的递归调用),因此所有的加载请求最终都应该传送到顶层的启动类加载器中。如果父类加载器可以完成这个类加载请求,就成功返回;只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。事实上,大多数情况下,越基础的类由越上层的加载器进行加载,因为这些基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API(当然,也存在基础类回调用户用户代码的情形,即破坏双亲委派模型的情形)。 关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和扩展类加载器为例作简单分析。
    标准扩展类加载器继承层次图-17.2kB
    系统类加载器继承层次图-16.4kB
      上面两张图分别是扩展类加载器继承层次图和系统类加载器继承层次图。通过这两张图我们可以看出,扩展类加载器和系统类加载器均是继承自 java.lang.ClassLoader抽象类。我们下面我们就看简要介绍一下抽象类 java.lang.ClassLoader 中几个最重要的方法:

    //加载指定名称(包括包名)的二进制类型,供用户调用的接口  
    public Class<?> loadClass(String name) throws ClassNotFoundException{}  
      
    //加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承用  
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{}  
      
    //findClass方法一般被loadClass方法调用去加载指定名称类,供继承用  
    protected Class<?> findClass(String name) throws ClassNotFoundException {}  
      
    //定义类型,一般在findClass方法中读取到对应字节码后调用,final的,不能被继承  
    //这也从侧面说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)  
    protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{}  
    

    通过进一步分析标准扩展类加载器和系统类加载器的代码以及其公共父类(java.net.URLClassLoader和java.security.SecureClassLoader)的代码可以看出,都没有覆写java.lang.ClassLoader中默认的加载委派规则 — loadClass(…)方法。既然这样,我们就可以从java.lang.ClassLoader中的loadClass(String name)方法的代码中分析出虚拟机默认采用的双亲委派机制到底是什么模样:

    public Class<?> loadClass(String name) throws ClassNotFoundException {  
        return loadClass(name, false);  
    }  
      
    protected synchronized Class<?> loadClass(String name, boolean resolve)  
            throws ClassNotFoundException {  
      
        // 首先判断该类型是否已经被加载  
        Class c = findLoadedClass(name);  
        if (c == null) {  
            //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载  
            try {  
                if (parent != null) {  
                    //如果存在父类加载器,就委派给父类加载器加载  
                    c = parent.loadClass(name, false);  
                } else {    // 递归终止条件
                    // 由于启动类加载器无法被Java程序直接引用,因此默认用 null 替代
                    // parent == null就意味着由启动类加载器尝试加载该类,  
                    // 即通过调用 native方法 findBootstrapClass0(String name)加载  
                    c = findBootstrapClass0(name);  
                }  
            } catch (ClassNotFoundException e) {  
                // 如果父类加载器不能完成加载请求时,再调用自身的findClass方法进行类加载,若加载成功,findClass方法返回的是defineClass方法的返回值
                // 注意,若自身也加载不了,会产生ClassNotFoundException异常并向上抛出
                c = findClass(name);  
            }  
        }  
        if (resolve) {  
            resolveClass(c);  
        }  
        return c;  
    }  
    

    通过上面的代码分析,我们可以对JVM采用的双亲委派类加载机制有了更直接的认识。下面我们就接着分析一下启动类加载器、标准扩展类加载器和系统类加载器三者之间的关系。可能大家已经从各种资料上面看到了如下类似的一幅图片:

    类加载器默认委派关系图-11.2kB

    上面图片给人的直观印象是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面我们就用代码具体测试一下:

    public class LoaderTest {  
      
        public static void main(String[] args) {  
            try {  
                System.out.println(ClassLoader.getSystemClassLoader());  
                System.out.println(ClassLoader.getSystemClassLoader().getParent());  
                System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
    }/* Output: 
            sun.misc.Launcher$AppClassLoader@6d06d69c  
            sun.misc.Launcher$ExtClassLoader@70dea4e  
            null  
     *///:~
    

    通过以上的代码输出,我们知道:通过java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器 ,并且可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时却得到了null。事实上,由于启动类加载器无法被Java程序直接引用,因此JVM默认直接使用 null 代表启动类加载器。我们还是借助于代码分析一下,首先看一下java.lang.ClassLoader抽象类中默认实现的两个构造函数:

    protected ClassLoader() {  
        SecurityManager security = System.getSecurityManager();  
        if (security != null) {  
            security.checkCreateClassLoader();  
        }  
        //默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器  
        this.parent = getSystemClassLoader();  
        initialized = true;  
    }  
    
    protected ClassLoader(ClassLoader parent) {  
        SecurityManager security = System.getSecurityManager();  
        if (security != null) {  
            security.checkCreateClassLoader();  
        }  
        //强制设置父类加载器  
        this.parent = parent;  
        initialized = true;  
    }  
    

    紧接着,我们再看一下ClassLoader抽象类中parent成员的声明:

    // The parent class loader for delegation  
    private ClassLoader parent; 
    

    声明为私有变量的同时并没有对外提供可供派生类访问的public或者protected设置器接口(对应的setter方法),结合前面的测试代码的输出,我们可以推断出:

    1.系统类加载器(AppClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

    2.扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null(null 本身就代表着引导类加载器)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

    事实上,这就是启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系。


    3、类加载双亲委派示例

    以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合JDK代码对JVM默认使用的双亲委派类加载机制做了分析。下面我们就来看一个综合的例子,首先在IDE中建立一个简单的java应用工程,然后写一个简单的JavaBean如下:

    package classloader.test.bean;  
    
    public class TestBean {  
          
        public TestBean() { }  
    }  
    

    在现有当前工程中另外建立一个测试类(ClassLoaderTest.java)内容如下:

    package classloader.test.bean;  
      
    public class ClassLoaderTest {  
      
        public static void main(String[] args) {  
            try {  
                //查看当前系统类路径中包含的路径条目  
                System.out.println(System.getProperty("java.class.path"));  
                //调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean  
                Class typeLoaded = Class.forName("classloader.test.bean.TestBean");  
                //查看被加载的TestBean类型是被那个类加载器加载的  
                System.out.println(typeLoaded.getClassLoader());  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
    }/* Output: 
            I:\AlgorithmPractice\TestClassLoader\bin
            sun.misc.Launcher$AppClassLoader@6150818a
     *///:~  
    

    将当前工程输出目录下的TestBean.class打包进test.jar剪贴到<Java_Runtime_Home>/lib/ext目录下(现在工程输出目录下和JRE扩展目录下都有待加载类型的class文件)。再运行测试一测试代码,结果如下:

        I:\AlgorithmPractice\TestClassLoader\bin
        sun.misc.Launcher$ExtClassLoader@15db9742
    

    对比上面的两个结果,我们明显可以验证前面说的双亲委派机制:系统类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。

    最后,将test.jar拷贝一份到<Java_Runtime_Home>/lib下,运行测试代码,输出如下:

        I:\AlgorithmPractice\TestClassLoader\bin
        sun.misc.Launcher$ExtClassLoader@15db9742
    

    可以看到,后两次输出结果一致。那就是说,放置到<Java_Runtime_Home>/lib目录下的TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载<JAVA_HOME>/lib目录下存在的陌生类。换句话说,虚拟机只加载<JAVA_HOME>/lib目录下它可以识别的类。因此,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除<JAVA_HOME>/lib/ext目录下和工程输出目录下的TestBean对应的class文件,然后再运行测试代码,则将会有ClassNotFoundException异常抛出。有关这个问题,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中设置相应断点进行调试,会发现findBootstrapClass0()会抛出异常,然后在下面的findClass方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过JDT中变量视图查看验证。


    三. Java 程序动态扩展方式

    Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以加载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。运行时动态扩展java应用程序有如下两个途径:


    1、反射 (调用java.lang.Class.forName(…)加载类)

    这个方法其实在前面已经讨论过,在后面的问题2解答中说明了该方法调用会触发哪个类加载器开始加载任务。这里需要说明的是多参数版本的forName(…)方法:

    public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException  
    

    这里的initialize参数是很重要的,它表示在加载同时是否完成初始化的工作(说明:单参数版本的forName方法默认是完成初始化的)。有些场景下需要将initialize设置为true来强制加载同时完成初始化,例如典型的就是加载数据库驱动问题。因为JDBC驱动程序只有被注册后才能被应用程序使用,这就要求驱动程序类必须被初始化,而不单单被加载。

    // 加载并实例化JDBC驱动类
    Class.forName(driver);
     
     // JDBC驱动类的实现
    public class Driver extends NonRegisteringDriver implements java.sql.Driver {
        public Driver() throws SQLException {
        }
    	// 将initialize设置为true来强制加载同时完成初始化,实现驱动注册
        static {
            try {
                DriverManager.registerDriver(new Driver());
            } catch (SQLException var1) {
                throw new RuntimeException("Can\'t register driver!");
            }
        }
    }
    

    2、用户自定义类加载器
      
      通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法做了介绍,这里就简要叙述一下一般用户自定义类加载器的工作流程(可以结合后面问题解答一起看):

    1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2;

    2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真实虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3;

    3、调用本类加载器的findClass(…)方法,试图获取对应的字节码。如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败, 向上抛异常给loadClass(…), loadClass(…)转而调用findClass(…)方法处理异常,直至完成递归调用。

    必须指出的是,这里所说的自定义类加载器是指JDK1.2以后版本的写法,即不覆写改变java.lang.loadClass(…)已有委派逻辑情况下。整个加载类的过程如下图:

    自定义类加载器加载类的过程-54.2kB


    四. 常见问题分析

    1、由不同的类加载器加载的指定类还是相同的类型吗?

    在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中,一个类用其全名 和 一个ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。我们可以用两个自定义类加载器去加载某自定义类型(注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果,如下所示:

    public class TestBean {
    
    	public static void main(String[] args) throws Exception {
    	    // 一个简单的类加载器,逆向双亲委派机制
    	    // 可以加载与自己在同一路径下的Class文件
    		ClassLoader myClassLoader = new ClassLoader() {
    			@Override
    			public Class<?> loadClass(String name)
    					throws ClassNotFoundException {
    				try {
    					String filename = name.substring(name.lastIndexOf(".") + 1)
    							+ ".class";
    					InputStream is = getClass().getResourceAsStream(filename);
    					if (is == null) {
    						return super.loadClass(name);   // 递归调用父类加载器
    					}
    					byte[] b = new byte[is.available()];
    					is.read(b);
    					return defineClass(name, b, 0, b.length);
    				} catch (Exception e) {
    					throw new ClassNotFoundException(name);
    				}
    			}
    		};
    
    		Object obj = myClassLoader.loadClass("classloader.test.bean.TestBean")
    				.newInstance();
    		System.out.println(obj.getClass());
    		System.out.println(obj instanceof classloader.test.bean.TestBean);
    	}
    }/* Output: 
            class classloader.test.bean.TestBean
            false  
     *///:~    
    

    我们发现,obj 确实是类classloader.test.bean.TestBean实例化出来的对象,但当这个对象与类classloader.test.bean.TestBean做所属类型检查时却返回了false。这是因为虚拟机中存在了两个TestBean类,一个是由系统类加载器加载的,另一个则是由我们自定义的类加载器加载的,虽然它们来自同一个Class文件,但依然是两个独立的类,因此做所属类型检查时返回false。


    2、在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

    Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码:

    //java.lang.Class.java  
    publicstatic Class<?> forName(String className) throws ClassNotFoundException {  
        return forName0(className, true, ClassLoader.getCallerClassLoader());  
    }  
      
    //java.lang.ClassLoader.java  
    // Returns the invoker's class loader, or null if none.  
    static ClassLoader getCallerClassLoader() {  
        // 获取调用类(caller)的类型  
        Class caller = Reflection.getCallerClass(3);  
        // This can be null if the VM is requesting it  
        if (caller == null) {  
            return null;  
        }  
        // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader  
        return caller.getClassLoader0();  
    }  
      
    //java.lang.Class.java  
    //虚拟机本地实现,获取当前类的类加载器,前面介绍的Class的getClassLoader()也使用此方法  
    native ClassLoader getClassLoader0(); 
    

    3、在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是谁?
      前面讲过,在不指定父类加载器的情况下,默认采用系统类加载器。可能有人觉得不明白,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:

    //摘自java.lang.ClassLoader.java  
    protected ClassLoader() {  
        SecurityManager security = System.getSecurityManager();  
        if (security != null) {  
            security.checkCreateClassLoader();  
        }  
        this.parent = getSystemClassLoader();  
        initialized = true;  
    } 
    

    我们再来看一下对应的getSystemClassLoader()方法的实现:

    private static synchronized void initSystemClassLoader() {  
        //...  
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();  
        scl = l.getClassLoader();  
        //...  
    }  
    

    我们可以写简单的测试代码来测试一下:

    System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());  
    

    本机对应输出如下:

    sun.misc.Launcher$AppClassLoader@73d16e93 
    

    所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:即使用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:

    • <Java_Runtime_Home>/lib下的类;
    • <Java_Runtime_Home>/lib/ext下或者由系统变量java.ext.dir指定位置中的类;
    • 当前工程类路径下或者由系统变量java.class.path指定位置中的类。

    4、在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?

    JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:即使用户自定义类加载器不指定父类加载器,那么,同样可以加载到<JAVA_HOME>/lib下的类,但此时就不能够加载<JAVA_HOME>/lib/ext目录下的类了。

    Ps:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题 5。


    5、编写自定义类加载器时,一般有哪些注意点?

    1)、一般尽量不要覆写已有的loadClass(…)方法中的委派逻辑(Old Generation)

    一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:

    //用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)  
    public class WrongClassLoader extends ClassLoader {  
      
        public Class<?> loadClass(String name) throws ClassNotFoundException {  
            return this.findClass(name);  
        }  
      
        protected Class<?> findClass(String name) throws ClassNotFoundException {  
            // 假设此处只是到工程以外的特定目录D:\library下去加载类  
            // 具体实现代码省略  
        }  
    }  
    

    通过前面的分析我们已经知道,这个自定义类加载器WrongClassLoader的默认类加载器是系统类加载器,但是现在问题4中的结论就不成立了。大家可以简单测试一下,现在<JAVA_HOME>/lib、<JAVA_HOME>/lib/ext 和 工程类路径上的类都加载不上了。

    //问题5测试代码一  
    public class WrongClassLoaderTest {  
        publicstaticvoid main(String[] args) {  
            try {  
                WrongClassLoader loader = new WrongClassLoader();  
                Class classLoaded = loader.loadClass("beans.Account");  
                System.out.println(classLoaded.getName());  
                System.out.println(classLoaded.getClassLoader());  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
    }/* Output: 
            java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)  
            at java.io.FileInputStream.open(Native Method)  
            at java.io.FileInputStream.<init>(FileInputStream.java:106)  
            at WrongClassLoader.findClass(WrongClassLoader.java:40)  
            at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
            at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)  
            at java.lang.ClassLoader.defineClass1(Native Method)  
            at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
            at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
            at WrongClassLoader.findClass(WrongClassLoader.java:43)  
            at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
            at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  
    Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object  
            at java.lang.ClassLoader.defineClass1(Native Method)  
            at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
            at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
            at WrongClassLoader.findClass(WrongClassLoader.java:43)  
            at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
            at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  
     *///:~    
    

    注意,这里D:"classes"beans"Account.class是物理存在的。这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass()引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。

    //问题5测试二  
    //用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)  
    public class WrongClassLoader extends ClassLoader {  
        protected Class<?> findClass(String name) throws ClassNotFoundException {  
            //假设此处只是到工程以外的特定目录D:\library下去加载类  
            //具体实现代码省略  
        }  
    }/* Output: 
            beans.Account  
            WrongClassLoader@1c78e57  
     *///:~  
    

    将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出正确。


    2). 正确设置父类加载器

    通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。


    3). 保证findClass(String name)方法的逻辑正确性

    事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。


    6、如何在运行时判断系统类加载器能加载哪些路径下的类?

    一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到。二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :System.getProperty(“java.class.path”)。如下所示,

    public class Test {
    	public static void main(String[] args) {
    		System.out.println("Rico");
    		Gson gson = new Gson();
    		System.out.println(gson.getClass().getClassLoader());
    		System.out.println(System.getProperty("java.class.path"));
    	}
    }/* Output: 
            Rico
    		sun.misc.Launcher$AppClassLoader@6c68bcef
    		I:\AlgorithmPractice\TestClassLoader\bin;I:\Java\jars\Gson\gson-2.3.1.jar
     *///:~ 
    

    如上述程序所示,Test类和Gson类由系统类加载器加载,并且其加载路径就是用户类路径,包括当前类路径和引用的第三方类库的路径。


    7、如何在运行时判断标准扩展类加载器能加载哪些路径下的类?

    利用如下方式即可判断:

    import java.net.URL;
    import java.net.URLClassLoader;  
    
    public class ClassLoaderTest {  
      
        /** 
         * @param args the command line arguments 
         */  
        public static void main(String[] args) {  
            try {  
                URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();  
                for (int i = 0; i < extURLs.length; i++) {  
                    System.out.println(extURLs[i]);  
                }  
            } catch (Exception e) {  
                //…  
            }  
        }  
    } /* Output: 
            file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/access-bridge-64.jar
            file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/dnsns.jar
            file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/jaccess.jar
            file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/localedata.jar
            file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/sunec.jar
            file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/sunjce_provider.jar
            file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/sunmscapi.jar
            file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/zipfs.jar
     *///:~ 
    

    五. 开发自己的类加载器

    在前面介绍类加载器的代理委派模型的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类,这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

    方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。

    类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

    在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。


    1、文件系统类加载器

    package classloader;  
      
    import java.io.ByteArrayOutputStream;  
    import java.io.File;  
    import java.io.FileInputStream;  
    import java.io.IOException;  
    import java.io.InputStream;  
      
    // 文件系统类加载器  
    public class FileSystemClassLoader extends ClassLoader {  
      
        private String rootDir;  
      
        public FileSystemClassLoader(String rootDir) {  
            this.rootDir = rootDir;  
        }  
      
        // 获取类的字节码  
        @Override  
        protected Class<?> findClass(String name) throws ClassNotFoundException {  
            byte[] classData = getClassData(name);  // 获取类的字节数组  
            if (classData == null) {  
                throw new ClassNotFoundException();  
            } else {  
                return defineClass(name, classData, 0, classData.length);  
            }  
        }  
      
        private byte[] getClassData(String className) {  
            // 读取类文件的字节  
            String path = classNameToPath(className);  
            try {  
                InputStream ins = new FileInputStream(path);  
                ByteArrayOutputStream baos = new ByteArrayOutputStream();  
                int bufferSize = 4096;  
                byte[] buffer = new byte[bufferSize];  
                int bytesNumRead = 0;  
                // 读取类文件的字节码  
                while ((bytesNumRead = ins.read(buffer)) != -1) {  
                    baos.write(buffer, 0, bytesNumRead);  
                }  
                return baos.toByteArray();  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
            return null;  
        }  
      
        private String classNameToPath(String className) {  
            // 得到类文件的完全路径  
            return rootDir + File.separatorChar  
                    + className.replace('.', File.separatorChar) + ".class";  
        }  
    }  
    

    如上所示,类 FileSystemClassLoader继承自类java.lang.ClassLoader。在java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。

    类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。加载本地文件系统上的类,示例如下:

    package com.example;  
      
    public class Sample {  
      
        private Sample instance;  
      
        public void setSample(Object instance) {  
            System.out.println(instance.toString());  
            this.instance = (Sample) instance;  
        }  
    }  
    
    package classloader;  
      
    import java.lang.reflect.Method;  
      
    public class ClassIdentity {  
      
        public static void main(String[] args) {  
            new ClassIdentity().testClassIdentity();  
        }  
      
        public void testClassIdentity() {  
            String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes";  
            FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);  
            FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);  
            String className = "com.example.Sample";  
            try {  
                Class<?> class1 = fscl1.loadClass(className);  // 加载Sample类  
                Object obj1 = class1.newInstance();  // 创建对象  
                Class<?> class2 = fscl2.loadClass(className);  
                Object obj2 = class2.newInstance();  
                Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);  
                setSampleMethod.invoke(obj1, obj2);  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
    }/* Output: 
            com.example.Sample@7852e922
     *///:~   
    

    2、网络类加载器

    下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。

    类 NetworkClassLoader负责通过网络下载Java类字节代码并定义出Java类。它的实现与FileSystemClassLoader类似。

    package classloader;  
      
    import java.io.ByteArrayOutputStream;  
    import java.io.InputStream;  
    import java.net.URL;  
      
    public class NetworkClassLoader extends ClassLoader {  
      
        private String rootUrl;  
      
        public NetworkClassLoader(String rootUrl) {  
            // 指定URL  
            this.rootUrl = rootUrl;  
        }  
      
        // 获取类的字节码  
        @Override  
        protected Class<?> findClass(String name) throws ClassNotFoundException {  
            byte[] classData = getClassData(name);  
            if (classData == null) {  
                throw new ClassNotFoundException();  
            } else {  
                return defineClass(name, classData, 0, classData.length);  
            }  
        }  
      
        private byte[] getClassData(String className) {  
            // 从网络上读取的类的字节  
            String path = classNameToPath(className);  
            try {  
                URL url = new URL(path);  
                InputStream ins = url.openStream();  
                ByteArrayOutputStream baos = new ByteArrayOutputStream();  
                int bufferSize = 4096;  
                byte[] buffer = new byte[bufferSize];  
                int bytesNumRead = 0;  
                // 读取类文件的字节  
                while ((bytesNumRead = ins.read(buffer)) != -1) {  
                    baos.write(buffer, 0, bytesNumRead);  
                }  
                return baos.toByteArray();  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
            return null;  
        }  
      
        private String classNameToPath(String className) {  
            // 得到类文件的URL  
            return rootUrl + "/"  
                    + className.replace('.', '/') + ".class";  
        }  
    }  
    

    在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用Java反射API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。我们使用接口的方式。示例如下:


    客户端接口:

    package classloader;  
      
    public interface Versioned {  
      
        String getVersion();  
    }   
    
    package classloader;  
      
    public interface ICalculator extends Versioned {  
      
        String calculate(String expression);  
    }  
    

    网络上的不同版本的类:

    package com.example;  
      
    import classloader.ICalculator;  
      
    public class CalculatorBasic implements ICalculator {  
      
        @Override  
        public String calculate(String expression) {  
            return expression;  
        }  
      
        @Override  
        public String getVersion() {  
            return "1.0";  
        }  
    } 
    
    package com.example;  
      
    import classloader.ICalculator;  
      
    public class CalculatorAdvanced implements ICalculator {  
      
        @Override  
        public String calculate(String expression) {  
            return "Result is " + expression;  
        }  
      
        @Override  
        public String getVersion() {  
            return "2.0";  
        }  
    }  
    

    在客户端加载网络上的类的过程:

    package classloader;  
      
    public class CalculatorTest {  
      
        public static void main(String[] args) {  
            String url = "http://localhost:8080/ClassloaderTest/classes";  
            NetworkClassLoader ncl = new NetworkClassLoader(url);  
            String basicClassName = "com.example.CalculatorBasic";  
            String advancedClassName = "com.example.CalculatorAdvanced";  
            try {  
                Class<?> clazz = ncl.loadClass(basicClassName);  // 加载一个版本的类  
                ICalculator calculator = (ICalculator) clazz.newInstance();  // 创建对象  
                System.out.println(calculator.getVersion());  
                clazz = ncl.loadClass(advancedClassName);  // 加载另一个版本的类  
                calculator = (ICalculator) clazz.newInstance();  
                System.out.println(calculator.getVersion());  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
    }
    

    六. 更多

    双亲委派模型是Java推荐的类加载模型,但违背该模型的案例有哪些?为什么会违背,又是怎么解决这种case的?这个将在《双亲委派模型与线程上下文类加载器》一文中进行介绍。


    ##引用:
    深入探讨 Java 类加载器
    深入理解Java类加载器(1):Java类加载原理解析

    展开全文
  • java类如何加载

    千次阅读 2018-11-06 14:19:04
    最近研究java的进阶知识,先从java类加载机制学起,原先看过《深入理解java虚拟机》这本书,奈何书的知识面太广及自身只看了一遍,很多知识并不是很理解,今天看了几篇别人讲解的java类加载机制,觉得讲的很好,帮助...

    最近研究java的进阶知识,先从java类加载机制学起,原先看过《深入理解java虚拟机》这本书,奈何书的知识面太广及自身只看了一遍,很多知识并不是很理解,今天看了几篇别人讲解的java类加载机制,觉得讲的很好,帮助很大,下面把别人的摘录过来,有时间多看看。

    什么是 Java 类加载机制?

    Java 虚拟机一般使用 Java 类的流程为:首先将开发者编写的 Java 源代码(.java文件)编译成 Java 字节码(.class文件),然后类加载器会读取这个 .class 文件,并转换成 java.lang.Class 的实例。有了该 Class 实例后,Java 虚拟机可以利用 newInstance 之类的方法创建其真正对象了。

    ClassLoader 是 Java 提供的类加载器,绝大多数的类加载器都继承自 ClassLoader,它们被用来加载不同来源的 Class 文件。

    类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

    其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。
    在这里插入图片描述

    1、加载
    简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class对戏那个存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。

    2、链接
    链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。
    1)、验证
    验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
    格式验证:验证是否符合class文件规范
    语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法视频被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
    操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过富豪引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
    2)、准备
    为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)
    被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值
    3)、解析
    将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
    可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)

    3、初始化
    将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。

    所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是方法,即类/接口初始化方法。该方法的作用就是初始化一个中的变量,使用用户指定的值覆盖之前在准备阶段里设定的初始值。任何invoke之类的字节码都无法调用方法,因为该方法只能在类加载的过程中由JVM调用。

    如果父类还没有被初始化,那么优先对父类初始化,但在方法内部不会显示调用父类的方法,由JVM负责保证一个类的方法执行之前,它的父类方法已经被执行。
    JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。

    Class 文件有哪些来源呢?

    上文提到了 ClassLoader 可以去加载多种来源的 Class,那么具体有哪些来源呢?

    首先,最常见的是开发者在应用程序中编写的类,这些类位于项目目录下;

    然后,有 Java 内部自带的核心类如 java.lang、java.math、java.io 等 package 内部的类,位于 $JAVA_HOME/jre/lib/ 目录下,如 java.lang.String 类就是定义在 $JAVA_HOME/jre/lib/rt.jar 文件里;

    另外,还有 Java 核心扩展类,位于 $JAVA_HOME/jre/lib/ext 目录下。开发者也可以把自己编写的类打包成 jar 文件放入该目录下;

    最后还有一种,是动态加载远程的 .class 文件。

    既然有这么多种类的来源,那么在 Java 里,是由某一个具体的 ClassLoader 来统一加载呢?还是由多个 ClassLoader 来协作加载呢?

    哪些 ClassLoader 负责加载上面几类 Class?

    实际上,针对上面四种来源的类,分别有不同的加载器负责加载。

    首先,我们来看级别最高的 Java 核心类,即$JAVA_HOME/jre/lib 里的核心 jar 文件。这些类是 Java 运行的基础类,由一个名为 BootstrapClassLoader 加载器负责加载,它也被称作 根加载器/引导加载器。注意,BootstrapClassLoader 比较特殊,它不继承 ClassLoader,而是由 JVM 内部实现;

    然后,需要加载 Java 核心扩展类,即 $JAVA_HOME/jre/lib/ext 目录下的 jar 文件。这些文件由 ExtensionClassLoader 负责加载,它也被称作 扩展类加载器。当然,用户如果把自己开发的 jar 文件放在这个目录,也会被 ExtClassLoader 加载;

    接下来是开发者在项目中编写的类,这些文件将由 AppClassLoader 加载器进行加载,它也被称作 系统类加载器 System ClassLoader;

    最后,如果想远程加载如(本地文件/网络下载)的方式,则必须要自己自定义一个 ClassLoader,复写其中的 findClass() 方法才能得以实现。

    因此能看出,Java 里提供了至少四类 ClassLoader 来分别加载不同来源的 Class。
    在这里插入图片描述

    不同加载器是如何工作的?什么是双亲委托模型及双亲委托存在的意义。

    String 类是 Java 自带的最常用的一个类,现在的问题是,JVM 将以何种方式把 String class 加载进来呢?

    我们来猜想下。

    首先,String 类属于 Java 核心类,位于 $JAVA_HOME/jre/lib 目录下。有的朋友会马上反应过来,上文中提过了,该目录下的类会由 BootstrapClassLoader 进行加载。没错,它确实是由 BootstrapClassLoader 进行加载。但,这种回答的前提是你已经知道了 String 在 $JAVA_HOME/jre/lib 目录下。

    那么,如果你并不知道 String 类究竟位于哪呢?或者我希望你去加载一个 unknown 的类呢?

    有的朋友这时会说,那很简单,只要去遍历一遍所有的类,看看这个 unknown 的类位于哪里,然后再用对应的加载器去加载。

    是的,思路很正确。那应该如何去遍历呢?

    比如,可以先遍历用户自己写的类,如果找到了就用 AppClassLoader 去加载;否则去遍历 Java 核心类目录,找到了就用 BootstrapClassLoader 去加载,否则就去遍历 Java 扩展类库,依次类推。

    这种思路方向是正确的,不过存在一个漏洞。

    假如开发者自己伪造了一个 java.lang.String 类,即在项目中创建一个包java.lang,包内创建一个名为 String 的类,这完全可以做到。那如果利用上面的遍历方法,是不是这个项目中用到的 String 不是都变成了这个伪造的 java.lang.String 类吗?如何解决这个问题呢?

    当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委托给它的父类加载器去执行,每一层的类都采用相同的方式,直至委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行加载。

    双亲委托模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。
    使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。

    使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委托给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种加载器环境中都是同一个类。相反,如果没有使用双亲委托模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。

    双亲委托模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委托的代码都集中在java.lang.ClassLoader的loadClass()方法中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载。

    类加载器的应用:自定义类加载器

    自定义类加载器,它允许我们在运行时可以从本地磁盘或网络上动态加载自定义类。这使得开发者可以动态修复某些有问题的类,热更新代码。

    自定义类加载器需要继承抽象类ClassLoader,实现findClass方法,该方法会在loadClass调用的时候被调用,findClass默认会抛出异常。不是loadClass()方法,因为ClassLoader提供了loadClass()(如上面的源码),它会基于双亲委托机制去搜索某个 class,直到搜索不到才会调用自身的findClass(),如果直接复写loadClass(),那还要实现双亲委托机制

    findClass方法表示根据类名查找类对象
    loadClass方法表示根据类名进行双亲委托模型进行类加载并返回类对象
    defineClass方法表示跟根据类的字节码转换为类对象

    https://www.cnblogs.com/xh_chiang/p/7575869.html
    https://www.cnblogs.com/xiaoxian1369/p/5498817.html

    展开全文
  • Java类加载的过程

    万次阅读 多人点赞 2018-09-14 19:09:22
    当我们调用 Java 命令运行某个 Java 程序时,该命令将会启动一条 Java 虚拟机进程,不管该 Java 程序有多么复杂,该程序启动了多少个线程,它们都处于该 Java 虚拟机进程里。同一个 JVM 的所有线程、所有变量都处于...

    JVM和类

    当我们调用 Java 命令运行某个 Java 程序时,该命令将会启动一条 Java 虚拟机进程,不管该 Java 程序有多么复杂,该程序启动了多少个线程,它们都处于该 Java 虚拟机进程里。同一个 JVM 的所有线程、所有变量都处于同一个进程里,它们都使用该 JVM 进程的内存区。当系统出现以下几种情况时, JVM 进程将被终止:

    • 程序运行到最后正常接收;
    • 程序运行到使用System.exit()或Runtime.getRuntime().exit()代码结束程序;
    • 程序运行中遇到未捕获的异常或错误结束;
    • 程序所在平台强制结束了JVM进程;

    类加载器就是寻找类或接口字节码文件进行解析并构造JVM内部对象表示的组件,在java中类装载器把一个类装入JVM,经过以下步骤:

    1、加载:查找和导入Class文件

    2、链接:其中解析步骤是可以选择的 (a)检查:检查载入的class文件数据的正确性 (b)准备:给类的静态变量分配存储空间 (c)解析:将符号引用转成直接引用

    3、初始化:对静态变量,静态代码块执行初始化工作

    类的加载过程

    当Java程序需要使用某个类时,如果该类还未被加载到内存中,JVM会通过加载、连接(验证、准备和解析)、初始化三个步骤来对该类进行初始化。

    类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:

    1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;

    2)如果类中存在初始化语句,就依次执行这些初始化语句。

    概述

    由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,如果该类还未被加载到内存中,Java虚拟机会通过加载、连接和初始化一个Java类, 使该类可以被正在运行的Java程序所使用。其中,加载(Loading)就是把类的.class文件读入Java虚拟机中; 而连接(Linking)就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去 。连接阶段分为三个子步骤——验证(Verification)、准备(Preparation)和解析(Resolution)。 验证步骤确保了Java类型数据格式正确并且适于Java虚拟机使用。而准备步骤则负责为该类型分配它所需的内存、比如为它的类变量分配内存。解析步骤则负责把常量池中的符号引用转换为直接引用。

    加载、 验证、准备和初始化这四个阶段的顺序是确定的,类的加载过程必须按照这种顺序接部就班地开始,而解析则不一定: 它在某些情况下可以在初始化阶段之后再开始, 这是为了支持 Java语言的运行时绑定 (也称为动态绑定或晩期绑定)。

    类初始化的时机

    在类和接口被加载和连接的时机上, Java虚拟机规范给实现提供了一定的灵活性 。但是它严格地定义了初始化的时机 。所有的Java虚拟机实现必须在每个类或接口首次主动使用时初始化 。下面这几种情形必须立即对类进行“初始化”:

    1)遇到 new、 getstatic、 putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化, 则需要先触发其初始化, 生成这4条指令的最常见的 Java代码场景是:

    • 使用 new关键字实例化对象的时候
    • 读取或设置一个类的静态字段的时候(即在字节码中,执行getstalic或putstatic指令时),被final修饰、已在编译期把结果放入常量池的静态字段除外
    • 调用一个类的静态方法的时候(即在字节码中执行invokestatic指令时)。

    2 ) 当调用Java API中的某些反射方法时, 比如类Class中的方法或者java.lang.reflect包的方法对类进行反射调用的时候, 如果类没有进行过初始化 , 则需要先触发其初始化。

    3 ) 当初始化一个类的时候, 如果发现其父类还没有进行过初始化, 则需要先触发其父类的初始化。

    4) 当虚拟机启动时, 用户需要指定一个要执行的主类(包合 main()方法的那个类) . 虚拟机会先初始化这个主类。

    5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

    对于这五种会触发类进行初始化的场景, 虚拟机规范中使用了一个很强烈的限定语:“有且只有 '', 这5种场景中的行为称为对一个类进行主动引用 。 除此之外,所有引用类的方式都不会触发初始化, 称为被动引用。

    类的加载

    加载是类加载过程的一个阶段,这两个概念一定不要混淆。在加载阶段, 虚拟机需要完成以下三件事情:

    1)通过一个类的全限定名来获取定义此类的二进制字节流。

    2 )将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

    3 ) 将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象, 作为方法区这个类的各种数据的访问入口。

    通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:

    • 从本地文件系统加载class文件;
    • 从一个ZIP、 JAR、 CAB或者其他某种归档文件中提取Java class文件,JDBC编程时使用到的数据库驱动就是放在JAR文件中,JVM可以直接从JAR包中加载class文件;
    • 通过网络加载class文件,这种场景最典型的应用就是 Applet;
    • 把一个java源文件动态编译、并执行加载
    • 运行时计算生成, 这种场景使用得最多的就是动态代理接术, 在 java.lang.reflect.Proxy中 , 就是用了 ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。

    类的连接

    当类被加载后,系统为之生成一个对应的Class对象,接着会进入连接阶段,连接阶段将会负责把类的二进制文件合并到JRE中。类连接分为如下三个阶段:

    • 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致;
    • 准备:准备阶段则负责为类的静态属性分配内存,并设置默认初始值;
    • 解析:将类的二进制数据中的符号引用替换成直接引用(符号引用是用一组符号描述所引用的目标;直接引用是指向目标的指针)

    验证

    验证是连接阶段的第一步, 这一阶段的目的是为了确保 Class文件的字节流中包含的信息符合当前虚拟机的要求, 井且不会危害虚拟机自身的安全。

    Java语言本身是相对安全的语言,但前面已经说过, Class文件并不一定要求用 Java源码编译而来, 可以使用任何途径, 包括用十六进制编译器直接编写来产生 Class 文件。在字节码的语言层面上, 上述 Java代码无法做到的事情都是可以实现的, 至少语义上是可以表达出来的。虚拟机如果不检査输入的字节流,对其完全信任的话, 很可能会因为载入了有害的字节流而导致系统崩溃 , 所以验证是虚拟机对自身保护的一项重要工作。从整体上看,验证阶段会完成下面四个阶段的检验过程: 文件格式验证、 元数据验证、 字节码验证、符号引用验证。

    1、文件格式验证

    第一阶段要验证字节流是否符合 Class文件格式的规范, 井且能被当前版本的虚拟机处理。这一阶段可能包括下面这些验证点:

    • 是否以魔数 0xCAFEBABE开头
    • 主、次版本号是否在当前虚拟机处理范围之内 。
    • 常量池的常量中是否有不被支持的常量类型(检査常量tag 标志)。
    • 指向常量的各种索引值中是否有指向不存在的常量或不符合装型的常量 。
    • CONSTANT_Utf8_info型的常量中是否有不符合 UTF8编码的数据
    • Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息

    实际上第一阶段的验证点还远不止这些, 上面这些只是从 HotSpot虚拟机源码中摘抄的一小部分而已。只有通过了这个阶段的验证之后, 字节流才会进入内存的方法区中进行存储, 所以后面的三个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

    2、元数据验证

    第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,这个阶段可能包括的验证点如下:

    这个类是否有父类(除了 java.lang.0bject之外,所有的类都应当有父类)

    这个类的父类是否继承了不允许被继承的类(被finaI修饰的类)

    如果这个类不是抽象类, 是否实現了其父类或接口之中要求实现的所有方法

    类中的字段、 方法是否与父类产生了矛盾(例如覆盖了父类的final字段, 或者出現不符合规则的方法重载, 例如方法参数都一致, 但返回值类型却不同等)

    第二阶段的验证点同样远不止这些,这一阶段的主要目的是对类的元数据信息进行语义检验, 保证不存在不符合 Java语言规范的元数据信息。

    3、字节码验证

    第三阶段是整个验证过程中最复杂的一个阶段, 主要目的是通过数据流和控制流的分析,确定语义是合法的。符号逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如:

    • 保证任意时刻操作数栈的数据装型与指令代码序列都能配合工作, 例如不会出现类似这样的情况:在操作栈中放置了一个 int类型的数据, 使用时却按long类型来加载入本地变量表中。
    • 保证跳转指令不会跳转到方法体以外的字节码指令上
    • 保证方法体中的类型转换是有效的, 例如可以把一个子类对象赋值给父类数据装型,这是安全的,但是把父类对象意赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、 完全不相干的一个数据类型, 则是危险和不合法的。

    即使一个方法体通过了字节码验证, 也不能说明其一定就是安全的。

    4、符号引用验证

    最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候 , 这个转化动作将在连接的第三个阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用) 的信息进行匹配性的校验, 通常需要校验以下内容:

    • 符号引用中通过字将串描述的全限定名是否能找到对应的类
    • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段 。
    • 符号引用中的类、字段和方法的访问性(private、 protected、 public、 default)是否可被当前类访问

    符号引用验证的目的是确保解析动作能正常执行, 如果无法通过符号引用验证, 将会抛出一个 java.lang.IncompatibleClassChangError异常的子类, 如 java.lang.IllegalAccessError、 java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

    对于虚拟机的装加载机制来说 ,验证阶段是一个非常重要的、 但不一定是必要的阶段(因为对程序没有影响)。如果所运行的全部代码(包括自己编写的以及第三方包中的代码)都已经被反复使用和验证过 , 那么在实施阶段就可以考虑使用一Xverify;none 参数来关闭大部分的验证措施, 以缩短虚拟机类加载的时间。

    准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配 。这个阶段中有两个容易产生混淆的概念需要强调一下, 首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中 。 其次,这里所说的初始值“通常情况”下是数据类型的零值。

    解析

    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程, 解新动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,分别对应于常量池的CONSTANT_Class_info、 CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_IntrfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info7种常量类型,解析阶段中所说的直接引用与符号引用关系如下:

    • 符号引用(Symlxiuc References):符号引用以一组符号来描述所引用的日标,符号可以是任何形式的字面量, 只要使用时能无歧义地定位到目标即可, 特号引用与配組机实现的内存1布.局11i-美 , 引用的日标并不一定已组加裁到内存中
    • 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的 , 同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同. 如果有了直接引用, 那引用的目标必定已经在内存中存在

    类的初始化

    初始化阶段是类加载过程的最后一步 , 前面的几个阶段, 除了在加载阶段用户应用程序可以通过自定 义类加载器參与之外, 其余动作完全由虚拟机主导和控制。到了初始化阶段, 才真正开始执行类中定义的 Java程序代码。从代码角度,初始化阶段是执行类构造器<clinit>()方法的过程。我们先看一下<clinit>()方法执行过程中可能会影响程序运行行为的特点和细节:

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

    类的初始化阶段主要是对类变量进行初始化,在Java类中对类变量指定初始值有两种方式:

    • 声明类变量时指定初始值
    • 使用静态初始化块为类变量指定初始值

    JVM初始化一个类一般包括如下几个步骤:

    1. 假如这个类还没有被加载和连接,程序先加载并连接该类;
    2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类;
    3. 假如类中有初始化语句,则系统依次执行这些初始化语句

    当执行第二步时,系统对直接父类的初始化也遵循此1、2、3步骤,如果该直接父类又有直接父类,系统再次重复这三步,所以JVM最先初始化的总是java.lang.Object类。

    参考:

    http://www.jb51.net/article/112006.htm

    https://blog.csdn.net/justloveyou_/article/details/72466416

     

     

    展开全文
  • Java类加载机制

    万次阅读 多人点赞 2019-03-02 10:21:04
    Java类加载机制 类加载的时机 隐式加载 new 创建类的实例, 显式加载:loaderClass,forName等 访问类的静态变量,或者为静态变量赋值 调用类的静态方法 使用反射方式创建某个类或者接口对象的Class对象。 初始化某个...
  • Java类和对象 详解(一)

    万次阅读 多人点赞 2016-10-06 20:48:02
    几乎现在的所有应用都以面向对象为主了,最早的面向对象的概念实际上是由IBM提出的,在70年代的Smaltalk语言之中进行了应用,后来根据面向对象的设计思路,才形成C++,而由C++产生了Java这门面向对象的编程语言。...
  • Java类加载,getClassLoader()

    万次阅读 多人点赞 2019-04-28 14:53:25
    转自 【java 加载的深入研究1】loadClass()的研究,IBM深入探讨 Java 加载器 加载器基本概念 顾名思义,加载器(class loader)用来加载 Java Java 虚拟机中。一般来说,Java 虚拟机使用 Java 的方式...
  • java类加载过程

    万次阅读 2019-03-10 11:39:38
    JVM将描述数据从.class文件中加载到内存,并对数据进行,解析和初始化,最终形成被JVM直接使用的Java类型。 从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七...
  • 2 Java虚拟机加载器结构简述 2.1 JVM三种预定义类型加载器 2.2 加载双亲委派机制介绍和分析 2.3 加载双亲委派示例 3 java程序动态扩展方式 3.1 调用java.lang.Class.forName(…)加载 3.2 用户自定义...
  • Java 之间的关系

    千次阅读 2019-04-28 20:58:11
    Java 之家的关系 一、继承关系 继承指的是一个(称为子类、子接口)继承另外的一个(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。 在Java中继承关系通过关键字extends明确标识,在设计时...
  • Java类的扩展

    千次阅读 2019-05-10 22:09:40
    ObjectJava中所有的公共父类,如果一个未明确地继承其他则一定是Object的子类。 Object的主要方法 方法名称 类型 描述 public Object() 构造 构造方法 public boolean equals(Object ...
  • JSP中调用java类中的方法 调用步骤 1、新建一个项目,在src文件夹下添加一个包:如:myjava 2、再在包中添加一个类: package myjava; public class myTestJava { public String getInfo(){ return "我是...
  • Java 变量和方法

    万次阅读 2018-05-22 14:16:35
    一、变量1、静态代码块 static{ } 静态代码块只会被执行一次;2、定义的时候,中变量创建在代码区,此时静态代码块会被自动执行;例子:package smm; public class LearnStatic { static int i=1; ...
  • java类加载器是什么?

    万次阅读 多人点赞 2019-07-30 15:23:10
    加载器是有了解吗?...一般来说,Java 虚拟机使用 Java 的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。 加载器负责读取 Java ...
  • Java 的成员有哪些?

    千次阅读 2019-05-29 10:13:35
    的成员有5:属性、方法、构造器、代码块、内部 :具有相同属性(特征)和行为(动作)对象的集合,是抽象出来的。 属性:描述类型中的的特征(未完待续…) ...
  • Java类加载过程图解

    万次阅读 2018-08-22 16:52:36
    朋友给我发了一道有意思的题目,如下 ...上图是我绘制的整个Java类加载过程。 首先是编译期,将Java源文件也就是敲好的代码通过编译,转换成.class文件,也就是字节码文件(byte),然后经过传输传...
  • https://blog.csdn.net/qq_35246620/article/details/77686098
  • @java类的加载顺序 Java类的加载顺序 父类静态代变量、 父类静态代码块、 子类静态变量、 子类静态代码块、 父类非静态变量(父类实例成员变量)、 父类构造函数、 子类非静态变量(子类实例成员变量)、 子类构造...
  • Java类的声明详解

    万次阅读 多人点赞 2018-06-20 17:50:20
    本文重点介绍在JAVA的声明。一、定义 可以理解为一个模版,它描述一对象的行为和状态。二、声明语法 [修饰符] [static] [final] [abstract] [strictfp] class 类名 [extends 父类名] [implements 接口名...
  • Java类的定义、声明及使用

    千次阅读 2020-03-01 23:15:22
    一、的声明 在使用前,必须先声明,然后才...名称遵循标识符命名规则与规范,这里不做详细讲解,有兴趣的可以去看看https://blog.csdn.net/MillionSong/article/details/104601811(Java命名规则和规范)。...
  • 普通java类使用@Autowired

    千次阅读 2019-03-23 12:27:07
    @Component //申明为spring组件
  • Java类的卸载机制

    万次阅读 多人点赞 2018-05-28 23:21:29
    的生命周期 当Sample被加载、连接和初始化后,它的生命周期就开始了。 当代表Sample的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,Sample在方法区内的数据也会被卸载,从而结束Sample...
  • Spring基于Java类配置Bean(一)

    千次阅读 2018-05-14 14:53:56
    之前可能使用最多的可能就是基于注解的配置方式了,基于XML的配置方式太多臃肿,而Java类的配置方式其实在一定程度上取代了xml的配置方式,特别实在SpringBoot中已经完全采用了Java类的配置方式。Java类的配置方式...
  • Java import以及Java类的搜索路径

    千次阅读 2018-05-25 10:15:29
    如果你希望使用Java包中的,就必须先使用import语句导入。import语句与C语言中的 #include 有些类似,语法为: import package1[.package2…].classname;package 为包名,classname 为类名。例如:1. import ...
  • Java 之间的调用

    万次阅读 多人点赞 2018-06-08 17:43:36
    例如://先定义一个import static java.lang.System.out; public class Son { public void IsSon() { out.append("调用Son中的IsSon成员方法成功!"); } } //实例化,调用import java.util...
  • java类中代码执行顺序

    万次阅读 2018-08-20 15:58:52
    java类中代码执行顺序  首先,没有学java或者不知道类继承的,可以不用浪费时间乐,直接左上角,看多了有害。  java的代码执行顺序是自上而下,从左到右,方法是以main方法为主方法(通常情况下,事实上很...
  • java类已存在,但IDEA提示找不到类

    千次阅读 热门讨论 2020-06-05 15:10:52
    有时已存在一个java类,但是IDEA提示没有找到,重启也没用,这个算是IDEA的一个小bug吧 解决方案 这个是因为缓存的原因,清理缓存,重启即可,点击如下选项 File —> Invalidate Caches/Restart… ...
  • JAVA 和对象的实例

    万次阅读 多人点赞 2019-01-15 11:01:23
    JAVA 和对象的实例 什么是? 1. 是具有相同的属性和功能的事物的抽象的集合,在面向对象程序设计中,人们把一事物的静态属性和动态可以执行的操作组合在一起就得到这个概念。 2. 是个抽象的概念,用来...
  • 秒懂java类路径

    万次阅读 2018-06-22 18:15:54
    java -cp 路径 全限定的类名 参数1 参数2 参数3 在上面的调用中,初学者可能会在两个地方掉进坑中: 1.java命令:在windows上,它是没有显示地写上exe后缀的可执行程序。大家都知道在计算机中,要指明一个文件,仅...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,808,270
精华内容 1,923,308
关键字:

java类

java 订阅