Java 基础知识整理 (待整理)
Posted liuxm-刘小明
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 基础知识整理 (待整理)相关的知识,希望对你有一定的参考价值。
JVM之类加载器(ClassLoader)基本介绍
类加载器用于将class文件加载到JVM中去执行。下面介绍类加载器涉及到的基本概念和加载基本过程。
一、Java虚拟机与程序的生命周期
在运行Java程序时,会启动JVM进程,该进程中会使用一个线程去执行我们的Java程序。在如下几种情况下,Java虚拟机将结束生命周期:
1.执行了System.exit(0)(内部调用了Runtime.getRuntime().exit(n)),如果是非0参数表示异常退出。
2.程序正常结束
3.程序在执行过程中遇到了异常或错误而导致异常终止。如果异常没有try-catch而一直throw,那么一直throw到main方法,
main方法还不处理的化那就会throw给JVM,JVM看到异常就会结束运行。
4.由于操作系统错误而导致的Java虚拟机进程终止
二、类的加载、连接与初始化
加载:查找并加载类的二进制数据到JVM
连接:分为三个步骤
Ø 验证:确保被加载类的正确性
Ø 准备:为类的静态变量分配内存,并将其初始化为默认值
Ø 解析:把类中的符号引用转换为直接引用
初始化:为类的静态变量赋予正确的初始值。(就是在类中定义变量时赋予的默认值)
以上的过程中并不会初始化对象变量,因为此时并不存在任何对象,只是将类的class文件数据加载到内存中,对象的生成是在该过程完成之后。
类中的静态代码块是按照声明的顺序执行的,例如:
static int a;
static {
a=1;
}
static{
a=2;
}
此时a的最终值是2,如果两个代码块颠倒顺序,那么a的最终值就是1。
三、类的主动使用与被动使用
Java程序中对类的使用可以分为两种:主动使用 和 被动使用。
1.Java程序对类的主动使用的情况
> 创建类的实例
> 访问某个类或接口的静态变量,或者对该静态变量赋值
> 调用类的静态方法
> 通过反射访问类,例如Class.forName(“package.ClassName”)
> 初始化一个类的子类,或访问子类的静态变量或静态方法也是对父类的主动使用。
> Java虚拟机启动时被标注为启动类的类,例如JUnit中的TestCase类。
所有的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时才初始化它们。因此Java虚拟机只有在一个类第一次
发生上面的六种情况时才会初始化该类,仅第一次主动使用时初始化。
除了以上的主动使用的情况,其它的情况都是对类的被动使用,因此都不会导致类的初始化。
类的加载、连接与初始化过程的详细分析(上)
一、类加载阶段
1.类加载方式
类的加载指的是将类的.class文件的二进制数据读入内存中,将其放在运行时数据区的方法区内。然后在堆区创建一个Java.lang.Class对象,
用来封装类在方法区内的数据结构,该对象是由JVM在加载类时创建的。所以每个类都会对应一个Class类型的对象,通过getClass()来获取,
并且无论生成该类的多个少对象,其Class类型的对象只有一个。Class类的构造方法是私有的,并且Class类的对象只有JVM才能创建,创建时机为加载.class文件时。
因此Class类是整个反射的入口,因为每个类都会在内存中对应一个描述它的Class类型的对象,使用这个对象就可以获取到目标类所关联的class文件中的数据结构。
类的加载有以下几种方式(加载.class文件的方式)
Ø 从本地文件系统直接加载
Ø 通过网络下载.class文件(java.NET.URLClassLoader)
Ø 从zip、jar等归档文件中加载 .class文件
Ø 从专有的数据库中提取 .class文件
Ø 将Java源文件动态编译为.class文件
类加载的最终产物就是位于堆区中的Class对象(注意此时并没有被加载类的对象存在,刚刚加载类)。Class对象封装了类在方法区中的数据结构,
并且向Java程序员提供了访问方法区内的数据结构的接口,这些接口就是Java反射的相关类和方法。
1.类加载器
有两种类型的类加载器
(1).Java虚拟机自带的类加载器,其中包括以下三种类加载器
Ø 根类加载器(Bootstrap ClassLoader)
Ø 扩展类加载器(Extension ClassLoader)
Ø 系统类加载器(System ClassLoader),又称为应用类加载器
其中第一种类加载器是JVM最底层的类加载器,由C++编写,因此我们无法访问到根类加载器。后面两种其实是基于第一种的,由Java语言实现的类加载器。
(2).用户自定义的类加载器,可以定义加载的方式,加载时机以及加载过程中做一些事情。
使用java.lang.ClassLoader的子类,通过ClassLoader来实现自定义的类加载器,这个过程中可以定义类的加载方式,加载时机以及加载过程中做一些事情。
通过给定ClassLoader一个类的名称,它会将其作为一个文件名称试图去读取该文件内容并根据内容组装一个类的描述。每个类对象其实都包含了一个对定义它的
那个ClassLoader的一个引用,因为任何类都是由类加载器加载的,因此通过该类就可以访问到对应的类加载器。
通过类对象的getClass().getClassLoader()或类的class属性的getClassLoader()就可以获取到类所对应的类加载器,也就是加载该类的哪个类加载器。
getClassLoader()可能返回一个null,那么就代表该类的类加载器是根类加载器(Bootstrap ClassLoader);换句话说,如果一个类是有根类加载器加载的,
那么就无法获取到该类加载器,此时getClassLoader()返回null,因为根类加载器是使用C++编写的,我们无法在程序中访问它。例如String等类就是由根类
加载器加载的,因此String.class.getClassLoader()将返回null。
对于JDK内置的类一般都是由根类加载器加载的,因此通过它们调用getClassLoader()返回null;自定义的类一般通过
sun.misc.Launcher$AppClassLoader加载,通过输出getClassLoader()结果即可看到。
对于JDK动态代理的InvocationHandler类的invoke方法,第一个参数是一个ClassLoader类型,就是用于动态的加载第二个参数所传递的类,
然后会根据所加载的类动态的创建出所加载类的对象,然后根据该对象创建出一个该对象的代理对象。
3.类加载时机
类加载器并不需要等到某个类被主动使用时才加载它。这与类的初始化不同,上面说过JVM必须在每个类或接口被Java程序首次主动使用时才初始化它们,
注意加载与初始化的却别!
JVM规范允许类加载器在预料到某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件不存在或有错误,此时并不会报错,
而是类加载器必须等到在程序首次使用该类时才报告错误,这种错误类型为LinkageError。因此如果这个类加载后一直没有被主动使用,
那么类加载器就一直不会报告错误。
类被加载后就进入了连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中。所谓的数据合并,因为编译后的每个class文件
在硬盘中都是独立的,但是每个class之间可能存在引用关系以及方法之间存在调用关系,此时就需要根据它们之间的关系将这些class数据合并在一起放入运行时环境中。
类的加载、连接与初始化过程的详细分析(中)
1.类的验证
类验证除了包装类的可用,还为了包装安全性,防止构件出自定义的类来侵入系统。
类验证所要完成的功能:
Ø 类文件结构的检查
确保类文件遵从Java类文件的固定格式
Ø 语义检查
确保类本身符合Java语言的语法规定,比如验证final类型的类有无子类,以及final类型的方法是否被覆盖或重写。
Ø 字节码验证
确保字节码流可以被Java虚拟机安全地执行。字节码流代表Java方法(包括静态和实例方法),它是由被称作操作码的单字节指令组成的序列,
每个操作码后都跟着一个或多个操作数。字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。
Ø 二进制兼容性的验证
确保相互引用的类之间协调一致。例如在Worker类的gotoWork()方法中会调用Car类的run()方法,此Java虚拟机验证验证Worker类的时候会
检查方法区内是否存在Car类的run()方法,如果不存(当Worker类和Car类的版本不兼容就会出现该这个问题,例如低版本JRE编译class的到高版本JRE中运行)
在就会抛出NoSuchMethodError错误。
2. 类连接之准备阶段
在准备阶段,Java虚拟机为类的静态变量分配内存空间并设置默认的初始值,注意不是程序中=赋值的哪个值,而是Java对象变量的默认值。
例如,对于以下的Simple类,在准备阶段为int类型的静态变量a分配4个字节的内存空间,并赋予默认值0;为long类型的变量b分配8个字节的内存空间,并赋予默认值0。
public class Simple {
private static int a=1;
private static long b;
static { b=2;}
}
3.类连接之解析阶段
在解析阶段,Java虚拟机会把类的二进制数据中的符号引用替换为直接引用。例如Worker类中gotoWork()方法中会引用Car类的run()方法:
public void gotoWork(){
car.run(); //这段代码在Worker类的二进制数据中表示为符号引用
}
在Worker类的二进制数据中,包含了一个对Car类的run()方法的符号引用,它由run()方法的全名和相关描述符组成。在解析阶段,Java虚拟机会
把这个符号引用替换为一个指针,该指针指向Car类的run()方法在方法区内的内存位置,这个指针就是直接引用。
类的加载、连接与初始化过程的详细分析(下)
1.类的初始化时机
类连接阶段的解析步骤完成后就进入了类的初始化阶段,并且只有主动使用类时才会执行初始化。在初始化阶段,Java虚拟机执行类的初始化语句,
为类的静态变量赋予初始值(程序中使用赋值语句所赋予的值)。
在程序中,静态变量的初始化有两种途径:
> 在静态变量的声明处进行初始化
> 在静态代码块中进行初始化,静态代码块也是在类加载后的这个初始化阶段被执行的。
例如下面的代码中,变量a、b被显式初始化,而变量c没有显式初始化,其保持默认值0
public class Sample {
private static int a=1; //声明时初始化静态变量
private static int b;
private static int c;
static {
b=2; //在静态代码块中初始化静态变量
}
}
2.类的初始化步骤:
(1) 如果类没有被加载和连接,那么就先执行加载和连接过程
(2) 如果类存在父类并且父类没有被初始化,那么就先初始化父类,一直初始化类继承结构到Object
(3) 如果类存在初始化语句,例如赋值语句或静态代码块,那么就执行这些初始化语句。
3.接口的初始化时机
当JVM初始化一个类时,要求它的所有父类都已经被初始化,但是这个规则不适用于接口。
(1) 在初始化一个类时,并不会先初始化它所实现的接口;
(2) 初始化一个接口时,并不会先初始化它所继承的父接口
因此,一个父接口并不会因为它的子接口或实现类的初始化而被初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
只有当程序访问的静态变量或静态方法确实在当前类或接口中定义时,才会认为是对类或接口的主动使用。
调用ClassLoader的loadClass方法加载一个类,并不设置对类主动使用的六种情况,因此也不会初始化。
Class.forName和ClassLoader.loadClass区别
Java中class是如何加载到JVM中的:
1.class加载到JVM中有三个步骤
装载:(loading)找到class对应的字节码文件。
连接:(linking)将对应的字节码文件读入到JVM中。
初始化:(initializing)对class做相应的初始化动作。
2.Java中两种加载class到JVM中的方式
2.1:Class.forName("className");
其实这种方法调运的是:Class.forName(className,
true, ClassLoader.getCallerClassLoader())方法
参数一:className,需要加载的类的名称。
参数二:true,是否对class进行初始化(需要initialize)
参数三:classLoader,对应的类加载器
2.2:ClassLoader.laodClass("className");
其实这种方法调运的是:ClassLoader.loadClass(name,
false)方法
参数一:name,需要加载的类的名称
参数二:false,这个类加载以后是否需要去连接(不需要linking)
2.3:两种方式的区别
forName("")得到的class是已经初始化完成的
loadClass("")得到的class是还没有连接的
一般情况下,这两个方法效果一样,都能装载Class。
但如果程序依赖于Class是否被初始化,就必须用Class.forName(name)了。
3.举例说明他们各自的使用方法
java使用JDBC连接数据库时候,我们首先需要加载数据库驱动。
Class.forName("com.MySQL.jdbc.Driver");//通过这种方式将驱动注册到驱动管理器上
Connection conn =
DriverManager.getConnection("url","userName","password");//通过驱动管理器获得相应的连接
查看com.mysql.jdbc.Driver源码:
public class Driver extends
NonRegisteringDriver
implements java.sql.Driver
{
//注意,这里有一个static的代码块,这个代码块将会在class初始化的时候执行
static
{
try
{
//将这个驱动Driver注册到驱动管理器上
DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can\'t register
driver!");
}
}
}
Class.forName("com.mysql.jdbc.Driver")方法以后,他会进行class的初始化,执行static代码块。
也就是说class初始化以后,就会将驱注册到DriverManageer上,之后才能通过DriverManager去获取相应的连接。
但是要是我们使用ClassLoader.loadClass(com.mysql.jdbc.Driver)的话,不会link,更也不会初始化class。
相应的就不会回将Driver注册到DriverManager上面,所以肯定不能通过DriverManager获取相应的连接。
Spring容器启动过程
一、一切从手动启动IoC容器开始
- ClassPathResource resource = new ClassPathResource("bean.xml");
- DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
- XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
- reader.loadBeanDefinitions(resource);
第一行代码:ClassPathResource的作用是起到了资源定位的作用。通常情况下,spring的配置信息使用文件来描述,通过这样一行代码,指明了需要加载的资源的位置,并且使用Spring能够理解的Resource接口的形式将资源描述出来。
第二行代码:DefaultListableBeanFactory是一个纯粹的IoC容器类,它是这个Spring的一个基础的IoC容器类,其他的一个IoC容器都是以这个类为基础进行扩展的。这样代码只是定义了一个IoC容器,它不具有任何其他的能力。
第三行代码:XmlBeanDefinitionReader是一个配置文件读取器。它实现了BeanDefinitionReader接口,它能够按照Spring配置文件,将其中的配置信息转换为spring内部可以识别的信息(BeanDefinition)。注意,在这里它的构造函数需要一个BeanDefinitionRegistry类型的参数,BeanDefinitionRegistry接口提供了一个回调函数,通过这个回调函数可以向IoC容器注册bean的定义信息。DefaultListableBeanFactory实现了这个接口。
第四行代码:调用loadeBeanDefinitions方法,通过给定的Resource资源,从中读取出spring的配置信息,转换为BeanDefinition,然后再调用BeanDefinitionRegistry的回调函数进行注册。
通过以上的四行代码,完成了spring容器的启动。
二、容器启动过程
1. 定位
在spring中,使用统一的资源表现方式Resource。根据不同的情况进行不同的选择。上述程序中,采用了编程式的资源定位方法,使用ClassPathResource定位位于classpath下的资源文件。
2. 加载
在加载这个过程中,主要工作是读取spring配置文件,解析配置文件中的内容,将这些信息转换成为Spring内容可以理解、使用的BeanDefinition。
3. 注册
加载过配置文件后,就将BeanDefinition信息注册到BeanDefinitionRegistry中,通常情况下Spring容器的实现类都实现这个接口。
SprinMVC工作流程描述
1. 用户向服务器发送请求,请求被Spring 前端控制Servelt DispatcherServlet捕获;
2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回;
3. DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。(附注:如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler(...)方法)
4. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。 在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:
HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
数据转换:对请求消息进行数据转换。如String转换成Integer、Double等
数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
5. Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象;
6. 根据返回的ModelAndView,选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet ;
7. ViewResolver 结合Model和View,来渲染视图
8. 将渲染结果返回给客户端。
Spring工作流程描述
为什么Spring只使用一个Servlet(DispatcherServlet)来处理所有请求?
详细见J2EE设计模式-前端控制模式
Spring为什么要结合使用HandlerMapping以及HandlerAdapter来处理Handler?
符合面向对象中的单一职责原则,代码架构清晰,便于维护,最重要的是代码可复用性高。如HandlerAdapter可能会被用于处理多种Handler。
ContextLoaderListener与 DispatcherContext 关系
DispatcherContext加载的时候会以ContextLoaderListener加载的spring context容器作为parent context容器,
问题?
web初始化
以上是关于Java 基础知识整理 (待整理)的主要内容,如果未能解决你的问题,请参考以下文章