02-01-01、JVM基础知识

原创
2020/02/23 02:01
阅读数 213
1、JVM
JVM即java虚拟机(Java Virtual Machine),是java语言的运行平台,java代码编译成符合 jvm规范字节码文件,然后由 jvm加载并执行
 
2、JVM是如何进行类加载的?类加载有哪些步骤?类的生命周期是什么样的?
类从被加载到JVM内存开始,到卸载出内存为止,其生命周期包括:
  • 加载(Loading):通过类的全限定名获取定义此类的二进制字节流,这个二进制流可以任意从哪里获取,可以从文件、网络、数据库、jsp文件、运行时动态生成都可以;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在Java堆中生成代表这个类的Class对象,作为方法区这些数据的访问接口。
  • 验证(Verification):检查二进制字节流是否符合JVM规范,以免危害虚拟机的执行;
  • 准备(Preparation)为类变量(即static修饰的变量)分配内存并设置类变量的初始值,这些内存都将在方法区中进行分配
  • 解析(Resolution):将常量池内的符号引用转换为直接引用;
  • 初始化(Initialization):就是执行类的构造方法;
  • 使用(Using)
  • 卸载(Unloading):使用完后实例被回收
 
 
3、JVM有哪些类型的类加载器?如何自定义类加载器?自定义类加载器使用在哪些场景?
启动类加载器(bootstrap):负责加载 <JavaHome>/lib目录下或 -Xbootclasspsth参数指定目录下的类,它是JVM的一部分,不能被应用程序调用;
 
扩展类加载器(Extention):负责加载 <JavaHome>/lib/ext目录下或 java.ext.dirs系统变量所指定的目录下的所有类库中的类,
 
应用类加载器(application):负责加载用户类路径下的类,也是默认的用户类加载器。
 
在jvm中,若应用加载器加载了一个a.b.c.xx.class的类,然后再通过一个用户自定义类加载器也加载a.b.c.xx.class这个类,那么这两个类是不同的两个类,只有同一个类加载器加载的类才是相同的类。
例如 在 main 方法中 通过自定义类加载器加载一个类 Foo.class,并获取其实例 fooObj,则boolean f = fooObj instanceof Foo; 表达式的结果是 false,因为此时表达式中的 Foo的类加载器是默认加载器; 若在 Foo.class 中实现一个方法判断实例类型并把fooObj传入,则 boolean f = fooObj instanceof Foo; 表达式的值为 true,因为此时表达式中的 Foo的类加载器是自定义的类加载器。
 
自定义类加载器只需继承ClassLoader 抽象类,然后实现findClass方法即可(不推荐直接实现loadClass()方法,因为在父类加载器加载失败后会自动去调用自己的findClass()方法,这样可以直接实现双亲委派)。因此扩展类加载器、应用类加载器、自定义类加载器之间不是继承关系,通常是以组合的方式(实现双亲委派模型)来实现类的加载。
 
当类字节码文件放在其他地方、或需要实现热加载更新时,就可以通过实现自定义类加载器来实现。
 
3.2、JVM类加载的双亲委派模型
当一个类接到一个类加载请求时,它首先不会自己尝试去加载这个类,而是先把这个请求委派给它的父类加载器去加载,只有当父类加载器反馈无法完整加载时,子类加载器才会自己尝试去加载。
双亲委派模型的好处是:java类随着它的类加载器一起,具有一种带有优先级的层次关系,可以避免相同的类被加载多次,使一个类在同一个环境中只使用的都是同一个类。
双亲委派模型的实现步骤:
先调用 findLoadedClass(name) 方法检查是否已经加载过,若已加载,直接返回加载过的class;若没有,如果能获取到父加载器(即父加载器不为空),调用父加载器的 loadClass(name,resove) 方法,如果没能获取到父加载器,则调用 findBootstrapClass(name) 方法 调用启动类加载器,若返回class则直接返回;若返回空或异常,则调用自定义的类加载方法进行加载。
双亲委派模型的作用:安全考虑,保护核心类库;防止类重复加载;
 
4、JVM的内存模型是什么样的?JMM(java内存模型)是什么样的?
程序计数器、方法区、堆、栈、本地方法栈,其中 堆区、方法区是所有线程共享的,虚拟机栈、本地方法栈、PC寄存器是线程独享的。
虚拟机栈是线程的栈空间,每创建一个线程就会在虚拟机栈中线程分配一定空间,存放栈帧。线程中执行的每个方法都会生成一个栈帧,栈帧中存放了方法的局部变量表、操作数栈、方法出口。
本地方法栈是存放的程序中调用(native)本地方法的相关数据及栈帧。
PC 存放的是指向当前执行的指令地址的指针
方法区存放的是常量 和 类(接口)、方法、字段的符号引用
存放生成的对象
 
