面试类-JVM原理(四)

说说解释执行和编译执行的区别(补充)

先说解释和编译的区别:

  • 解释:将源代码逐行转换为机器码。
  • 编译:将源代码一次性转换为机器码。

一个是逐行,一个是一次性,再来说说解释执行和编译执行的区别:

  • 解释执行:程序运行时,将源代码逐行转换为机器码,然后执行。
  • 编译执行:程序运行前,将源代码一次性转换为机器码,然后执行。

Java 一般被称为“解释型语言”,因为 Java 代码在执行前,需要先将源代码编译成字节码,然后在运行时,再由 JVM 的解释器“逐行”将字节码转换为机器码,然后执行。

这也是 Java 被诟病“慢”的主要原因。

但 JIT 的出现打破了这种刻板印象,JVM 会将热点代码(即运行频率高的代码)编译后放入 CodeCache,当下次执行再遇到这段代码时,会从 CodeCache 中直接读取机器码,然后执行。这大大提升了 Java 的执行效率。

  1. Java 面试指南(付费)    收录的腾讯 Java 后端实习一面原题:说说 Java 解释执行的流程。

42.能说一下类的生命周期吗?

一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading),其中验证、准备、解析三个部分统称为连接(Linking)。

类的生命周期
                                                                                类的生命周期

43.类加载的过程知道吗?

除去使用和卸载,就是 Java 的类加载过程。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。

参考:一文彻底搞懂 Java 类加载机制open in new window

载入

载入过程中,JVM 需要做三件事情:

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

载入阶段结束后,Java 虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区中了,方法区中的数据存储格式完全由虚拟机实现自行定义,《Java 虚拟机规范》未规定此区域的具体数据结构。

验证

JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。

准备

VM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化,对应数据类型的默认初始值,如 0、0L、null、false 等。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。

初始化

该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法(javap 中看到的 <clinit>() 方法)的过程。

44.类加载器有哪些?

主要有四种类加载器:

  • 启动类加载器(Bootstrap ClassLoader)用来加载 java 核心类库,无法被 java 程序直接引用。

  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

  • 用户自定义类加载器 (user class loader),用户通过继承 java.lang.ClassLoader 类的方式自行实现的类加载器。

45.什么是双亲委派机制?

 

双亲委派模型

                                                                                     双亲委派模型

双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。

46.为什么要用双亲委派机制?

答案是为了保证应用程序的稳定有序。

例如类 java.lang.Object,它存放在 rt.jar 之中,通过双亲委派机制,保证最终都是委派给处于模型最顶端的启动类加载器进行加载,保证 Object 的一致。反之,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中就会出现多个不同的 Object 类。

47.如何破坏双亲委派机制?

如果不想打破双亲委派模型,就重写 ClassLoader 类中的 fifindClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。而如果想打破双亲委派模型则需要重写 loadClass()方法。

48.历史上有哪几次双亲委派机制的破坏?

双亲委派机制在历史上主要有三次破坏:

双亲委派模型的三次破坏

                                                                                       双亲委派模型的三次破坏

第一次破坏

双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即 JDK 1.2 面世以前的“远古”时代。

由于双亲委派模型在 JDK 1.2 之后才被引入,但是类加载器的概念和抽象类 java.lang.ClassLoader 则在 Java 的第一个版本中就已经存在,为了向下兼容旧代码,所以无法以技术手段避免 loadClass()被子类覆盖的可能性,只能在 JDK 1.2 之后的 java.lang.ClassLoader 中添加一个新的 protected 方法 findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在 loadClass()中编写代码。

第二次破坏

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,如果有基础类型又要调用回用户的代码,那该怎么办呢?

例如我们比较熟悉的 JDBC:

各个厂商各有不同的 JDBC 的实现,Java 在核心包\lib里定义了对应的 SPI,那么这个就毫无疑问由启动类加载器加载器加载。

但是各个厂商的实现,是没办法放在核心包里的,只能放在classpath里,只能被应用类加载器加载。那么,问题来了,启动类加载器它就加载不到厂商提供的 SPI 服务代码。

为了解决这个问题,引入了一个不太优雅的设计:线程上下文类加载器 (Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

JNDI 服务使用这个线程上下文类加载器去加载所需的 SPI 服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为。

第三次破坏

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,例如代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。

OSGi 实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。

49.你觉得应该怎么实现一个热部署功能?

我们已经知道了 Java 类的加载过程。一个 Java 类文件到虚拟机里的对象,要经过如下过程:首先通过 Java 编译器,将 Java 文件编译成 class 字节码,类加载器读取 class 字节码,再将类转化为实例,对实例 newInstance 就可以生成对象。

类加载器 ClassLoader 功能,也就是将 class 字节码转换到类的实例。在 Java 应用中,所有的实例都是由类加载器,加载而来。

一般在系统中,类的加载都是由系统自带的类加载器完成,而且对于同一个全限定名的 java 类(如 com.csiar.soc.HelloWorld),只能被加载一次,而且无法被卸载。

这个时候问题就来了,如果我们希望将 java 类卸载,并且替换更新版本的 java 类,该怎么做呢?

既然在类加载器中,Java 类只能被加载一次,并且无法卸载。那么我们是不是可以直接把 Java 类加载器干掉呢?答案是可以的,我们可以自定义类加载器,并重写 ClassLoader 的 findClass 方法。

想要实现热部署可以分以下三个步骤:

  • 1)销毁原来的自定义 ClassLoader
  • 2)更新 class 类文件
  • 3)创建新的 ClassLoader 去加载更新后的 class 类文件。

到此,一个热部署的功能就这样实现了。

50.Tomcat 的类加载机制了解吗?

Tomcat 是主流的 Java Web 服务器之一,为了实现一些特殊的功能需求,自定义了一些类加载器。

Tomcat 类加载器如下:

Tomcat类加载器

                                                                          Tomcat类加载器

Tomcat 实际上也是破坏了双亲委派模型的。

Tomact 是 web 容器,可能需要部署多个应用程序。不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。如多个应用都要依赖 hollis.jar,但是 A 应用需要依赖 1.0.0 版本,但是 B 应用需要依赖 1.0.1 版本。这两个版本中都有一个类是 com.hollis.Test.class。如果采用默认的双亲委派类加载机制,那么无法加载多个相同的类。

所以,Tomcat 破坏了双亲委派原则,提供隔离的机制,为每个 web 容器单独提供一个 WebAppClassLoader 加载器。每一个 WebAppClassLoader 负责加载本身的目录下的 class 文件,加载不到时再交 CommonClassLoader 加载,这和双亲委派刚好相反。

posted @ 2024-03-26 11:15  猿码哥  阅读(5)  评论(0编辑  收藏  举报