2020-09-07

Class 对象

要理解RRTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的。

这项工作是由被称为Class对象的特殊对象来完成的,它包含了与类有关的信息。

事实上,Class对象就是用来创建的所有"常规"对象的。

java使用Class对象来执行其RTTI,即使你正在执行的是类似转型的这样的操作。

RTTI

一、RTTI(Run-Time Type identification),通过运行时类型信息,程序能够使用基类的指针或引用来检查这些指针或引用所指向的对象的实际派生类型。

这就是所说的多态,父类对象可以指向子类的引用。

面向对象的编程语言C++,Java,delphi都提供了RTTI的支持。RTTI并不是什么新技术,很早就有了,他主要提供了运行时确定类对象类型的方法。

1.经典造型,如“Shape”,它用RTTI确保造型的正确性,并在遇到一个失败的造型后产生一个ClassCastException违例。
2.代表对象类型的Class对象。可查询Class对象,获取有用的运行期资料。
3. instanceof 告诉我们对象是不是一个特定类型的实例。它会返回一个布尔值,以便以问题的形式使用,就象下面这样:

if(x instanceof Dog)
((Dog)x).bark();
  • 将x造型至一个Dog前,上面的if语句会检查对象x是否从属于Dog类。进行造型前,如果没有其他信息可以告诉自己对象的类型,那么instanceof的使用是非常重要的——否则会得到一个ClassCastException违例。

对RTTI来说,编译器会在编译期打开和检查.class文件。但对“反射”来说,.class文件在编译期间是不可使用的,而是由运行时环境打开和检查 ,我们利用反射机制一般是使用java.lang.reflect包提供给我们的类和方法。


Class 对象

1 类是程序的一部分,每个类都有一个Class对象。换句话说就是,每当编写了一个新类,就会产生一个Class对象(更准确的说,是被保存到一个同名的.class文件中)。为了生成这个类的对象,运行这个程序的Java虚拟机(JVM)将使用被称为“类加载器”的子系统。【想生成一个对象,你得先将  .class 文件加载到JVM中去,具体来说是这个JVM的子系统---类加载器

2-类加载器子系统实际上是可以包含一条类加载器链,但是只有一个原生类加载器【换一句话便是还有其他的扩展的类加载器】,类加载器是JVM【有哪些部分构成??】实现的一部分。原生类加载器加载的是所谓的可信类,包括Java API 类,他们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果有特殊的需求【以某种特殊的方式加载类,以支持web服务器应用,或者在网络中下载类】,你用一种方式可以挂接额外的类加载器。

3-所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创建第一个对类的静态成员的引用时,就会加载这个类。这个证明构造方法也是类的静态方法,即使在构造器的前面没有添加static关键字。因此,使用new操作符创建类的新对象也会被当作对类的静态成员的引用。

因此,java程序在它开始运行之前并非被完全加载,其各个部分是在必须使用时才加载的。这一点儿与许多传统语言都不同。动态加载的行为,在c++这样的静态语言中很难或者根本无法复制。

4-类加载器首先检查这个类的Class 对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找相关的 .class文件(例如某个类加载器可能会在数据库中查找字节码)。这个类的字节码被加载时,他会接受验证,以确保其没有被破坏,并且不包含不良的java代码。【这是java中用于安全防范目的的措施之一】

一旦某个类的class对象被加载到内存中,他就被用来创建这个的所有对象。


一旦定义了类,就可以在类(在java中你所做的全部工作就是定义类,长生哪些类的对象)中设置两种类型的元素:字段【数据成员】和方法【成员函数】。字段可以是任何类型的对象,可以通过其引用与其进行通信;

普通字段不能在对象间共享。

基本成员的默认值,若类的某个数据成员是基本数据类型,即使没有初始化,Java也会确保它获得一个默认值。

当变量作为类的数据成员时,java才确保给其默认值,以确保哪些是基本类型的成员变量得到初始化,【C或C++中没有这个功能】防止产生程序错误。但是这种方法并不适用于“局部”变量(并非某个类的字段,可以是方法中变量)。因此如果是在某个方法中定义int x;那么便不会被自动初始化为0.所以在使用x前,应先对其赋一个适当的值。如果忘记了赋值,那么java在编译时会返回一个错误,告诉你变量没有初始化。这正是Java 优于C++的地方。

在第一次使用方法中没有被进行初始化的变量时产生的错误。

Variable 'x' might not have been initialized