5、JVM是如何进行内存管理(内存划分)的?
注:持久代在Java8中已经去掉,取而代之的是元空间(metaSpace),元空间使用的不是JVM内存,而是外内存,也就是直接内存。
 
6.0、GC如何判断一个对象可以回收?
通过可达性分析算法进行确定。即:
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
 
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
 
为什么不使用引用计数法判断对象可回收?
难以解决循环引用问题
 
引用分为:强引用、软引用、弱引用、虚引用,不同的引用类型会在不同的时机被GC回收
  • 强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。(常用于缓存的实现)
  • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。(ThreadLocal中有使用WeakReference实现ThreadLocalMap; WeakHashMap也是基于此实现的)
  • 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
 
GC将对象回收的过程(要真正宣告一个对象死亡,至少要经历两次标记过程):
从GC Root 开始,向下搜索,对对象进行可达性分析。如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
(如果一个对象没有覆写finalize()方法,被判定为“没有必要执行”finalize()方法,那又当如何?直接回收了?)
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
 
垃圾回收算法:
标记-清除算法
复制算法
标记-整理算法
分代收集算法
 
6、JVM有哪些常用的垃圾回收器?都是如何进行垃圾回收的?了解ZGC吗
Serial收集器:串行新生代收集器
ParNew收集器:par(Parallel - 并行)并行新生代收集器
Parallel Scavenge收集器:新生代收集器,使用复制算法实现,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 +垃圾收集时间),因此Paralle Scavenge 常称为 吞吐量优先收集器
Serial Old收集器:串行老年代收集器,“标记-整理”算法实现
Parallel Old收集器:并行老年代收集器,“标记-整理”算法实现,是parallel scavenge 的老年代版本。
CMS(Concurrent Mark Sweep)收集器:并行老年代收集器,“标记-清除”算法实现,是一种以获取最短回收停顿时间为目标的收集器,因此常称为 响应优先收集器
G1收集器:它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的,它既会标记-整理,也会标记-清除,因此不会产生内存碎片。
对于分代收集器,新生代收集器只负责回收年轻代堆空间,老年代收集器只负责回收老年代堆空间,因此新生代收集器和老年代收集器需要组合使用。常用的组合如上图。
 
 
G1可以独立使用,因为他不再区分新生代和老年代
 
7、GC包括哪几种?什么情况下会引发fullGC?
minorGC、FullGC
 
8、写一段代码让其触发3次minorGC和3次fullGC
//  TODO
 
