1.JVM 为什么可以跨平台
JVM 能跨计算机体系结构(操作系统)来执行 Java 字节码( JVM 字节码指令集),屏蔽可与各个计算机平台相关的软件或者硬件之间的差异,使得与平台相关的耦合统一由 JVM 提供者来实现。
指令集:计算机所能识别的机器语言的命令集合。
每个运行中的 Java 程序都是一个 JVM 实例。
2.描述 JVM 体系结构
(1) 类加载器:JVM 启动时或者类运行时将需要的 class 加载到 JVM 中。每个被装载的类的类型对应一个 Class 实例,唯一表示该类,存于堆中。
(2) 执行引擎:负责执行 JVM 的字节码指令( CPU )。执行引擎是 JVM 的核心部分,作用是解析字节码指令,得到执行结果(实现方式:直接执行,JIT(just in time)即时编译转成本地代码执行,寄存器芯片模式执行,基于栈执行)。本质上就是一个个方法串起来的流程。每个 Java 线程就是一个执行引擎的实例,一个 JVM 实例中会有多个执行引擎在工作,有的执行用户程序,有的执行 JVM 内部程序( GC ).
(3) 内存区:模拟物理机的存储、记录和调度等功能模块,如寄存器或者 PC 指针记录器。存储执行引擎执行时所需要存储的数据。
(4) 本地方法接口:调用操作系统本地方法返回结果。
3.描述 JVM 工作机制
机器如何执行代码:源代码-预处理器-编译器-汇编程序-目标代码-链接器-可执行程序。
Java 编译器将高级语言编译成虚拟机目标语言。
JVM 执行字节码指令是基于栈的架构,所有的操作数必须先入栈,然后根据操作码选择从栈顶弹出若干元素进行计算后将结果压入栈中。
通过 Java 编译器将源代码编译成虚拟机目标语言,然后通过 JVM 执行引擎执行。
4.为何 JVM 字节码指令选择基于栈的结构
JVM 要设计成平台无关性,很难设计统一的基于寄存器的指令。
为了指令的紧凑性,让编译后的 class 文件更加紧凑,提高字节码在网络上的传输效率。
5.描述执行引擎的架构设计
创建新线程时,JVM 会为这个线程创建一个栈,同时分配一个 PC 寄存器(指向第一行可执行的代码)。调用新方法时会在这个栈上创建新的栈帧数据结构。执行完成后方法对应的栈帧将消失,PC 寄存器被销毁,局部变量区所有值被释放,被 JVM 回收。
6.描述 javac 编译器的基本结构
Javac 编译器的作用是将符合 Java 语言规范的的源代码转换成 JVM 规范的 Java 字节码。
(1) 词法分析器组件:找出规范化的 Token 流
(2) 语法分析器组件:生成符合 Java 语言规范的抽象语法树
(3) 语义分析器组件:将复杂的语法转化成最简单的语法,注解语法树
(4) 代码生成器组件:将语法树数据结构转化成字节码数据结构
7.描述 JVM 编译优化
早期(编译器):
很少;编译时,为节省常量池空间,能确定的相同常量只用一个引用地址。
晚期(运行期):
方法内联:去除方法调用的成本;为其他优化建立良好基础,便于在更大范围采取连续优化的手段。
冗余访问消除:公共子表达式消除
复写传播:完全相等的变量可替代
无用代码消除:清除永远不会执行的代码
(1) 公共子表达式消除(语言无关):如果公共子表达式已经计算过了,并且没有变化,那就没有必要再次计算,可用结果替换。
(2) 数组边界检查消除(语言相关):限定循环变量在取值范围之间,可节省多次条件判断。
(3) 方法内联(最重要):去除方法调用的成本;为其他优化建立良好基础,便于在更大范围采取连续优化的手段。
(4) 逃逸分析(最前沿):分析对象的动态作用域;变量作为调用参数传递到其他方法中-方法逃逸;被外部线程访问-线程逃逸。
栈上分配-减少垃圾系统收集压力
同步消除-如果无法逃逸出线程,则可以消除同步
标量替换-将变量恢复原始类型来访问
小抄:final 修饰的局部变量和参数,在常量池中没有符号引用,没有访问标识,对运行期是没有任何影响的,仅仅保证其编译期间的不变性。
8.ClassLoader (类加载器)有哪些
(1) Bootstrap ClassLoader(启动类加载器):完全由 JVM 控制,加载 JVM 自身工作需要的类( JAVA_HOME/lib )
(2) Extension ClassLoader(扩展类加载器):属于 JVM 自身一部分,不是 JVM 自身实现的(JAVA_HOME/lib/ext)
(3) Appclication ClassLoader(应用程序类加载器):父类是 Extension ClassLoader,加载 Classpath (用户类路径)上的类库
9.描述 ClassLoader 的作用(什么是类加载器)和加载过程
将 Class 文件加载到 JVM 中、审查每个类由谁加载(父优先的等级加载机制)、将 Class 字节码重新解析成 JVM 统一要求的对象( Class 对象)格式。
.class->findclass->Liking:Class 规范验证、准备、解析->类属性初始化赋值(static 块的执行)->Class 对象(这也就是为什么静态块只执行一次)
10.描述 JVM 类加载机制
ClassLoader 首先不会自己尝试去加载类,而是把这个请求委托给父类加载器完成,每一个层次都是。只有当父加载器反馈无法完成请求时(在搜索范围内没有找到所需的类),子加载器才会尝试加载(等级加载机制、父优先、双亲委派)。
好处:类随着它的加载器一起具有一种带有优先级的层次关系;保证同一个类只能被一个加载器加载。
11.JVM 加载 class 文件到内存的两种方式
(1) 隐式加载:继承或者引用的类不在内存中
(2) 显式加载:代码中通过调用 ClassLoader 加载
12.加载类错误分析及其解决
(1) ClassNotFoundException:没有找到对应的字节码(.class)文件;检查 classpath 下有无对应文件
(2) NoClassDefFoundError:隐式加载时没有找到,ClassNotFoundException 引发 NoClassDefFoundError ;确保每个类引用的类都在 classpath 下
(3) UnsatisfiedLinkError:(未满足链接错误)删除了 JVM 的某个 lib 文件或者解析 native 标识的方法时找不到对应的本地库文件
(4) ClassCastException:强制类型转换时出现这个错误;容器类型最好显示指明其所包含对象类型、先 instanceof 检查是不是目标类型,再类型转换
(5) ExceptionInitializerError:给类的静态属性赋值时
13:Java 应不应该动态加载类( JVM 能不能动态加载类)
JVM 中对象只有一份,不能被替换,对象的引用关系只有对象的创建者持有和使用,JVM 不可干预对象的引用关系,因为 JVM 不知道对象是怎么被使用的,JVM 不知道对象的运行时类型,只知道编译时类型。
但是可以不保存对象的状态,对象创建和使用后就被释放掉,下次修改后,对象就是新的了( JSP )。
14.Java 中哪些组件需要使用内存
(1) Java 堆:存储 Java 对象
(2) 线程:Java 运行程序的实体
(3) 类和类加载器:存储在堆中,这部分区域叫永久代( PermGen 区)
(4) NIO:基于通道和缓冲区来执行 I/O 的新方式。
(5) JNI:本地代码可以调用 Java 方法,Java 方法也可以调用本地代码
15.描述 JVM 内存结构及内存溢出。
JVM 是按照运行时数据的存储结构来划分内存结构的。
PC 寄存器数据:严格来说是一个数据结构,保存当前正在执行的程序的内存地址。为了线程切换后能恢复到正确的执行位置,线程私有。不会内存溢出。
(1) Java 栈:方法执行的内存模型,存储线程执行所需要的数据。线程私有。 –OutOfMemoryError:JVM 扩展栈时无法申请到足够的空间。一个不断调用自身而不会终止的方法。
–StackOverflowError:请求的栈深度大于 JVM 所允许的栈深度。创建足够多的线程。
(2) 堆:存储对象,每一个存在堆中 Java 对象都是这个对象的类的副本,复制包括继承自他父类的所有非静态属性。线程共享。
–OutOfMemoryError:对象数量到达堆容量限制。可通过不断向 ArrayList 中添加对象实现。
(3) 方法区:存储类结构信息。包括常量池(编译期生产的各种字面量和符号引用)和运行时常量池。线程共享。
–OutOfMemoryError:同运行时常量池。
(4) 本地方法栈:与 Java 栈类似,为 JVM 运行 Native 方法准备的空间。线程私有。( C 栈) OutOfMemoryError 和 StackOverflowError 同 JVM 栈。
(5) 运行时常量池:代表运行时每个 class 文件中的常量表。运行期间产生的新的常量放入运行时常量池。
–OutOfMemoryError:不断向 List 中添加字符串,然后 String.inern(),PermGen Space(运行时常量池属于方法区)。
(6)本地直接内存:即 NIO。
–OutOfMemoryError:通过直接向操作系统申请分配内存。
16.描述 JVM 内存分配策略
(1) 对象优先分配在 Eden
(2) 大对象直接进入老年代
(3) 长期存活的对象将进入老年代
(4) 幸存区相同年龄对象的占幸存区空间的多于其一半,将进入老年代
(5) 空间担保分配(老年代剩余空间需多于幸存区的一半,否则要 Full GC )
17.描述 JVM 如何检测垃圾
通过可达性分析算法,通过一些列称为 GC Roots 的对象作为起始点,从这些起始点向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连( GC Roots 到这个对象不可达),则证明这个对象是不可用的。
使用可达性分析算法而不是引用计数算法。因为引用计数算法很难解决对象之间相互循环引用的问题。
18.哪些元素可作为 GC Roots
(1) JVM 栈(栈帧中的本地变量表)中的引用
(2) 方法区中类静态属性引用
(3) 方法区中常量引用
(4) 本地方法栈中 JNI(一般的 Native 方法)引用
19.描述分代垃圾收集算法的思路:
把对象按照寿命长短来分组,分为年轻代和老年代,新创建的在老年代,经历几次回收后仍然存活的对象进入老年代,老年代的垃圾频率不像年轻代那样频繁,减少每次收集都去扫描所有对象的数量,提高垃圾回收效率。
20.描述基于分代的堆结构及其比例。
(1) 年轻代( Young 区-1/4 ):Eden+Survior ( 1/8,这个比例保证只有 10%的空间被浪费,保证每次回收都只有不多于 10%的对象存活)=From+To,存放新创建的对象.
(2) 老年代( Old 区 ):存放几次垃圾收集后存活的对象
(3) 永久区( Perm 区):存放类的 Class 对象
21.描述垃圾收集算法
(1) 标记-清除算法:首先标记处所要回收的对象,标记完成后统一清除。缺点:标记效率低,清除效率低,回收结束后会产生大量不连续的内存碎片(没有足够连续空间分配内存,提前触发另一次垃圾回收)。适用于对象存活率高的老年代。
(2) 复制算法(Survivor 的 from 和 to 区,from 和 to 会互换角色):
将内存容量划分大小相等的两块,每次只使用其中一块。一块用完,就将存活的对象复制到另一块,然后把使用过的一块一次清除。不用考虑内存碎片,每次只要移动顶端指针,按顺序分配内存即可,实现简单运行高效。适用于新生代。
缺点:内存缩小为原来的一般,代价高。浪费 50%的空间。
(3) 标记-整理算法: 标记完成后,将存活的对象移动到一端,然后清除边界以外的内存。适用于对象存活率高的老年代。
22.描述新生代和老年代的回收策略
Eden 区满后触发 minor GC,将所有存活对象复制到一个 Survivor 区,另一 Survivor 区存活的对象也复制到这个 Survivor 区中,始终保证有一个 Survivor 是空的。
Toung 区 Survivor 满后触发 minor GC 后仍然存活的对象存到 Old 区,如果 Survivor 区放不下 Eden 区的对象或者 Survivor 区对象足够老了,直接放入 Old 区,如果 Old 区放不下则触发 Full GC。
Perm 区满将触发 Major GC。
23.描述 CMS 垃圾收集器
CMS 收集器:Concurrent Mark Sweep 并发标记-清除。重视响应速度,适用于互联网和 B/S 系统的服务端上。初始标记还是需要 Stop the world 但是速度很快。缺点:CPU 资源敏感,无法浮动处理垃圾,会有大量空间碎片产生。
24.Java 应不应该动态记载类
Java 的优势正是基于共享对象的机制,达到信息的高度共享,也就是通过保存并持有对象的状态而省去类信息的重复创建和回收。对象一旦被创建,就可以被人持有和利用。动态加载理论上可以直接替换这个对象,然后更新 Java 栈中所有对原对象的引用关系,但是仍然不可行,因为这违反了 JVM 的设计原则,对象的引用只有对象的创建者持有和使用,JVM 并不可以干预对象的引用关系,因为 JVM 并不知道对象是怎么办使用的,JVM 并不知道对象的运行时类型而只知道编译时类型。
假如一个对象的属性结构被修改,但在运行时其他对象可能仍然引用该属性。
但是可以采取不保存对象的状态的解决办法,对象被创建后使用后就被释放掉,下次修改后,对象也就是新的了。JSP 和其他解释型语言都是如此。