java是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由Sun Microsystems公司于1995年5月推出的Java程序设计语言和Java平台(即JavaEE, JavaME, JavaSE)的总称。本站提供基于Java框架struts,spring,hibernate等的桌面应用、web交互及移动终端的开发技巧与资料

保持永久学习的心态,将成就一个优秀的你,来 继续搞起java知识。

JVM和类

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

1.程序运行到最后正常结束。

2.程序运行到使用System.exit()或Runtime.getRuntime().exit()代码处结束程序。

3.程序执行过程中遇到未捕获的异常或错误而结束。

4.程序所在平台强制结束了JVM进程。

类的加载

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或类初始化。类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

类的加载

类的加载有类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。通过使用不同的类加载器,可以从不同出自加载类的二进制数据,通常有如下几种出自。

1.从本地文件系统加载class文件,这是绝大部分程序的类加载方式。

2.从JAR包加载class文件,这种方式是很常见的,例如JDBC编程中用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。

3.通过网络加载class文件。

4.把一个Java源文件动态编译,并执行加载。

类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

类的连接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下三个阶段: 1.验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。

2.准备:类准备阶段则负责为类变量分配内存,并设置默认初始值。

3.解析:将类的二进制数据中的符号引用替换成直接引用。

类的初始化

在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对类变量进行初始化。在Java类中对类变量指定初始化值有两种方式: 1.声明类变量时指定初始值;

2.使用静态初始化块为类变量指定初始值。

例如:

1public class Test {
    // 声明变量a时指定初始值
    static int a = 5;
    static int b;
    static int c;
    static {
        // 使用静态初始化块为变量b指定初始值
        b = 6;
    }
}

对于上面代码,程序为类变量a、b都显示指定了初始值,所以这两个类变量的值分别为5、6,但类变量c则没有指定初始值,它将采用默认值0。

声明变量时指定初始值,静态初始化块都将被当成类的初始化语句,JVM会按这些语句在程序中的排列顺序依次执行他们,例如下面的类。

1public class Test {
    static {
        // 使用静态初始化块为变量b指定初始值
        b = 6;
        System.out.println("---------");
    }
    // 声明变量a时指定初始值
    static int a = 5;
    static int b = 9;
    static int c;
    public static void main(String[] args) {
        System.out.println(Test.b);
    }
}

上面代码先在静态初始化块中为b变量赋值,此时类变量b的值为6;接着程序向下执行,执行到

1static int b = 9;

这行代码也属于该类的初始化语句,所以程序再次为类变量b赋值。也就是说,当Test类初始化结束后,该类的类变量b的值为9.

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

1.假如这个类没有被加载和链接,则程序先加载并连接该类。

2.假如该类的直接父类还没有初始化,则先初始化其直接父类。

3.假如类中有初始化语句,则系统依次执行这些初始化语句。

所以JVM最先初始化的总是java.lang.Object类。

类初始化的时机

当Java程序首次通过下面6中方式来使用某个类或接口时,系统就会初始化类或接口。 1.创建类的实例。为某个类创建实例的方式包括:使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。

2.调用某个类的类方法(静态方法)。

3.访问某个类或接口的类变量,或未该类变量赋值。

4.使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。例如代码Class.forName(“Person”),如果系统还未初始化Person类,则这行代码将会导致该Person类被初始化,并返回Person类对应的java.lang.Class对象。

5.初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化。

6.直接使用java.exe命令来运行某个主类。当运行某个主类时,程序会预先初始化该主类。

除此之外,下面的几种情形需要特别指出:

1.对于一个final型的变量,如果该类变量的值在编译时就可以确定下来,那么这个类变量相当于“宏变量”。Java编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态类变量,也不会导致该类的初始化。

如果final修饰的类变量的值不能再编译时确定下来,则必须等到运行时才可以确定该类变量的值,如果通过该类来访问它的类变量,则会导致该类被初始化。

2.当使用ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class的forName()静态方法才会导致强制初始化该类。

类加载器

类加载器负责将.class文件(可能在磁盘上,也有可能在网路上)加载到内存中,并为之生成对应的java.lang.Class对象。

类加载器简介

类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。那么系统式怎么识别出算是“同一个类”呢?

正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其权限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为唯一标识。

