前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试被问:运行一个HelloWorld程序JVM都经历了什么

面试被问:运行一个HelloWorld程序JVM都经历了什么

作者头像
田维常
发布2020-03-26 10:02:01
6040
发布2020-03-26 10:02:01
举报

面试官:别紧张,简答说一下运行一个HelloWorld程序JVM都经历了什么

首先说一下类加载时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:

  • 加载(Loading)
  • 连接(Linking):验证(Verification),准备(Preparation),解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

虚拟机规范严格要求有且仅有5种情况必须立即对类进行“初始化”

  • 遇到new,getstatic,putstatic或invokestatic这四条字节码指令的时候,且类没有被初始化过
    • 使用new实例化对象的时候
    • 读取或者设置一个类的静态字段(被final修饰,已在编译期把结果放到常量池的静态字段除外)
    • 调用一个类的静态方法的时候。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果这个类没有进行过初始化
  • 当初始化一个类,发现其父类还没有进行初始化,需要先触发父类的初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类)
  • 当使用JDK 1.7的动态语言支持,如果一个java.lang.invoke.MethodHandle实例最后解析结果是REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化。

接下来说一下类加载过程

类加载

加载是类加载过程的一个阶段,在加载过程虚拟机需要完成3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将一个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
数据类的创建

一个数组类创建过程遵循以下规则:

  • 如果数据的组件类型是引用类型,那就递归的加载这个组件类型
  • 如果数组的组件类型不是引用类型(例如int[]数组),Java虚拟机将会把数组C标记为与引导类加载器关联
  • 数据类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public

数据验证

  1. 文件格式验证
  2. 元数据验证
    1. 这个类是否有父类
    2. 这个类的父类是否继承了不允许被继承的类
    3. 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
    4. 类中的字段,方法是否和父类产生矛盾
  3. 字节码验证
  4. 符号引用验证
    1. 通过字符串描述的全限定名是否能找到对应的类
    2. 在指定类中是否存在符合方法的字段的描述符以及简单名称所描述的方法和字段
    3. 符号引用的类,字段,方法的访问性是否可被当前类访问

注:如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类:java.lang.IllegalAccessError, java.lang.NoSuchFieldError, java.lang.NoSuchMethodError等。

通过-Xverify:none参数可以关闭大部分类验证措施

程序准备阶段

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。(是类变量即static变量,不是实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中)。

代码语言:javascript
复制
public static int value = 123;

value在准备阶段后的初始值是0而不是123,因为这时候还没有开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法中,所以把value赋值为123的动作将在初始化阶段才会执行。

代码语言:javascript
复制
public static final int value = 123;

注:如果被final修饰,字段属性表会存在ConstantValue属性,那么准备阶段变量value就会被初始化成123.

所有基本类型的零值:

数据类型零值int0long0Lshort(short)0char'\u0000'byte(byte)0booleanfalsefloat0.0fdouble0.0drefrencenull

class文件解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程,在Class文件格式中它以CONSTANT_Class_info, CONSTANT_Fiedref_info, CONTSTANT_Methodref_info等类型的常量出现,那解析阶段中所说的直接引用与符号引用又有什么关联呢?

  • 符号引用:以一组符号来描述所引用的目标,与虚拟机实现的内存布局无关。
  • 直接引用:是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。与虚拟机实现的内存布局相关。

在执行了anewarray,checkcast,getfield,getstatic,instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual,ldc,ldc_w,multianewarray,new,putfield,putstatic这16个用于操作符号引用的字节码指令前,先对它们所使用的符号引用进行解析。

除了invokedynamic外,虚拟机实现可以对第一次解析的结果进行缓存。

类或接口解析

当前类为D,未解析符号引用为N,解析为一个类或者接口C的直接引用

  1. C非数组,把N的全限定名传给D的类加载器去加载C
  2. C是数组,并且数据元素为对象,那么按上面描述加载数据元素类型
  3. 上面步骤没有异常,C就是一个有效的类或者接口了,解析完成前,需要验证,确认D是否有对C的访问权限。
字段解析

解析一个未被解析的字段符号引用,首先将会对字段表内的class_index项索引的CONSTANT_Class_info符号引用进行解析,如果成功,将这个类或者接口用C表示

  1. 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  2. 否则,如果C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  3. 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父接口,如果父接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  4. 否则查找结束,抛出java.lang.NoSuchFieldError异常。

成功返回后,会对这个字段做权限验证。

类方法解析

与字段解析类似。

  1. 类方法和接口方法符号引用的常量类型定义是分开的,如果类方法表中发现class_index中索引的C是个接口,那直接抛出java.lang.IncompatibleClassChangeError
  2. 在类C中查找是否有简单名称和描述符都和目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
  3. 否则,在类C的父类中递归查找是否有简单名称和描述符都和目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
  4. 否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明C是一个抽象类,这是查找结束抛出java.lang.AbstractMethodError
  5. 否则,查找失败,抛出java.lang.NoSuchMethodError

成功返回后,会对方法做权限验证。

接口方法解析
  1. 与类方法解析不同,如果在接口方法表中发现class_index中的索引C是一个类而不是接口,那么直接抛出java.lang.IncompatibleClassChangeError
  2. 否则,接口C中查找是否有简单名称和描述符都和目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
  3. 否则,在接口C的父接口中递归查找,知道java.lang.Object类,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  4. 否则,查找失败,抛出java.lang.NoSuchMethodError

初始化

  • ()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static块)中的语句合并而成,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
    • 静态语句块只能防伪到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
  • ()方法与类的构造函数()不同,它不需要显式的调用父类的构造器,虚拟机会保证子类的()方法之前,父类的()方法执行完毕。因此虚拟机中第一个被执行的()方法类肯定是java.lang.Object
  • 由于父类的()方法先执行,也就意味着父类的静态语句块会优先于子类赋值
  • ()方法对于类或者接口不是必须的,没有静态语句块也没有赋值的话,编译器可以不为这个类生成()方法
  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成()方法,但是接口和类不同的是,执行接口的()方法不需要先执行父接口的()方法。只有当父接口中定义的变量使用时,父接口才会被初始化。
  • 虚拟机会保证一个类的()方法方法在多线程环境中被正确的加锁,同步。如果一个类的()方法方法中有耗时很长的操作,有可能造成多线程阻塞。

类加载器

用于实现类加载动作。

类和类加载器

比较两个类是否相等,只有在这两个类是同一个类加载器加载的时候才有意义,否则即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要它们的类加载器不同,这两个类就必然是不等的。

双亲委派模型

如果一个类加载器收到了类加载请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,以此类推。只有当父类加载器反馈自己无法加载这个加载请求的时候,子加载器才会尝试自己去加载。

好处:具备了一种带有优先级的层次关系

破坏双亲委派模型

线程上下文类加载器(Thread Context ClassLoader)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java后端技术栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 首先说一下类加载时机
  • 接下来说一下类加载过程
    • 类加载
      • 数据类的创建
    • 数据验证
      • 程序准备阶段
        • class文件解析
          • 类或接口解析
          • 字段解析
          • 类方法解析
          • 接口方法解析
        • 初始化
        • 类加载器
          • 类和类加载器
            • 双亲委派模型
              • 破坏双亲委派模型
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档