Java是一门面向对象的编程语言。

面向对象以抽象为基础,有封装、继承、多态三大特性。

宇宙万物,经过抽象,均可归入相应的种类。不同种类之间,有着相对井然的分别。

Java中的类,便是基于现实世界中的类别抽象出来的。

类本身表示一类事物,是对这类事物共性的抽象与封装。类封装了一类事物的属性和方法。

类与类之间,有着不同的层级。

以生物界中的分类为例,遵循“界门纲目科属种”的级别体系,人类(亦可称为“人种”)的层级体系是:动物界---脊索动物门---哺乳纲---灵长目---人科---人属---人种。

从人种到动物界,依次继承父类的共有属性和方法,而且又独具形态。

举例来说,动物都需要吃东西来维持生命所需的能量,同是吃东西,不同种类的动物各有特点。

又譬如,动物界与植物界的一个关键区别是,能否移动。在动物界之中,都是移动,但是各子类的移动方式几乎互不相同。

举例来说,人通过走路、奔跑、攀爬等来移动,鸟通过飞翔、两下肢等来移动,鱼则通过在水中漂游来移动等。这使得动物的移动功能丰富多彩。

不仅如此,即便属于同一种类的个体,在表现出来的公有功能方面,也是各不相同。

譬如,虽然同为人类,普遍具备说话的功能,但是每个具体的个人在说话时,音色又各自不同。

我们生活的世界,就是这样丰富多彩。既有共性的东西,又有具体不同的风格。

Java语言源于为解决现实世界中各种各样应用问题提供一整套解决方案。

所以,我们生活的现实世界,乃至整个宇宙,深深地映射入Java语言中。

世界与宇宙何其深邃与复杂,同样,Java的博大精深不言而喻。

可以说,每个Java程序的运行,都是为了解决某个或某种应用问题而生。

古人说“格物致知”,我们探秘Java程序运行的内在原理,有助于帮助我们深入认识Java世界的运行机制。

每个Java程序,都离不开类和对象。

所以,我们就从类加载说起。

一、类的生命周期

想象一下,你在Eclipse里写了一个Java程序,通过javac(Java编译器),将Java源代码编译为.class字节码文件。

字节码文件静静地躺在你的电脑磁盘里,你要运行这个Java程序,就要去运行编译后的字节码文件。

加载.class字节码文件到内存,形成供JVM使用的类,并到这个类从内存中销毁,这便是类的生命周期。

总的来说,类的生命周期经过了如图所示的阶段:

1.加载

关于加载,其实,就是根据.class文件找到类的信息将其加载到方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类信息的入口。

需要简单科普一下的是:Java程序运行起来时成为进程,操作系统需要为该进程分配内存空间。Java程序的进程会将所分得的内存空间再予以分区,主要有栈区(存储局部变量)、堆区(存储创建的对象)、方法区(存储类的方法代码,以及类的静态成员变量信息,还有常量池)、程序计数器(记录线程的执行信息)、本地方法栈(与 操作系统底层交互时使用)。如图所示:

2.链接

有的出处称为“连接”,若从英文单词“linking”判断,则翻译为“链接”比较合适。

链接一般会与加载阶段和初始化阶段交叉进行。

链接的过程由三部分组成:验证、准备和解析。
(1)验证:该阶段是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
(2)准备:主要是为由static修饰的成员变量分配内存空间,并设置默认的初始值。默认初始值如下:

  ①8种基本数据类型的默认初始值是0。
  ②引用类型默认的初始值是null。
  ③对于有static final修饰的常量会直接赋值,例如:static final int x=10;则x默认就是10。
(3)解析:就是把常量池中的符号引用转换为直接引用,也就是说,JVM会将所有的类或接口名、字段名、方法名转换为具体的内存地址。

3.初始化
这是将静态成员变量(也称为“类变量”)赋值的过程。

也就是说,只有static修饰的变量才能被初始化,执行的顺序是:

父类静态域(静态成员变量)或者静态代码块,然后是子类静态域或者子类静态代码块。

并非所有的类都会被初始化,只有那些被直接引用(主动引用)的类才会被初始化。在Java中,类被直接引用的情况有:

  ①通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法;
  ②通过反射方式执行以上三种行为;
     ③初始化子类的时候,会触发父类的初始化;
     ④作为程序入口直接运行时(也就是直接调用main方法);

除了以上4种情况,其他使用类的方式叫做被动引用,被动引用不会触发类的初始化。

被动引用举例:

(1)子类调用父类的静态变量,子类不会被初始化,只有父类被初始化。对于静态字段,只有直接定义这个字段的类才会被初始化。

(2)通过数组定义来引用类,不会触发类的初始化。

(3)访问类的常亮,不会初始化类。