例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例k1负责加载,则该Person类对应的Class对象在JVM中表示为(Person、pg、k1)。这意味着两个类加载器加载的同名类:(Person、pg、k1)和(Person、pg、k2)是不同的、它们所加载的类也是完全不同的、互不兼容的。

当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构。

Bootstrap ClassLoader:根类加载器。

Extension ClassLoader:扩展类加载器。

System ClassLoader:系统类加载器。

Bootstrap ClassLoader被称为引导(也称为原始或根)类加载器,它负责加载Java的核心类。

Extension ClassLoader被称为扩展类加载器,它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中JAR包的类。

System ClassLoader被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定得JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()方法来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以类加载器作为父加载器。

类加载机制

JVM的类加载机制主要由如下三种。 1.全盘负责。所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所以来的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

2.父类委托。所谓父类委托,则是先让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

3.缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

注意:类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系式类加载器实例之间的关系。

除了可以使用Java提供的类加载器之外,开发者也可以实现自己的类加载器,自定义的类加载器通过继承ClassLoader来实现。JVM中这4种类加载器的层次结构从下到上依次是:用户类加载器 -> 系统类加载器 -> 扩展类加载器 -> 根类加载器。

下面我们尝试访问JVM的类加载器。代码如下:

1    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 extensinLoader = systemLoader.getParent();
        System.out.println("扩展类加载器:" + extensinLoader);
        System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
        System.out.println("扩展类加载器的parent:" + extensinLoader.getParent());
    }

运行上面的程序,会看到如下运行结果:

1系统类加载器:sun.misc.Launcher$AppClassLoader@73d16e93
file:/Users/Lyong/Documents/IDE/workspace/Test/bin/
扩展类加载器:sun.misc.Launcher$ExtClassLoader@15db9742
扩展类加载器的加载路径:/Users/Lyong/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
扩展类加载器的parent:null

从上面运行结果可以看出,系统类加载器的加载路径是程序运行的当前路径,扩展类加载器的加载路径是jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext,因为我这里的环境是Mac,不同的操作系统结果可能不同,但都应该是jre/lib/ext。但此处看到扩展类加载器的父加载器是null,并不是根类加载器。这是因为根类加载器并没有继承ClassLoader抽象类,所以扩展类加载器的getParent()方法返回null。但实际上,扩展类加载器的父类加载器是根类加载器,只是根类加载器并不是Java实现的。

从运行结果可以看出,系统类加载器是AppClassLoader的实例,扩展类加载器是ExtClassLoader的实例。实际上,这两个类都是URLClassLoader类的实例。

类加载器加载Class的步骤

类加载器加载Class大致要经过如下8个步骤:1.检查此Class是否载入过(即在缓存区中是否有此Class),如果有则直接进入第8不,否则接着第2步。

2.如果父类加载器不存在(如果没有父类加载器,则要么parent一定是根类加载器,要么本身就是根类加载器),则跳到第4步执行;如果父类加载器存在,则接着执行第3步。

3.请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步。

4.请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步。

5当前类加载器尝试寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步。

6.从文件载入Class,成功载入后跳到第8步。

7.抛出ClassNotFoundException异常。

8.返回对应的java.lang.Class对象。

其中,第5、6步允许重写ClassLoader的findClass()方法来实现自己的载入策略,甚至重写loadClass()方法来实现自己的载入过程。

创建并使用自定义的类加载器

JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。查阅API文档中关于ClassLoader的方法不难发现,ClassLoader中包含了大量的protected方法——这些方法都可以被子类重写。ClassLoader类有如下两个关键方法。

1.loadClass(String name, boolean resolve):该方法为ClassLoader的入口点,根据指定名称来加载类,系统就是调用ClassLoader的该方法获取指定类对应的Class对象。

2.findClass(String name):根据指定名称来查找类。

如果需要实现自定义的ClassLoader,则可以通过重写以上两个方法来实现,通常推荐重写findClass()方法,而不是重写loadClass()方法。

在ClassLoader里还有一个核心方法:Class defineClass(String name,byte[] b,int off,int len),该方法负责将指定类的字节码文件(即Class文件,如Helllo.class)读入字节数组byte[] b内,并把它转换为Class对象,该字节码文件可以出自于、网络等。此方法为final方法,不可重写。

