面试笔记——java的类加载和对象创建小记

1 java文件执行过程

主要为两个过程:编译与运行
编译:把我们写好的java文件,通过javac命令编译成字节码,也就是.class文件。
运行:把编译声称的.class文件交给java虚拟机(JVM)执行。
运行中的类加载过程即是指jvm虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。

1.1 类加载器

1 java类加载器

  • java源代码.java文件通过编译成字节码.class文件后,需要被加载到java虚拟机的内存空间中使用,这个过程就是类加载。类加载依靠的是java类加载器
  • java类加载是java运行时环境的一部分,负责动态加载java类到java虚拟机的内存空间中,类通常是按需加载的,即第一次使用该类时才加载。由于有了类加载器,java运行时系统不需要知道文件的位置与文件系统。

2 类加载器结构
java类的加载器大致可分为两类:
一类是系统提供的:
①引导类加载器(bootstrapclass loader):它用来加载java的核心库,是用原生代码而不是用java实现的,并不继承自java.lang.ClassLoader,除此之外基本上所有的类加载器都是java.lang.ClassLoader类的一个实例。
②扩展类加载器(extensionsclass loader):它用来加载java的扩展库。java虚拟机的实现会提供一个扩展库目录(一般为%JRE_HOME%/lib/ext)。该类加载器在此目录里面查找并加载java两类。
③系统类加载器(systemclass loader或 App class loader):它根据当前java应用的类路径(CLASSPATH)来加载java类。一般来说,java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
另一类则是由java应用开发人员编写的:
开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。
自定义类加载器的用处:

  • 一方面是由于java代码容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后在通过实现自己的自定义类加载器进行解密,最后在加载;
  • 另一方面也可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。

3 双亲委托模型(Parentdelegation模型)
从1.2版本开始,java引入了双亲委托模型,从而更好的保证java平台的安全。在此模型下,当一个装载器被请求装载某个类时,它首先委托自己的parent去装载,若parent能装载,则返回这个类所对应的Class对象,若parent不能装载,则由parent的请求者去装载。
在此模型下用户自定义的类装载器不可能装载应该由父亲装载器装载的可靠类,从而防止不可靠甚至恶意代码代替由父亲装载器装载的可靠代码。
示例: 假如loader2的parent为loader1,loader1的parent为system class loader。假设loader2被要求装载类MyClass,在parent delegation模型下,loader2首先请求loader1代为装载,loader1再请求系统类装载器去装载MyClass。若系统装载器能成功装载,则将MyClass所对应的Class对象的reference返回给loader1,loader1再将reference返回给loader2,从而成功将类MyClass装载进虚拟机。若系统类装载器不能装载MyClass,loader1会尝试装载MyClass,若loader1也不能成功装载,loader2会尝试装载。若所有的parent及loader2本身都不能装载,则装载失败。

4 加载类的过程
类加载器的双亲委托模式会首先代理给其他类加载器来尝试加载某个类,这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。**真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。**前者称为一个类的定义加载器,后者称为初始加载器。**在java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。**也就是说,哪个类加载启动类的的加载过程并不重要,重要的是最终定义这个类的加载器。
两个类加载器的关联之处在于:一个类的定义加载器是它引用的其他类初始加载器。如类com.example.Outer 引用了类com.example.Inner,则由类com.example,Outer的定义加载器辅助启动类com.example.Inner的加载过程。
类加载器在成功加载某个类之后,会把得到的java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。即对于一个类加载器实例而言,相同全名的类只加载一次,即loadClass方法不会被重复调用。

1.2 类加载过程

过程主要分为三部分:加载,链接,初始化。而链接可细分为三小部分:验证,准备,解析。

1.2.1 加载
简单来书,加载指的是把class字节码文件从各个来源通过类加载器装载如内存中。
这里有两个重点:

  • 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远处网络,以及动态代理实时编译
  • 类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。

1.2.2 验证
主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。

  • 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
  • 字节码验证:通过数据流和控制流分析,确保程序语义合法,符合逻辑的;
  • 符合引用验证:发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
    1.2.3 准备
    类变量是被static修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。实例变量不会在这阶段分配内存,他将会在对象实例化是随着对象一起分配在java堆中。(实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次)
    初始值一般为0值,例如下面的类变量value被初始化为0而不是123.
