JVM常见面试题2——类加载与反射

6.类加载

   What(是什么?) Java是面向对象的语言,对Java来说,所有的都是对象,包括类文件。类加载就是把一个类文件(.class文件)读入内存,并为其创建一个对象的过程。这个对象是可以被虚拟机直接使用的Java类型。
   Why(为什么?)Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能。
双亲委派模型(递归实现)
在这里插入图片描述
  工作过程:如果一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
  加载过程:会先检查是否已被加载,自底向上检查(以保证该类只被加载一次);若未被加载,则由父类加载器先加载,自顶向下逐层尝试加载此类。
  好处:java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。

7.创建一个对象的详细过程?类的生命周期(类加载)? 对象的生命周期?

  Java类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。其中准备、验证、解析3个部分统称为连接(Linking),如图所示:
在这里插入图片描述

  • 类初始化时机:
      1). 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先对其进行初始化。2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。4) 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。5) 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果。
  • 对象创建的时机:
      1). 使用new关键字创建对象;2). 使用Class类的newInstance方法(反射机制);3). 使用Constructor类的newInstance方法(反射机制)(可调用私有构造);4). 使用Clone方法创建对象;5). 使用(反)序列化机制创建对象。
      创建一个对象常常需要经历如下几个过程:父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。
      虚拟机会保证一个类的类构造器()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器(),其他线程都需要阻塞等待,直到活动线程执行()方法完毕。 类构造器()与实例构造器()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器()执行之前,父类的类构造()执行完毕。 在同一个类加载器下,一个类只会被初始化一次,但是一个类可以任意地实例化对象。也就是说,在一个类的生命周期中,类构造器()最多会被虚拟机调用一次,而实例构造器()则会被虚拟机调用多次,只要程序员还在创建对象。
      当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是实例变量初始化、实例代码块初始化以及构造函数初始化。
    在这里插入图片描述

  实例化一个类的(创建)对象的过程是一个典型的递归过程: 在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。此时,首先实例化Object类,再依次对以下各类进行实例化,直到完成对目标类的实例化。具体而言,在实例化每个类时,都遵循如下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造函数初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。
  另外,在类的初始化阶段需要做的是执行类构造器()。类构造器本质上是编译器收集所有静态语句块和类变量的赋值语句按语句在源码中的顺序合并生成类构造器()。

//父类
class Foo {
    int i = 1;
    Foo() {
        System.out.println(i);             -----------(1)
        int x = getValue();
        System.out.println(x);             -----------(2)
    }
    { i = 2;}
    protected int getValue() {
        return i;
     }
}
//子类
class Bar extends Foo {
    int j = 1;
    Bar() {j = 2;}
    {j = 3;}
    @Override
    protected int getValue() {
        return j;
    }
}
public class ConstructorExample {
    public static void main(String args) {
        Bar bar = new Bar();
        System.out.println(bar.getValue());             -----------(3)
    }
}
/* Output: 
            2
            0
            2
 */
  • 1、一个实例变量在对象初始化的过程中会被赋值几次?
      我们知道,JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。如果我们在声明实例变量x的同时对其进行了赋值操作,那么这个时候,这个实例变量就被第二次赋值了。如果我们在实例代码块中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。如果我们在构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。也就是说,在Java的对象初始化过程中,一个实例变量最多可以被初始化4次。
  • 2、类的初始化过程与类的实例化过程的异同?
      类的初始化是指类加载过程中的初始化阶段对类变量按照程序猿的意图进行赋值的过程;而类的实例化是指在类完全加载到内存中后创建对象的过程。
  • 3、假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?
      我们知道,要想创建一个类的实例,必须先将该类加载到内存并进行初始化,也就是说,类初始化操作是在类实例化操作之前进行的,但并不意味着:只有类初始化操作结束后才能进行类实例化操作。实例初始化不一定要在类初始化结束之后才开始初始化。
package com.liuwen.JVM虚拟机.类加载;
/**
 * @description:      实例初始化不一定要在类初始化结束之后才开始初始化。
 * @author: Liu Wen
 * @create: 2020-03-17 00:32
 **/
public class StaticTest {
    public static void main(String[] args) {
        staticFunction();
    }

    static StaticTest st = new StaticTest();
    static {
        System.out.println("1");             //静态代码块
    }

    {
        System.out.println("2");             //实例代码块
    }