9、工作中常用到哪些JVM参数?这些参数都有什么用?
JVM 参数是在执行java 命令是传递给jvm的参数,也是启动一个jvm进程时的参数,多个参数与参数之间用空格隔开;java 命令语法如下:
java  [ options classname  [ args ]
java  [ options -jar   filename  [ args ]
 
HotSpot JVM 包含两个编译器,一个是Client compiler ,另一个是Server compiler,当使用Cilent 模式运行时,使用的是Client compiler,它编译速度快;当使用Server模式运行时,使用Server compiler,它编译优化程度更高;当在server模式下开启分层编译时(1.7后默认是开启),两个编译器都使用,分层的第0层代码解释执行,第一层使用Cilent compiler编译,第二层使用Server compiler编译。
 
10、JVM性能调优你用到了哪些性能分析工具?怎么用的?
监控java应用(Monitor Java Applications)
jconsole 图形界面,监控本地或远程JVM进程
jvisualvm 图形界面,监控本地或远程JVM进程,可查看jvm参数、内存使用、线程、取样等
 
监控JVM(Monitor the JVM)
jps 查看jvm进程
jstat 收集jvm进程信息
jstatd
jmc Java Mission Control是一个性能分析,监视和诊断工具套件。
 
故障排除(Troubleshooting)
jcmd
jinfo 显示jvm配置信息
jhat 查看heapdump快照
jmap jvm内存快照
jsadebugd
jstack 查看线程栈快照
 
 
高阶(编译器优化)
JVM通过编译实现优化的几种优化方式:
  • 针对不同CPU型号对应的指令集进行特定的编译,即生成对应的机器码
  • 逃逸分析
  • 栈上分配
  • 热点代码编译缓存:热点代码有 多次被调用执行的代码,被多次执行的循环体
  • 方法内联
 
 
11、公共子表达式的消除、数组边界检查消除
公共子表达式消除是指编译器在进行编译时,会对方法中某些相同且值不会变的表达式进行合并,该表达式只需进行一次计算即可,如:
int r = (a+b) * c + d + (e * (a+b)) ,消除公共子表达式后可变成 int r = E * c + d + (e * E) , 其中 E = a + b 只需计算一次。
 
java是一门动态安全的语言,像在访问数组时若发生越界,会抛出相应的异常,访问null会抛NPE,而不会像C/C++那样,但java的动态安全是隐含了对边界检查的代码,也就是在进行这些操作时,都会对边界进行检查。每次都检查是消耗性能的,对于某些情况,如for循环等,不需要每次都检查,
 
12、内联
当方法解释执行调用次数达到 -XX:CompileThreshold= invocations 设置的次数后,将会进行编译执行,执行时会将一些热点小方法(方法体编译大小不大于 -XX:InlineSmallCode= size 的方法)直接内联编译到调用它的方法中,从而减少调用栈数量。
什么情况下可以内联?
当调用的方法大小小于设置的阈值,且调用是确定的,而不再需要再进行类型判断时,可以进行内联优化。
若是方法在调用时还需要进行类型判断,如重载方法、覆写方法都是无法进行内联的,而静态方法、私有方法则是可以内联的。
对于虚方法,编译器会使用 类型继承关系分析(CHA)技术进行内联优化。
 
栈上替换
对于被执行多次的循环体,jvm会将其认定为 热点代码,然后在执行的过程中编译循环体所在方法,因为是执行中,因此方法还在栈帧上,因此称为栈上替换(On Stack Replacement,简称为OSR编译)
 
 
13、逃逸分析(Escape Analysis)、方法逃逸、线程逃逸是怎么回事?如何分析?
逃逸分析并不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术。
当一个对象在方法中被创建后,如果它被方法外的对象引用,而方法执行完后该对象不能被回收,因此叫 方法逃逸
若这个对象被外部线程所引用,如赋值给类变量、或传递给其他线程执行,它的作用域已不再是原本的代码块,因此叫 线程逃逸
逃逸分析的基本行为就是 分析对象动态作用域
如果可以确定一个对象不会发生方法逃逸 和 线程逃逸,则可对对象进行如下优化:
栈上分配(Stack Allocation):一般情况下,对象是存储在堆中的,堆空间是所有线程共享的,只要有对对象的引用就可以访问到对象,对象不再使用后将会被垃圾收集器进行回收。如果能够确定一个对象不会逃逸,那么将对象分配在栈中,随着方法调用栈的出/入栈而创建销毁,那将减少GC压力。
 
同步消除(Synchronization Elimination):线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉。
 
标量替换(Scalar Replacement):标量(Scalar)是指一个数据已经无法再分解成更小的数据来表示了,Java虚拟机中的原始数据类型(int、long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量。相对的,如果一个数据可以继续分解,那它就称作聚合量(Aggregate),Java中的对象就是最典型的聚合量。如果把一个Java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,有很大的概率会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。
 
14、指令重排
优化,对指令进行重新排序,其执行顺序发生变化,但结果相同。
 
15、分支预测
分支预测是处理器在执行程序指令时,对于分支程序,为了提高执行速度,会根据前面执行的历史情况,预测分支向哪一个分支执行,并将下一步的指令预先读取,以提高效率。
分支预测技术可以提高处理器的效率,但这也是建立在预测的准确性上,若处理器的预测准确率很高,那么效率将非常高,换句话说, 如果程序指令具有很好的可预测性,那么执行效率将非常高,因此我们应该写具有更高可预测性的代码(如减少分支、使分支被执行的更集中)。如果处理器的预测准确率很低,预读取的指令还得重新读取,那将使效率变得很低。
 
16、裁剪未被选择的分支
// TODO
 
17、JVM的解释模式、编译模式、混合模式
Cilent model、Server model,当开启分层编译时,会同时使用Cilent compiler 和 Server compiler 进行编译,这就是混合模式。
可通过 -version 或 -showversion 参数查看运行模式
 
18、javac的编译过程
解析与填充符号表:1. 词法、语法分析;2.填充符号表,获得语法树。
注解处理:
语义分析与字节码生成:1.标注检查;2.数据及控制流分析;3.解语法糖;4.字节码生成。
 
关于JVM的学习,推荐看《深入理解Java虚拟机 JVM高级特性与最佳实践》
 
 
写得不好,说得不一定对,希望您带着批判性阅读,希望您能多提提宝贵意见,希望不误人子弟。也没很细致校对,错误之处望不吝指出,谢谢!

 

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部