public static int value = 123;

如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为0.

public static final int value = 123;

1.2.4 解析
将常量池内的符号引用替换为直接引用的过程。
两个重点:
①符号引用:即一个字符串,但这个字符串给出一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
②直接引用:可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量。

示例:现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

1.2.5 初始化
这个阶段主要是对类变量初始化,是执行静态构造器的过程。换句话说,只对static修饰的变量或语句进行初始化。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

java程序初始化顺序:
1、父类静态变量

2、父类静态代码块

3、子类静态变量

4、子类静态代码块

5、父类非静态变量

6、父类非静态代码块

7、父类构造器

8、子类非静态变量

9、子类非静态代码块

10、子类构造器
java程序初始化一般遵循3个原则:

  1. 静态对象(变量)先于非静态对象(变量)初始化。其中静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化很多次
  2. 父类优先于子类进行初始化
  3. 按照成员变量的定义顺序进行初始化。即使变量定义散布于方法之中,他们依然在任何方法(包括构造函数)被调用前先初始化
    1.2.6 总结
    类加载过程只是一个类生命周期的一部分,在其前,有编译过程,只有对源代码编译之后,才能获得能够被虚拟机加载的字节码文件;在其后还有具体的类使用过程,当使用完成之后,还会在方法区垃圾回收的过程中进行卸载。

1.3 类初始化时机

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列5种情况必须对类进行初始化(加载,验证,准备都会随着发生):

  1. 遇到new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这4条指令的场景是:使用new关键字实例化对象的时候;读取或设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK1.7的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为REF_getStatic, REF_putStatic, REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
①通过子类引用父类的静态字段,不会导致子类初始化:

System.out.println(SubClass.value); // value 字段在 SuperClass 中定义

②通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类时一个有虚拟机自动生成的,直接继承自Object的子类,其中包含了数组的属性和方法。

SuperClass[] sca = new SuperClass[10];

③常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

System.out.println(ConstClass.HELLOWORLD);

1.4 对象创建过程

在这里插入图片描述1.4.1 类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载,解析和初始化过了,如果没有,那么必须先执行相应的类加载过程。

1.4.2 分配内存
在类加载检查通过后,接下来虚拟机将会为新生的对象分配内存。对象所需的内存大小在类加载完成后便可以完全确定,为对象分配空间等同于把一块确定大小的内存从java堆中划分出来。分配方式有“指针碰撞”和“空闲列表”两种,选择那种分配方式由java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
java堆内存是否规整,取决于GC收集器的算法是“标记-清除”,还是“标记-整理”(也称作“标记-压缩”),值得注意的是,复制算法内存也是规整的。在使用Serial、ParNew等待整理过程的收集器时,采用的是指针碰撞,在使用CMS这种mark-sweep算法的收集器时,使用的是空闲列表。
内存分配并发问题:
在创建对象的时候,有一个很重要的问题,就是线程安全。在实际开发过程中,创建对象时很频繁的事情,例如正在给A对象分配内存,但是指针还没修改,这时候对象B可能使用原来的指针来分配内存的情况。作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
CAS+失败重试: CAS是乐观锁的一种实现方式。所为乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
TLAB:为每个线程预先在Eden去分配一块内存。JVM在给线程中的对象分配内存时,首先在各个线程的TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,在采用上述的CAS进行内存分配。虚拟机是否启用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
1.4.3初始零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。如果使用TLAB,这一工作过程也可以提前到TLAB分配时进行。
1.4.4 设置对象头
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希吗,对象的GC分代年龄等信息,这些信息存放在对象的对象头中。根据虚拟机当前的运行状态的不同,对象头会有不同的设置方法。
1.4.5 执行init方法
在上面工作都完成之后,从虚拟机的角度来看,一个新的对象已经产生了,但从java程序的视角来看,对象创建才刚开始,方法还没执行,所有的字段都还为零。所以一般来说,执行new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

