【面试:JVM】
说说Java虚拟机的生命周期及体系结构?
参见:Java虚拟机生命周期和体系结构
Java 是如何实现跨平台的?
注意:跨平台的是 Java 程序,而不是 JVM。 JVM 是用 C/C++ 开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的 JVM。
所谓“跨平台”,即实现“一次编译,到处运行”:
- 我们编写的 Java 源码,编译后会生成一种 .class 文件,称为字节码文件;
- Java 虚拟机(JVM)负责将字节码文件翻译成特定平台下的机器码然后运行。
也就是说,只要在不同平台上安装对应的 JVM,就可以运行字节码文件,运行我们编写的 Java 程序,这个过程,我们编写的 Java 程序没有做任何改变,仅仅是通过 JVM 这一“中间层”,就能在不同平台上运行(不同平台使用不同的 JVM 实现),从而实现“跨平台”。
什么是 JVM ?
JVM,即 Java 虚拟机(“Java Virtual Machine”)。它通过模拟一个计算机来达到一个计算机所具有的的计算功能。JVM 能够跨计算机体系结构来执行 Java 字节码,主要是由于 JVM 屏蔽了与各个计算机平台相关的软件或者硬件之间的差异,使得与平台相关的耦合统一由 JVM 提供者来实现。
JVM 由哪些部分组成?
JVM 的结构基本上由 4 部分组成:
- 类加载器,在 JVM 启动时或者类运行时将需要的 class 加载到 JVM 中。
- 执行引擎,执行引擎的任务是负责执行 class 文件中包含的字节码指令,相当于实际机器上的 CPU。
- 内存区,将内存划分成若干个区以模拟实际机器上的存储、记录和调度功能模块,如实际机器上的各种功能的寄存器或者 PC 指针的记录器等。
- 本地方法调用,调用 C 或 C++ 实现的本地方法的代码返回结果。
说一说类加载器?
Java虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称作虚拟机的类加载机制。 参见:深入理解JVM:虚拟机类加载机制
类的生命周期
类的生命周期:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading);
类加载的过程
类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。 一般来说,Java 虚拟机使用 Java 类的方式如下: 1、Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。 2、类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance() 方法就可以创建出该类的一个对象。 实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
类加载的过程:
- 加载
- 验证
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
- 准备
- 解析
- 初始化
类加载器
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。
对于 JVM 而言,只存在两种不同的类加载器:
- “启动类加载器”(BootstrapClassLoader):这个类加载器使用 C++ 语言实现(对于Hotspot而言),是虚拟机自身的一部分;
- 其他所有的类加载器:这些类加载器都由 Java 语言实现,独立存在于虚拟机外部,并且全都继承自抽象类“java.lang.ClassLoader”。
对于 Java 开发者来说,可以进一步划分为“三层类加载器”:
- 启动类加载器(Bootstrap Class Loader):负责加载存放在“<JAVA_HOME>\lib”目录,或者被“-Xbootclasspath”参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库加载到虚拟机的内存中。
- JVM 按照文件名识别类库(如rt.jar、tools.jar),名字不符合的类库即使放在lib目录中也不会被加载;
- 启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可。
- 扩展类加载器(Extension Class Loader):它负责加载“<JAVA_HOME>\lib\ext”目录中,或者被“java.ext.dirs”系统变量所指定的路径中所有的类库。
- 这个类加载器是在类“sun.misc.Launcher$ExtClassLoader”中以Java代码的形式实现的。
- 由于扩展类加载器是由 Java 代码实现的,开发者可以直接在程序中使用扩展类加载器来加载 Class 文件。
- 应用程序类加载器(Application Class Loader):它负责加载用户类路径(ClassPath)上所有的类库。
- 由于应用程序类加载器是“ClassLoader”类中的“getSystemClassLoader()”方法的返回值,所以有些场合中也称它为“系统类加载器”。
- 这个类加载器由“sun.misc.Launcher$AppClassLoader”来实现。
- 如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。开发者同样可以直接在代码中使用这个类加载器。
双亲委派模型
双亲委派模型的工作过程: 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
- 要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
- 类加载器之间的父子关系,通常使用组合(Composition)而非继承(Inheritance)的关系来复用父加载器的代码。
- 优点:Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系。
例如:
- 类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都能够保证是同一个类。
- 反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中就会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。
- 如果写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现它可以正常编译,但永远无法被加载运行。
- 【如果使用自定义类加载器的 defineClass() 方法去加载一个以“java.lang”开头的类,会收到一个由 Java 虚拟机内部抛出的“java.lang.SecurityException:Prohibited package name:java.lang”异常。】
Java 虚拟机是如何判定两个 Java 类是相同的?
Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。 只有两者都相同的情况,才认为两个类是相同的。
即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。
- 比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器 ClassLoaderA 和 ClassLoaderB 分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class 类的实例来表示这个类:
- 这两个实例是不相同的;
- 对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。
类似-Xms、-Xmn这些参数的含义?
堆内存分配:
- -Xms:初始分配的内存,默认是物理内存的1/64
- -Xmx:最大分配的内存,默认是物理内存的1/4
- 默认空余堆内存小于 40% 时,JVM就会增大堆直到 -Xmx 的最大限制;空余堆内存大于 70% 时,JVM会减少堆直到 -Xms 的最小限制。
- 因此服务器一般设置 -Xms、-Xmx 相等以避免在每次 GC 后调整堆的大小。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。
非堆内存分配:
- -XX:PermSize:设置非堆内存初始值,默认是物理内存的1/64;
- -XX:MaxPermSize:设置最大非堆内存的大小,默认是物理内存的1/4。
- -Xmn2G:设置年轻代大小为2G。
- -XX:SurvivorRatio:设置年轻代中 Eden 区与 Survivor 区的比值。