4.使用

类在使用过程中也存在三步:对象实例化、垃圾收集、对象终结。
(1)对象实例化:就是执行类中构造函数的内容,如果该类存在父类,JVM会通过显式或者隐式的方式先执行父类的构造函数,在堆内存中为父类的实例变量开辟空间,并赋予默认的初始值;然后,引用变量获取对象的首地址,通过操作对象来调用实例变量和方法。
(2)垃圾收集:当对象不再被引用的时候,就会被JVM虚拟机标上特别的垃圾标识,在堆区中等待被GC回收。
(3)对象的终结:对象被GC回收后,对象就不再存在了,对象的生命也就走到了尽头。
5.卸载
这是类的生命周期中最后的一步。

程序中不再有该类的引用,该类会被JVM执行垃圾回收,类在本次程序运行中的生命结束。

二、双亲委派

Java中的类加载存在层次性,一个重要的加载模型是双亲委派。

先来看Java中类加载器的层次体系:

什么是类加载器呢?

简而言之,类加载器可以将.class字节码文件加载到JVM内存中的方法区形成类模板(或者称为该类的数据结构/镜像),并在堆区中产生Class对象。

如果站在JVM的角度来看,只存在两种类加载器:

1.启动类加载器(Bootstrap ClassLoader):

也称为“根加载器”。由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。

2.其他类加载器:

由Java语言实现,继承自抽象类ClassLoader。如:
(1)扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
(2)应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况下,如果我们没有自定义类加载器,默认就是用这个加载器。通过在控制台打印(System.out.println(System.getProperty("java.class.path"));),可以看到应用程序类加载器加载的路径信息。如图所示:

C:\Program Files\Java\jdk1.8.0_181\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar;
E:\workspace\eclipse\work_j2ee\java1_8\bin

双亲委派模型的工作过程是:

如果一个类加载器收到类加载的请求,它会先判断这个类是否已经加载过,若已经加载过,就不再重复加载;若还未加载过,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,若该类加载器无父类加载器,则将加载请求委派给根类加载器。每个类加载器都是如此(根类加载器除外)。只有当父类加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子类加载器才会尝试自己去加载。
Java在类加载中采用双亲委派模型有什么好处呢?

使得Java类同其类加载器一起具备了一种带优先级的层次关系,从而保证了程序运行中类的唯一性。

我们知道,程序运行起来时,每个类在堆内存中的Class对象仅有唯一的一个,不会引起程序运行中类的混乱,其根源在于Java类加载中采用的双亲委派模型。

三、自定义类加载器

有的时候,我们需要当前程序以外的class文件,这时,我们就需要自定义类加载器,对相应的class文件进行加载。

自定义类加载器的步骤是:

1.继承ClassLoader

2.重写findClass()方法

3.调用defineClass()方法

接下来自定义一个类加载器,加载E:/test下的Test2.class文件。

Test2.class文件的源代码文件Test2.java:

package bwie2;

public class Test2 {
public void say() {
System.out.println("Hello China");
}
}

接着,创建自定义类加载器:

package bwie;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream; public class MyCloassLoader2 extends ClassLoader {
private String classPath;// 要加载的类路径 public MyCloassLoader2(String classPath) {// 构造方法传参
this.classPath = classPath;
} @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {// 查找类
byte[] classData = getData(name); if (classData == null) {
//若字节码为空,则抛出异常
throw new ClassNotFoundException();
} else {
// defineClass,将字节码转化为类
return defineClass(name, classData, 0, classData.length);
}
//return super.findClass(name);
} // 返回类的字节码
private byte[] getData(String className) {
InputStream in = null;
ByteArrayOutputStream out = null;
String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try {
in = new FileInputStream(path);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
in.close();
out.close();
return out.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

然后,通过测试类进行测试:

package bwie;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
//自定义类加载器的加载路径
MyCloassLoader2 classLoader = new MyCloassLoader2("E:/test"); //包名+类名
Class<?> clazz = classLoader.loadClass("bwie2.Test2");
if(clazz!=null) {
Object obj = clazz.newInstance();
Method method = clazz.getMethod("say");
method.invoke(obj);
System.out.println(clazz.getClassLoader().toString());
}
}
}

程序执行后,控制台打印如图所示:

可见,笔者使用自定义的类加载器MyCloassLoader2成功地加载了程序以外的class文件。

四、深入讲解反射

反射是Java语言中一个非常重要的机制。

程序员们一般都知道:通过反射,可以获取类与对象的所有信息,执行若干操作(如创建对象,方法调用),还可以修改类的数据结构(如修改访问权限)。

在Java中,反射对应的单词是reflect。

提到反射,不免让人霎时想起光的反射(Reflection of light)。

