在咱们面试中常常会遇到面试官问一些无关 JVM 的问题,上面我大略从运行时数据域、类加载机制、类加载器、垃圾收集器、垃圾收集算法、JVM 堆内存模型、JVM 内存构造、JVM 调优等几个方面来讲一下 JVM。
一、运行时数据区域
在执行 Java 程序的时候,JAVA 虚构机会将本人所治理的内存划分为若干个不同的数据区域,每个区域分工不同,这些区域统称为“运行时数据区域”。上面来依据一张图来看一下这几个区域。
1、程序计数器
1> 较小的内存空间。
2> 以后线程字节码的行号指示器。
3> 扭转计数器的值来选取下一条须要执行的字节码指令。
4> 一个处理器只会执行一条线程中的指令,为了线程切换后能复原到正确的执行地位,每个线程一个计数器。
2、Java 虚拟机栈
1> 线程公有。
2> 生命周期与线程雷同。
3> 每个办法执行都会创立栈帧,用于存储局部变量表、操作数栈、动静链接、办法进口等信息。
3、本地办法栈
1> 线程公有。
2> 应用到的本地(Native)办法服务。
4、Java 堆
1> 内存中最大的一块。
2> 线程共享。
3> 堆满了抛出 OutOfMemoryError 异样。
5、办法区
1> 线程共享。
2> 用于存储已被虚拟机加载的类型信息、常量、动态变量、及时编译器编译后的代码缓存等数据。
6、运行时常量池(次区域在办法区外部)
1> 运行时常量池是办法区的一部分。
2> 寄存各种字面量与符号援用。
二、垃圾收集算法
1、标记 - 革除算法
首先标记出所有须要回收的对象,在标记实现后,对立回收掉所有被标记的对象。如图。
2、标记 - 复制算法
将可用内存按容量划分为大小相等的两块,每次只应用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块下面,而后再把已应用过的内存空间一次清理掉。如图。
3、标记 - 整顿算法
标记 - 整顿算法就是标记 - 革除算法后再将存活对象整顿到一起,从而腾出来更大的间断空间。如图。
三、垃圾收集器
1、Serial 收集器
Serial 收集器是最根底、历史最悠久的收集器,这个收集器是一个单线程工作的收集器。
2、ParNew 收集器
ParNew 收集器本质上是 Serial 收集器的多线程并行版本,能够同时应用多条线程进行垃圾收集。
3、Parallel Scavenge 收集器
Parallel Scavenge 收集器是一款新生代收集器,它是基于标记 - 复制算法实现的收集器。Parallel Scavenge 收集器的指标则是达到一个可管制的吞吐量,所谓吞吐量就是处理器用于运行用户代码的工夫与处理器总耗费工夫的比值,即。
4、Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器。
5、Parallel Old 收集器
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,反对多线程并行收集,基于标记 - 整顿算法实现。
6、CMS 收集器
CMS 收集器是一种以获取最短回收进展工夫为指标的收集器,它的运作过程分为四个步骤,包含:
1> 初始标记
2> 并发标记
3> 从新标记
4> 并发革除
初始标记:初始标记须要 stw,初始标记仅仅只是标记一下 GC Roots 能间接关联到的对象,速度很快。
并发标记:并发标记阶段就是从 GC Roots 的间接关联对象开始遍历整个对象图的过程,这个过程耗时较长然而不须要进展用户线程,能够与垃圾收集线程一起并发运行。
从新标记:从新标记阶段则是为了修改并发标记期间,因用户程序持续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的进展工夫通常会比初始标记阶段稍长一些,但也远比并发标记阶段的工夫短。
并发革除:这个阶段清理删除掉标记阶段判断的曾经死亡的对象,因为不须要挪动存活对象,所以这个阶段也是能够与用户线程同时并发的。
6、Garbage First 收集器(简称 G1 收集器)
G1 不再保持固定大小以及固定数量的分代区域划分,而是把间断的 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都能够依据须要,表演新生代的 Eden 空间、Survivor 空间,或者老年代空间。如图。
G1 收集器的运作过程大抵能够划分为以下四个步骤:
初始标记:仅仅只是标记一下 GC Roots 能间接关联到的对象,
并发标记:从 GC Root 开始对堆中对象进行可达性剖析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
最终标记:对用户线程做另一个短暂的暂停,用于解决并发阶段完结后仍遗留下来的最初那大量的 SATB 记录。
筛选回收:负责更新 Region 的统计数据,对各个 Region 的回收价值和老本进行排序,依据用户所冀望的进展工夫来制订回收打算,能够自由选择任意多个 Region 形成回收集,而后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全副空间。这里的操作设计存活对象的挪动,是必须暂停用户线程,由多条收集器线程并行实现的。
四、类加载机制
当咱们运行一个类的时候,咱们会通过 java.exe 来调用 jvm.dll 文件,创立一个类加载器,而后再加载咱们的类,咱们的类在加载的过程中会经验加载、验证、筹备、解析、初始化。
加载:此阶段将类的二进制字节流加载到内存中。
验证:此阶段将验证文件的格局、元数据验证、字节码验证、符号利用验证,保障这些代码运行后不会危害虚拟机本身的平安。
筹备:筹备阶段是为类中定义的变量(即动态变量,被 static 润饰的变量)分配内存并设置类变量初始默认值。
解析:解析阶段是 Java 虚拟机将常量池内的符号援用替换为间接援用的过程,符号援用是用符号来形容所援用的指标,而间接援用是用地址来形容所利用的指标。
初始化:因为之前筹备阶段是给动态变量赋值默认值,而初始化阶段是给动态变量赋值他的实在的值。
五、类加载器
1、启动类加载器:这个类加载器负责加载寄存在 <JAVA_HOME>\lib 目录中的零碎的 jar。
2、扩大类加载器:这个类加载器负责加载寄存在 <JAVA_HOME>\lib\ext 目录中的扩大类 jar。
3、应用程序类加载器:这个类加载器负责加载咱们本人写的类。
六、双亲委派模型
后面咱们说到每个类会有一个对应的类加载器去加载这个类,而不同的类加载器所加载的类的类型不同,当咱们要加载一个类的时候首先会去应用程序类加载器加载过的汇合里(注:这里是加载过的汇合里)查看有没有加载这个类,如果没有就去扩大类加载器加载过的汇合里查看有没有加载过这个类,如果没有就再向下来找疏导类加载器加载过的汇合里看有没有加载过这个类,如果也没有就会从疏导类加载器要加载的外围类中寻找有没有要加载的类,如果没有就向下寻找扩大类加载器中要加载的扩大类中有没有,如果也没有就去应用程序加载器中寻找。
益处:
1、不会反复去加载一个类,如果应用程序类加载器加载过 Student 类,那么下载再加载这个类的时候只须要判断应用程序类加载过的汇合里有没有加载过这个类,如果加载过就不必再次加载了。
2、避免歹意批改外围类库,比方咱们本人写一个 String 类,咱们去运行这个类,零碎会在疏导类加载器中加载 Java 的外围类库中的 String 类,而不会加载咱们自定义个的 String 类,这就避免咱们擅自篡改外围类库。
七、JVM 堆内存模型
在 JVM 中咱们的堆内存模型大略为图所示,咱们新生成的对象会放到 eden 区,当咱们 eden 区域放满了,咱们会进行一个轻 GC,会把 eden 区域和 S0 区域的存活对象放到 S1 区域,而后将 eden 区域和 S0 区域清空,而后新生成的对象接着放到 eden 区域,当 eden 区域再次满了,会将 eden 区域和 S1 区域的存活对象放到 S0 区域,而后将 eden 区域和 S1 区域清空,如此周而复始,没进行一次循环会将没有被革除的对象年龄 +1,如果存活的对象年龄达到 15(这个数值能够调整),就会将此对象放到老年代,或者是 survivor 区域满了也会将一些对象放到老年代,如果当老年代满了就会进行一次重 GC。
八、JVM 调优
所谓 JVM 调优次要就是缩小堆内存中重 GC 的次数,这样咱们就依据对象什么状况会进入老年代来进行剖析,首先是存活 15 代(这个数值能够调整),这种状况个别就是一些常量等,这些个别不会有再大的优化空间,或者一些代码的问题造成循环调用,这种状况通过批改调整代码即可。其次是当 survivor 区域满了的话就会向老年代放入一些对象,这个时候咱们就须要依据业务状况来调整堆内存的大小以及年老代和老年代的比例,个别咱们例如一些电商零碎,一些订单对象进入内存中就是朝生夕是,订单从创立到长久化数据库中就不在内存中应用了,所以一些失常的业务产生的对象会在轻 GC 的时候被发出,然而有一些大对象可能放不下年老代中所以就会放入老年代,这种状况咱们就要调大堆内存中年老代的大小,使得一些大对象能够放入到年老代中,并随着轻 GC 被垃圾收集器发出。