    StaticTest() {                           //实例构造器
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b);
    }

    public static void staticFunction() {
        System.out.println("4");
        System.out.println(b);
    }
    int a = 110;                             //实例变量
    static int b = 112;                      //静态变量
 }
 /*
   在类的初始化阶段需要做的是执行类构造器<clinit>()。类构造器本质上是编译器收集所有静态语句块和类变量的
赋值语句按语句在源码中的顺序合并生成类构造器<clinit>()。因此,对上述程序而言,JVM将先执行第一条静态变量
的赋值语句在实例化上述程序中的st变量时,实际上是把实例初始化嵌入到了静态初始化流程中,并且在上面的程序中,
嵌入到了静态初始化的起始位置。这就导致了实例初始化发生在静态初始化静态方法之前,当然,这也是导致a为110,
b为0的原因。
 */
//因此执行顺序如下:
/*
public class StaticTest {
    <clinit>(){                    //执行clinit()
        a = 110;                   // 实例变量
        System.out.println("2");   // 实例代码块
        System.out.println("3");   // 实例构造器
        System.out.println("a=" + a + ",b=" + b);
        System.out.println("1");   // 静态代码块
        System.out.println("4");   // 方法调用
        System.out.println(b);
    }
}
*/

8.反射

   What(是什么?) Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。而这也是Java被视为动态(或准动态,为啥要说是准动态,因为一般而言的动态语言定义是程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言。从这个观点看,Perl,Python,Ruby是动态语言,C++,Java,C#不是动态语言。)语言的一个关键性质。
   Why(为什么?) 我们知道反射机制允许程序在运行时取得任何一个已知名称的class的内部信息,包括包括其modifiers(修饰符),fields(属性),methods(方法)等,并可于运行时改变fields内容或调用methods。那么我们便可以更灵活的编写代码,代码可以在运行时装配,无需在组件之间进行源代码链接,降低代码的耦合度;还有动态代理的实现等等;但是需要注意的是反射使用不当会造成很高的资源消耗!
  获取class的三种方式:(一个类在 JVM 中只会有一个 Class 实例,因此c1= c2= c3)
1、通过对象调用 getClass() 方法来获取,通常应用在:比如你传过来一个 Object
类型的对象,而我不知道你具体是什么类,用这种方法
    Person p1 = new Person();Class c1 = p1.getClass();
2、直接通过 类名.class 的方式得到,该方法最为安全可靠,程序性能更高
这说明任何一个类都有一个隐含的静态成员变量 class
    Class c2 = Person.class;
3、通过 Class 对象的 forName() 静态方法来获取,用的最多,
但可能抛出 ClassNotFoundException 异常
    Class c3 = Class.forName(“com.ys.reflex.Person”);
通过 Class 类可以获取成员变量、成员方法、接口、超类、构造方法等。常用的方法如下:
  getName():获得类的完整名字。
  getFields():获得类的public类型的属性。
  getDeclaredFields():获得类的所有属性。包括private 声明的和继承类
  getMethods():获得类的public类型的方法。
  getDeclaredMethods():获得类的所有方法。包括private 声明的和继承类
  getMethod(String name, Class[] parameterTypes):获得类的特定方法,name参数指定方法的名字,parameterTypes 参数指定方法的参数类型。
  getConstructors():获得类的public类型的构造方法。
  getConstructor(Class[] parameterTypes):获得类的特定构造方法,parameterTypes 参数指定构造方法的参数类型。
  newInstance():通过类的不带参数的构造方法创建这个类的一个对象。
暴力反射:获取私有属性和方法的方式称为暴力反射,但是这是不建议的。

反射的使用场景:
  比如对于Tomcat而言,它并不知道我们会有什么样的方法,这些都只是在项目被部署进webapp下后才确定的,由此分析,必然用到了Java的反射来实现类的动态加载、实例化、获取方法、调用方法。Tomcat需要根据请求调用方法,动态地加载方法所在的类,完成类的实例化并通过该实例获得需要的方法,最终将请求传入方法执行。
  灵活使用反射能让我们代码更加灵活,这里比如JDBC原生代码注册驱动、动态代理、MyBatis的实体类、Spring 的 AOP等等都有反射的实现。但是凡事都有两面性,反射也会消耗系统的性能,增加复杂性等,合理使用才是真!
动态代理:https://blog.csdn.net/qq_41822345/article/details/105108955
MyBatis的实体类:https://blog.csdn.net/qq_41822345/article/details/104688219
Spring 的 AOP:https://blog.csdn.net/qq_41822345/article/details/104983468

JVM常见面试题3:https://blog.csdn.net/qq_41822345/article/details/104531640

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进击的程序猿~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值