Java里运用反射,是否与光的反射有关?这也涉及Java为什么要取名为反射。

举个例子来说,一个美女站在镜子前,请问,镜子里的美女和镜子前的美女,是否同一个美女?

答案是肯定的。

我们再来看Java程序的加载与运行。

一个被编译为.class字节码文件的类,经过JVM的加载,在方法区中形成对应的类模板。

那么请问,JVM加载出的类模板,与加载前的类,是不是同一个类?

答案是肯定的。

大家想一下:一个人站在镜子前,通过光的反射,可以在镜子里产生一个镜像。镜像与镜子前的人是同一个人。这是运用了光的反射规则。

实际上,我们能看到五彩缤纷的世界,一个重要原因是光的反射的存在。

光的反射外在表现为一种现象,本质是一种机制和规则。

同样,一个表现为.class字节码文件的类,经过JVM中的类加载器加载,在方法区中形成类模板,也相当于类的“镜像”。

大家再想下:Java中,加载前、表现为.class字节码文件的类,与加载后、在方法区中形成的类模板,同属于一个类,这与光的反射是不是有异曲同工之妙?

这也就是Java为什么将类加载后、在内存的方法区中形成类模板的机制,称为反射的缘由。

看来,Java语言的缔造者不愧是大牛,将技术比喻得那么贴切,又那么接近生活!

大家还会看到,上图中,堆区里有个Class对象,类加载时会在堆区中产生Class对象。

程序加载运行时,一个类在内存中的Class对象与类模板都是唯一的。

程序中通过Class对象操作类模板。

可以说,程序中要运用反射,就离不开Class对象。那么,Class对象究竟是什么?

如果我们把JVM看作是人的话,对于程序员来说,通过阅读Java源代码,能够了解一个类的数据结构,那么,Java程序在运行中,JVM又是如何读懂类的数据结构的呢?

这要归功于类加载器加载class文件在方法区生成该类的模板。如果说,class文件静态地存储了类信息,类加载器加载出来的类模板相当于类在动态运行环境中的数据结构,JVM就是通过这个类模板来认识与操作这个类的。

编程语言实现了人机交互。Java语言也是如此。

我们要操控JVM虚拟机去操作内存中的某个类,应该怎么办呢?Java语言为所有Java数据类型(基本数据类型与引用数据类型)均提供了class属性,通过该属性可以返回Class对象,这个Class对象是我们在程序中运用反射机制,是我们与JVM交互、指挥JVM去操作类模板的接口性工具。

机器懂的,我们未必懂。怎么办呢?找个中间人,通过中间人操作机器。这就好比,我们通过操作系统去操作电脑硬件那样。

我们通过Class对象,指挥JVM操作程序动态运行中的类模板。

五、对象的生命周期

在Java中,对象的生命周期包括以下几个阶段:

1.  创建阶段(Created)
2. 应用阶段(In Use)
3. 不可见阶段(Invisible)
4. 不可达阶段(Unreachable)
5. 收集阶段(Collected)
6. 终结阶段(Finalized)
7. 对象空间重分配阶段(De-allocated)

如图所示:

1.创建阶段(Created)
在创建阶段系统通过下面的几个步骤来完成对象的创建过程:
    l  为对象分配存储空间
    l  开始构造对象
    l  从超类到子类对static成员进行初始化
    l  超类成员变量按顺序初始化,递归调用超类的构造方法
    l  子类成员变量按顺序初始化,子类构造方法调用
一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段。

2.应用阶段(In Use)
对象至少被一个强引用持有着。

3.不可见阶段(Invisible)
当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然这些引用仍然是存在着的。
简单来说,就是程序的执行已经超出了该对象的作用域了。

比如,在使用某个局部变量count时,已经超出该局部变量的作用域(不可见),那么就称该变量count处于不可见阶段。这种情况下,编译期在编译阶段通常就会提示与报错。
4.不可达阶段(Unreachable)
对象处于不可达阶段是指该对象不再被任何强引用所持有。
与“不可见阶段”相比,“不可达阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为”GC root”。这些GC root可能会导致对象的内存泄露,使得对象无法被回收。


5.可收集阶段、终结阶段与释放阶段

这是对象生命周期的最后一个阶段:可收集阶段、终结阶段与释放阶段。

当对象处于这个阶段的时候,可能处于下面三种情况:

(1)垃圾回收器发现该对象已经不可到达,则对象进入“可收集阶段”。

(2)finalize方法已经被执行,则对象空间等待被垃圾回收器进行回收,即“终结阶段”。

(3)对象空间已被重用,即“对象空间重新分配阶段”。

当对象处于上面的三种情况时,该对象就处于可收集阶段、终结阶段与释放阶段了。JVM虚拟机就可以直接将该对象回收了。

