Java:JVM知识点梳理
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和其他解释型语言都是如此。