关于ClassLoader还有一些普通方法,大家可以通过查阅API来了解。这里就不一一讲解了。

下面我们自定义一个ClassLoader,并重写findClass()方法来实现自定义的类加载机制。这个ClassLoader可以在加载类之前编译该类的源文件,从而实现运行Java之前先编译该程序的目标,这样即可通过该ClassLoader直接运行Java源文件。

1package com.lyong.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class CompileClassLoader extends ClassLoader {

    /**
     * 读取一个文件的内容
     * @param fileName 文件名
     * @return 文件内容
     * @throws IOException 异常
     */
    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文件的方法
     * @param javaFile Java文件名
     * @return
     */
    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 e) {
            e.printStackTrace();
        }
        // 获取javac线程的退出值
        int ret = p.exitValue();
        // 返回编译成功
        return ret == 0;
    }

    /**
     * 重写ClassLoader的findClass方法
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> clazz = null;
        // 将包含路径中的(.)换成斜线(/)
        String fileStub = name.replace(".", "/");
        String javaFileName = fileStub + ".java";
        String classFileName = fileStub + ".class";
        File javaFiel = new File(javaFileName);
        File classFile = new File(classFileName);
        //当制定Java源文件存在,且Class不存在,或者Java源文件的修改时间比Class文件的修改时间更晚时,重新编译
        if(javaFiel.exists() && (!classFile.exists()) || javaFiel.lastModified() > classFile.lastModified()) {
            try {
                // 如果编译失败,或者该Class文件不存在
                if(!compile(javaFileName) || !classFile.exists()) {
                    throw new ClassNotFoundException("ClassNotFoundException:" + javaFileName);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // 如果class文件存在,系统负责将该文件转换成Class对象
        if(classFile.exists()) {
            try {
                // 将Class文件的二进制数据读入数组
                byte[] raw = getBytes(classFileName);
                // 调用ClassLoader的defineClass方法将二进制数据转换成Class对象
                clazz = defineClass(name, raw, 0, raw.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //如果clazz为null,表明加载失败,则抛出异常
        if(clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

    public static void main(String[] args){
        // args = new String[]{"com.lyong.test.Test"};
        // 如果运行该程序时没有参数,既没有目标类
        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;
        try {
            clazz = ccl.loadClass(progClass);
            // 获取要运行的类的主方法
            Method main = clazz.getMethod("main", (new String[0]).getClass());
            Object argsArray[] = {progArgs};
            main.invoke(null, argsArray);
        } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

}

上面程序中重写了findClass()方法,通过重写该方法就可以实现自定义的类加载机制。。在本类的findClass()方法中线检查需要加载类的Class文件是否存在,如果不存在则先编译源文件,在调用ClassLoader的defineClass()方法来加载这个Class文件,并生成相应的Class对象。

使用自定义的类加载器,可以实现如下常见功能:

1.执行代码前自动验证数字签名。

2.根据用户提供的密码解密代码,从而可以实现代码混淆器来避免反编译*.class文件。

3.根据用户需求来动态地加载类。

4.根据应用需求吧其他数据以字节码的形式加载到应用中。

URLClassLoader类

Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类(此处的父类,就是指类与类之间的继承关系)。URLClassLoader功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。 在应用程序中可以直接使用URLClassLoader加载类,URLClassLoader类提供了如下两个构造器:

1.URLClassLoader(URL[] urls):使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询并加载类。

2.URLClassLoader(URL[] urls,ClassLoader parent):使用指定的类加载器创建一个ClassLoader对象,其他功能与前一个构造器相同。

一旦得到了URLClassLoader对象之后,就可以调用该对象的laodClass()方法来加载指定的类。

创建URLClassLoader时传入了一个URL数组参数,该ClassLoader就可以从这系列URL指定的资源中加载指定类,这里的URL可以以file:为前缀,表明从本地文件系统加载;可以以http:为前缀,表明从互联网通过HTTP访问来加载;也可以以ftp:为前缀,表明从互联网通过FTP访问来加载……功能非常强大。

虚拟机classjava

因为水平有限,难免有疏忽或者不准确的地方,希望大家能够直接指出来,我会及时改正。一切为了知识的分享。

后续会有更多的精彩的内容分享给大家。