探秘Java类加载的更多相关文章

  1. 探秘Java中的String、StringBuilder以及StringBuffer

    探秘Java中String.StringBuilder以及StringBuffer 相信String这个类是Java中使用得最频繁的类之一,并且又是各大公司面试喜欢问 到的地方,今天就来和大家一起学习 ...

  2. 探秘Java中String、StringBuilder以及StringBuffer

    探秘Java中String.StringBuilder以及StringBuffer 相信String这个类是Java中使用得最频繁的类之一,并且又是各大公司面试喜欢问 到的地方,今天就来和大家一起学习 ...

  3. 转发: 探秘Java中的String、StringBuilder以及StringBuffer

    原文地址 探秘Java中String.StringBuilder以及StringBuffer 相信String这个类是Java中使用得最频繁的类之一,并且又是各大公司面试喜欢问到的地方,今天就来和大家 ...

  4. 探秘 Java 热部署三(Java agent agentmain)

    前言 让我们继续探秘 Java 热部署.在前文 探秘 Java 热部署二(Java agent premain)中,我们介绍了 Java agent premain.通过在main方法之前通过类似 A ...

  5. 探秘 Java 热部署二(Java agent premain)

    # 前言 在前文 探秘 Java 热部署 中,我们通过在死循环中重复加载 ClassLoader 和 Class 文件实现了热部署的功能,但我们也指出了缺点-----不够灵活.需要手动修改文件等操作. ...

  6. 探秘Java中的String、StringBuilder以及StringBuffer(转载)

    探秘Java中String.StringBuilder以及StringBuffer 相信String这个类是Java中使用得最频繁的类之一,并且又是各大公司面试喜欢问到的地方,今天就来和大家一起学习一 ...

  7. 【转】探秘Java中的String、StringBuilder以及StringBuffer

    探秘Java中String.StringBuilder以及StringBuffer 相信String这个类是Java中使用得最频繁的类之一,并且又是各大公司面试喜欢问到的地方,今天就来和大家一起学习一 ...

  8. java笔记--理解java类加载器以及ClassLoader类

    类加载器概述: java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制 ...

  9. java类加载器深入研究

    看了下面几篇关于类的加载器的文章,豁然开朗.猛击下面的地址开始看吧. Java类加载原理解析      深入探讨 Java 类加载器 分析BootstrapClassLoader/ExtClassLo ...

随机推荐

  1. Docker(十七)-修改Docker容器启动配置参数

    有时候,我们创建容器时忘了添加参数 --restart=always ,当 Docker 重启时,容器未能自动启动, 现在要添加该参数怎么办呢,方法有二: 1.Docker 命令修改 docker c ...

  2. layout图形化界面看不到内容 Failed to find the style corresponding to the id

    1.问题 在创建新的工程的时候,选择目标SDK为api21,编译SDK为api23.创建出来的layout文件图形化界面中看不到,并且报错: Failed to find the style corr ...

  3. InputStream流无法重复读取的解决办法

    前言:今天工作的需要需要读取aws云上S3桶里面的PDF数据,第一步能够正常的获取PDF文件的InputStream流,然后,我为了测试使用了IOUtils.toString(is)将流System. ...

  4. TRichEdit怎样新增的内容到最后一行?

    Delphi里使用TRichEdit,使用SetSelTextBuf时可以设置显示的字体格式,但是显示位置是在当前的插入光标后,如果人为改变插入光标的位置,比如在其他位置单,以后再插入的内容位置就没办 ...

  5. Java之资源文件读取

    ClassLoaderWrapper.java package org.utils.resource; import java.io.InputStream; import java.net.URL; ...

  6. Nginx upstream 配置

    1.轮询(默认)每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除. 2.weight指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况.例如:u ...

  7. kvm安装配置

    操作系统版本:CentOS Linux release 7.5.1804 (Core) 内核版本:3.10.0-862.el7.x86_64 1.安装 安装以下软件.yum -y install qe ...

  8. poj 1511 Invitation Cards(最短路中等题)

    In the age of television, not many people attend theater performances. Antique Comedians of Malidine ...

  9. error::尝试加载 Oracle 客户端库时引发 BadImageFormatException。如果在安装 32 位 Oracle 客户端组件的情况下以 64 位模式运行,将出现此问题。

    1.VS出现此问题 问题分析:本地电脑安装的oracle客户端为64位客户端,vs启动网站默认启动自带的32位IIS Express,所以出错. 解决方案: 方案1.本地电脑安装oracle32位客户 ...

  10. zabbix使用percona插件监控mysql

    1.添加percona仓库. # yum install -y http://www.percona.com/downloads/percona-release/redhat/0.1-4/percon ...