在类的声明中,属性是用变量来表示的。这种变量就称为实例变量【数据成员】【实例变量在对象创建的时候创建,在对象销毁的时候销毁】,是在类声明的内部但是在类的其他成员方法之外声明的。类的每个对象维护它自己的一份实例变量的副本。


方法的基本组成,名称、参数、返回值、方法体。

Java中任何传递对象都是传递的这个对象的引用。

返回类型是void 的方法,关键字 return 的作用只是用来推出方法。因此没有必要到方法结束时才离开,可以在任何地方返回。但是如果返回类型不是void,那么无论在何处返回,编译器都会强制返回一个正确类型的返回值。


Java的类加载过程

===============Java类的加载、链接、初始化===============

1. 加载:查找并加载类的字节码文件

2. 链接

a.验证:确保被加载类的正确性,不会破坏JVM的恶意代码。

b.准备:为类的静态变量分配类存,并执行隐式初始化(有虚拟机把静态变量初始化为其默认值)

c.解析:把类的符号引用转换为直接引用

3. 初始化:为类的变量赋予正确的初始值。(执行静态变量的赋值语句、静态代码块)

分析观察如下代码:

package com.wzm;

public class Test1 {

    public static void main(String[] args) {

        System.out.println(Singleton.count1);
        System.out.println(Singleton.count2);
    }
}
class Singleton{
    private static Singleton singleton=new Singleton();
    public static int count1;
    public static int count2=0;
    private Singleton() {
        count1++;
        count2++;
    }
    public static Singleton getInstance(){
        return singleton;
    }
}

上面一段代码控制台输出的结果是:1、0
如果我们理解Java的类加载过程,我们就会理解为什么上一段代码的输出结果是:1、0
下面就开始介绍Java的类加载过程:

 

上面就是Java类的加载过程,知道了类的加载过程,我们就能分析为什么上面一段代码的输出结果是:1、0
分析:
准备阶段:singleton=null;count1=0;count2=0;
初始化阶段:执行singleton=new Singleton();这时count1=1,count2=1;
执行count2=0;这时count2又变为0;所以最终的输出结果为:1、0

其实就是一个类的加载过程中,主要是:类的加载,链接,初始化三步。其中关于链接又分为三步,验证、准备、解析三个阶段。

准备阶段:为类的静态变量分配类存,并执行隐式初始化(有虚拟机把静态变量初始化为其默认值),以及静态代码块分配空间。即使是静态方法没有调用也不会自动的运行。

初始化阶段,则按照程序的顺序自动进行初始化。


#类加载的机制的层次结构
每个编写的".java"拓展名类文件都存储着需要执行的程序逻辑,这些".java"文件经过Java编译器编译成拓展名为".class"的文件,".class"文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的".class"文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载,这里我们需要了解一下类加载的过程,如下: 


  • 加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象

  • 验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

  • 准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中

  • 解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用,如需更详细了解,可参考《深入Java虚拟机》)。

  • 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

这便是类加载的5个过程,而类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,在虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器),下面分别介绍
##启动(Bootstrap)类加载器
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

##扩展(Extension)类加载器
扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

//ExtClassLoader类中获取路径的代码
private static File[] getExtDirs() {
     //加载<JAVA_HOME>/lib/ext目录中的类库
     String s = System.getProperty("java.ext.dirs");
     File[] dirs;
     if (s != null) {
         StringTokenizer st =
             new StringTokenizer(s, File.pathSeparator);
         int count = st.countTokens();
         dirs = new File[count];
         for (int i = 0; i < count; i++) {
             dirs[i] = new File(st.nextToken());
         }
     } else {
         dirs = new File[0];
     }
     return dirs;
 }

##系统(System)类加载器
也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
  在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面我们进一步了解它。

#理解双亲委派模式

##双亲委派模式工作原理
双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:

双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的实力坑爹啊?那么采用这种模式有啥用呢?

##双亲委派模式优势
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常

java.lang.SecurityException: Prohibited package name: java.lang

所以无论如何都无法加载成功的。下面我们从代码层面了解几个Java中定义的类加载器及其双亲委派模式的实现,它们类图关系如下

从图可以看出顶层的类加载器是ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),这里我们主要介绍ClassLoader中几个比较重要的方法。

https://blog.csdn.net/javazejian/article/details/73413292这个博客写的挺好的,实际上jvm原理上面也有这些内容。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值