1.5 对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为三个区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
①HotSpot虚拟机的对象头包括两部分信息:
第一部分用来存储对对象自身的运行时数据,例如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分数据在32位和64的虚拟机中分别为32bit和64bit,成为Mark Word。
另一部分是类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
②实例数据部分存储的是对象真正有效的信息,也是在程序代码中定义的各种类型的字段内容。无论从父类中继承下来的,还是在子类中定义的都需要记录下来。
③对齐填充并不是必须的部分,没有特别的含义,仅仅起着占位符的作用,因为Hostpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

1.6 对象的访问定位

建立对象是为了使用对象,java程序中需要通过栈上的reference引用数据来操作堆上的具体对象。对象的访问方式取决于虚拟机的实现,主流的方式有句柄池(符号引用)和直接指针(直接引用)两种。
①句柄池。如果使用句柄池的话,java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
在这里插入图片描述②直接指针。如果使用直接指针,那么java堆中的对象的布局就必须考虑如何放置访问类型数据的相关信息,而reference中存储的就是对象的直接地址。
在这里插入图片描述使用句柄访问最大的好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针最大的好处就是速度更快,节省了一次指针定位的时间开销。

2 关于加载器小记

2.1 java.lang.ClassLoader抽象类
几乎所有的类加载器都是继承自java.lang.ClassLoader类,所以有必要看一下ClassLoader中几个比较重要的方法:
① 供用户调用接口,加载制定名称类(二进制)

public Class<?> loadClass(String name)throws ClassNotFoundException{//…}

protected synchronized Class<?>loadClass(String name,boolean resolve)throws ClassNotFoundException{//…}

② 被loadClass方法调用去加载指定名称类:

protected Class<?> findClass(String name)throws ClassNotFoundException{//…}

③一般在findClass方法中读取到对应字节码后调用来定义类型(无需重写):

protected finalClass<?> defineClass(String name,byte[] b,intoff,intlen) throws ClassFormatError{//…}

2.2 线程上下文类加载器
java提供了很多服务提供者接口,允许第三方为这些接口提供实现。常见的SPI有JDBC和JNDI等。这些SPI的接口由java核心库来提供,但这些接口的实现很可能是作为引入的jar包被包含进来的。SPI接口中的代码经常需要加载具体的实现类,问题就出现了:SPI的接口是java核心库的一部分,由引导类加载器来加载,而SPI的实现类一般由系统类加载器加载的, 根据双亲委派模型机制,因为引导类加载器是系统类加载器的祖先类加载器,所以引导类加载器加载SPI接口之后无法找到SPI的实现类。
线程上下文类加载器正好解决了这个问题。如果不做任何的设置,java应用的线程的上下文类加载器默认就是系统上下文类加载器。在SPI接口的代码中使用线程上下文类加载器,就可以成功的加载到SPI实现的类。线程上下文类加载器在很多SPI的实现中都会用到。

2.3 类加载器与Web容器
对于运行在java EE的容器中的Web应用来说,类加载器的实现方式与一般的java应用有所不同。不同的Web容器的实现方式也会有所不同。以Apache Tomcat来说,每个Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序相反。这是java Servlet规范中的推荐做法,其目的是使得Web应用自己的类的优先级高于Web容器提供的类。这种代理模式的一个例外是:java核心库的类是不在查找范围之内的。这也是为了保证java核心库的类型安全。

2.4 其他加载类的方法——Class.forName
Class.forName 是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader) 和Class.forName(String className)。第一种形式的参数name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。第二中形式则相当于设置了参数initialize的值为true,loader的值为当前类的类加载器。Class.forName(“org.apache.derby.jdbc.EmbeddedDriver”).newInstance()用来加载ApacheDerby数据库的驱动。

3 总结图

在这里插入图片描述

本文参考:
https://www.cnblogs.com/aiqiqi/p/10770864.html#_label1
https://blog.csdn.net/lettyisme/article/details/80565075?utm_source=app
https://blog.csdn.net/ln152315/article/details/79223441
https://blog.csdn.net/stypace/article/details/40613953?utm_source=app

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值