Java虚拟机知识总结

Java虚拟机

第一部分 JVM基础

第一章 Java内存区域与内存溢出异常

运行时数据区域

1.程序计数器(线程私有)

当前线程执行的字节码的行号指示器。

Java虚拟机的多线程通过线程轮流切换并分配处理器执行时间的方式来实现,一个处理器或者多核处理器的一个内核只会执行一条线程,每条线程需要一个独立的程序计数器

  1. 如果线程在执行一个普通的Java方法,计数器记录正在执行的虚拟机字节码指令的地址;
  2. 如果线程正在执行一个Native方法,计数器的值为空;
  3. 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2.Java虚拟机栈(线程私有)

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表,操作数栈,动态链接、方法出口等信息。每个方法的执行过程,就对应一个栈帧在虚拟机栈中的入栈和出栈。

1)局部变量表
  1. 基本数据类型(booleanbytecharshortintlongdouble
  2. 对象引用(reference类型)对象本身、对象起始地址的引用指针、指向一个对象的句柄、其他与此对象相关的位置
  3. returnAddress类型 指向一条字节码指令的地址
2)两种异常状态

StackOverflowError异常:线程请求的栈深度大于虚拟机所允许的深度

OutOfMemoryError异常:虚拟机栈可以动态扩展,如果扩展的时无法申请到足够的内存

3.本地方法栈

作用与Java虚拟机栈相似:Java虚拟机栈为Java方法服务,抛出异常同虚拟机栈

被动方法栈为Native方法服务

4.Java堆

被所有的线程共享的一块内存区域,算是虚拟机所管理的内存中最大的一块,带区域主要存储对象实例,是主要垃圾收集器管理的区域。

如果堆中没有内存完成实例分配,就会抛出OutOfMemoryError异常。

5.方法区

各个线程共享区域,用于存储已被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。

6.运行时常量池

运行时常量池方法区的一部分

  1. Class文件存放类的版本、字段、方法、接口等描述信息外,还有常量池,常量池存放编译器生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池存放。
  2. Java虚拟机对Class文件的每一部分的格式要求都有严格的规定。
  • 运行常量池Java虚拟机规范没有任何细节的要求。
  • 运行常量池中保存Class文件中描述的符号引用,翻译出来的直接引用也会存储在运行时常量池。
  • 运行时常量池具有动态性,不要求常量一定要在编译器产生,并非置入Class文件中常量池的内容才能进入方法区运行时常量区,运行期间也可以将常量放在运行常量池中
程序的执行方式有
  1. 静态编译执行:事前(编译时)编译,编译成机器码,直接由CPU执行

  2. 动态编译执行:运行时编译,JIT编译

    动态编译通常指运行时将所有代码都编译

    JIT编译将部分代码进行编译(热点代码)

  3. 动态解释执行:JVM有解析器,按照字节码指令逐行解析逐行执行,每次执行都需要解析。

JIT编译比解释器快,说的是执行编译后的代码比解释器解释执行要快,而不是编译这个动作比解释快。对于只执行一次的代码而言,解释执行可能比编译执行要快。

HotSpot虚拟机对象

对象创建

  1. 遇到new现在常量池定位这个类的符号引用,没有进行类加载过程。

  2. 对象分配内存

    1. 指针碰撞

      java堆中内存绝对规整,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点指示器。分配内存就是把指针向空闲部分挪动一段与对象大小相同的距离

    2. 空闲列表

      Java堆中内存不规整,虚拟机维护一个列表记录可以内存,分配之时从列表中查找足够大的内存即可。

    多线程下对象内存分配

    1. 对分配内存空间动作进行同步处理,采用CAS配上失败重方式保证更新操作的原子性。
    2. 把内存分配动作按照线程划分为不同的空间之上运行,每个线程预先分得一块内存,本地线程分配缓冲(TLAB).那个线程的TLAB用完,分配新的TLAB才进行同步锁定。
  3. 对象内存布局

    3个区域:对象头,实际数据,对齐填充

    hashcode,GC分代年龄,等等

    相同宽度的字段分配在一起,短的字段可以分配到之前字段的空缺位置,子类中的变量也可以分配在父类的字段部分

    确保对象起始地址为8字节的整数倍。

  4. 对象访问定位

    1.通过句柄方式访问,

    在Java堆中分出一块内存进行存储句柄池,这样的话,在栈中存储的是句柄的地址

    Java虚拟机知识总结_第1张图片

    优点:

    当对象移动的时候(垃圾回收的时候移动很普遍),这样值需要改变句柄中的指针,但是栈中的指针不需要变化,因为栈中存储的是句柄的地址

    缺点:

    需要进行二次定位,寻找两次指针,开销相对于更大一些

    2.直接指针访问方式

    Java栈直接与对象进行访问,在Java堆中对象帆布中必须考虑存储访问类型的数据的相关信息,因为没有了句柄了

    Java虚拟机知识总结_第2张图片

    优点:

    速度快,不需要和句柄一样指针定位的开销

第二章垃圾收集器与内存分配策略

哪些区域的内存是垃圾收集器关注的?

Java 内存中程序计数器,虚拟机栈和本地方法栈3个区域都是线程私有的,其中内存都会随着方法结束,线程结束自动回收。而堆内存和方法区的内存分配回收时垃圾收集器关注的部分。

对象死亡判断

1.引用计数算法

对象添加一个计数器,每一次引用它就在计数器加一,每次引用失效,就在计数器减一。计数器为0的对象不能被使用,是垃圾回收的对象。

缺点:很难解决对象之间相互引用问题

public class Test{
    public Object instance = null;
    public static void testGC(){
        Test a = new Test();
        Test b = new Test();
        a.instance = b;
        b.instance = a;
        a = null;
        b = null;
        System.gc();
    }
}
2.可直达分析算法

通过一系列的GC Roots的对象作为起始点,从这些结点开始向下搜索,搜索走过的路径成为引用链,当一个对象到GC Roots没有任何引用链,也就是从GC Roots到这个对象不可达,证明对象不可用。不可用对象将是可回收对象。

主流程序语言都是使用可直达算法作为判定对象是否存活的。

Java语言中可作为GC Roots的对象包括
  1. 虚拟机栈中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象
引用

强引用

代码之中普遍存在类似 Object a = new Object();的引用,只要强引用还存在,垃圾收集器就不会回收被引用的对象

软引用

描述一些有用但是并非必须的对象,对于软引用对象,在系统发生内存溢出异常之前,把软引用对象列入回收范围之内进行第二次回收。如果第二次回收之后还是没有足够的内存才会抛出内存溢出的异常。

弱引用

描述非必要对象,强度比软引用弱,被弱引用引用的对象只能存活到下一次垃圾回收之前。当垃圾收集器工作时,无论内存是否足够,都会回收只被弱引用引用的对象。

虚引用

幽灵引用或者幻影引用,最弱的引用。该引用的存在不会影响垃圾回收,也无法通过虚引用来获取对象实例,唯一目的:能在垃圾回收时收到一个系统通知

jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,而直接内存是在堆内存之外(其实是内存映射文件,自行去理解虚拟内存空间的相关概念),所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作。

finalize()方法

如果对象在可直达分析后发现没有与GC Roots相连接的引用链,它会进行第一次标记,进行进一步的筛选。

进一步的筛选条件是对象是否有必要执行finalize()方法,对象没有覆盖该方法或者该方法已经被虚拟机调用过,这两种情况都是没有必要执行。

如果这个对象有必要执行finalize()方法,这个对象就会放置在一个F-Queue队列中,稍后虚拟机建立Finalizer线程去执行。但是不等待它结束。

finalize方法是对象逃脱死亡的最后机会,只要在finalize中重新与引用链上任何对象建立关联,这个对象就会免死亡。没有逃脱,该对象就被回收了。

注意:finalize方法在面临第二次回收的时候不会在执行。

垃圾收集算法

1.标记清除算法

标记所有需要回收的对象,标记完成后统一回收被标记对象。

不足:

  1. 效率问题,标记和清除过程的效率都不高
  2. 空间问题,标记清除后产生大量不连续内存碎片,空间碎片太多导致分配较大对象时找不到足够的连续空间而不得不提前出发再一次垃圾收集。
2.复制算法

将内存划分为大小相同的两块,每次只使用一块,一块用完,就将存活的对象复制在另一块,并把原来的块的内存清空,对整个半区进行垃圾回收。

缺点:将内存区域缩小为原来的一半。

优化不按照1:1的比例划分而是划分为Eden和两个Survivor空间。每次使用Eden和其中一个Survivor,将存活的对象复制到两一个Survivor清理之前用过的Survivor和Eden空间。默认Eden空间和Survivor比例是8:1,由于新生代垃圾回收有98%对象需要回收。当回收的对象大于10%的时候借助其他内存如老年代。

3.标记整理算法

复制算法在存活率较高的情况下,进行较多的复制操作,效率降低。

针对老年代对象存活率较高,提出标记整理算法。

标记整理算法的标记过程与标记清除相同,清除过程将对象向一端移动然后清除边界以外的内存。

4.分代收集算法

把内存分成新生代和老年代,根据各个年代的特点采用适当的收集算法

HotSpot算法实现

1.枚举根结点

由于GC Roots的结点主要在全局性引用与执行上下文(栈帧中的本地变量表)中,光方法区就有数百兆,逐个检查这里的引用会消耗大量时间。

可达性分析对于执行时间的敏感性还体现在GC停顿上。分析性工作必须在一个确保一致的快照中进行。不可以出现对象引用关系的改变。需要停止Java执行线程(Stop The World)

主流Java使用准确式GC,在执行系统停顿下来,不需要一个不漏的检查所有执行上下文和全局的引用位置,虚拟机有办法知道哪些地方存放对象引用。在HotSpot的实现中,用OopMap数据结构达到这个目的,在类加载完成的时候,HotSpot就把对象内什么偏移上什么类型的数据计算出来,在JIT编译的过程中,会在特定的位置记录栈和寄存器中哪些地方是引用。

2.安全点

可能导致引用关系变化或者说OopMap的内容变化的指令很多,如果为每条指令都生成对应的OopMap,会消耗大量的额外空间。

HotSpot没有为每条可能改变引用关系的指令生成OopMap,而是在特定的位置记录,这些位置称为安全点

到达安全点后,执行系统停顿开始GC,只有到达安全点之后才能停止。

在方法调用,循环跳转,异常跳转等功能指令下会产生Safepoint。

如何让所有的线程都跑到最近的安全点并停顿下来?

  1. 抢占式中断

    不需要线程主动配合,GC发生后所有线程中断,如果有的线程中断的地方不在安全点,就恢复线程让它执行到安全点。(几乎没有虚拟机使用抢占式中断)

  2. 主动式中断

    当GC需要中断线程时,不直接对线程操作,而是设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真,就自己中断线挂起,轮询标志的地方和安全点重合,再加上创建对象分配内存的地方。

3.安全区域

程序不执行,在下次sleep和blocked状态下,这些线程无法响应JVM中断将线程执行到安全点的地方挂起。

安全区域是指在一段代码片段中引用关系不会发生变化。在线程执行到安全区域,就不用管表示自己在安全区域状态的线程。在线程离开安全区域时,检查系统是否完成根结点枚举,完成后线程继续执行,否则必须等到收到可以安全离开安全区域的信号为止。

垃圾收集器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-prfvEa7X-1576995119051)(D:\面试\springbootimages\20160505170035450)]

两个收集器之间存在连线说明可以搭配使用,所在区域表示它属于新生代收集器还是老年代收集器。

(红色部分为新生代)

1.Serial收集器

单线程收集器,使用一个CPU一条收集线程完成垃圾收集工作,在垃圾收集时必须停掉其他的所有工作线程,直到收集结束。Stop The World

在垃圾收集器不断改进中,停顿时间不短缩小,但是还是没有办法消除。

Serial收集器是虚拟机Client模式下默认的新生代收集器,简单高效,在限定单CPU的情况下,没有线程交互开销。在用户桌面应用场景下,分配给虚拟机管理的内存不大,停顿时间完全在可控范围内。

2.ParNew收集器

Serial的多线程版本,运行在Server模式下虚拟机新生代首选的新生代收集器。

  • 并发:多个线程同时执行(但是在微观上是交替进行的)
  • 并行:多个线程同一时刻都在运行
3.Parallel Scavenge收集器

新生代收集器,使用复制算法,并行的多线程收集器。

  • 特别之处:

Parallel Scavenge收集器关注目的是达到一个可以控制的吞吐量。

​ 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

高吞吐量可以高效利用CPU时间,尽快完成程序的运算任务。

相比之下:

CMS等收集器关注尽可能地缩短垃圾收集用户线程停顿时间

停顿时间越短越适合需要与用户交互的程序,良好的相应速度提升用户体验。

  • Parallel Scavenge收集器两个参数用于精确控制吞吐量。

-XX:MaxGCPauseMillis控制最大的停顿时间

-XX:GCTimeRatio直接设置吞吐量大小

自适应调节策略

Parallel Scavenge收集器被称为吞吐量优先的收集器

-XX:UseAdaptiveSizePolicy参数是一个开关参数,该参数打开,不需要手动指定新生代的大小,Eden与Survivor区的比例,晋升老年代对象的大小等细节参数了,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间和最大的吞吐量。这种调节方式叫做GC自适应的调节策略。

自适应调节是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

4.Serial Old收集器

使用标记整理算法,单线程老年代收集器,Client模式下虚拟机使用。如果在Server模式下两个用途:

  1. 在JDK1.5之前的版本中与Parallel Scavenge收集器搭配使用
  2. 作为CMS收集器的后备预案,在并发收集中发生Concurrent Mode Failure时使用
5.Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年版本,多线程,标记整理算法。

由于Parallel Scavenge收集器在Parallel Old收集器出现之前,Parallel Scavenge收集器只能与Serial Old配合,Serial在服务器端应用性能拖累,Parallel Scavenge收集器的最大吞吐量不能获得最大化效果。

6.CMS收集器

以获取最短回收停顿时间为目标的收集器,目前大部分Java应用集中在互联网网站或者B/S系统的服务器端上,这类应用重视服务器响应速度,希望停顿时间最短,带来较好的用户体验。

标记清除算法

运行步骤:

  1. 初始标记

  2. 并发标记

  3. 重新标记

  4. 并发清除

    解释

  • 其中初始标记和重新标记需要Stop The World。
  • 初始标记标记一下GC Roots能直接关联到的对象,速度快
  • 并发标记阶段进行GC Roots Tracing 的过程
  • 重新标记为了修正并发标记期间因用户程序继续运行而导致的标记产生变化的那一部分对象的标记,这阶段的停顿时间大于初始标记停顿时间。
  • 并发标记和并发清除都可以和用户线程一起运行,从整体上可以看做CMS收集器的内存回收过程与用户线程一起并发执行。

缺点

  1. CPU资源敏感

  2. 无法处理浮动垃圾:在CMS进行并发垃圾清除阶段,用户线程还在执行过程中产生的垃圾没有标记,此次GC无法处理这个阶段产生的垃圾,这些垃圾叫做浮动垃圾,只能遗留至下一次GC过程进行垃圾回收。

    参数-XX:CMSInitiatingOccupancyFraction设置老年代使用空间达到多少时激活CMS收集器,一般设置为68%,设置阈值偏高会导致,Concurrent Mode Failure,虚拟机启用Serial Old收集器,提高停顿时间性能降低。

  3. 由于吃用标记清除算法导致产生大量的内存垃圾

7.G1收集器

内存分配策略

对象优先在Eden分配

大多数情况下,对象会在新生代Eden区中分配,当Eden没有足够的空间,虚拟机发起一次Minor GC。

​ Minor GC是新生代GC发生在新生代垃圾回收动作,新生代对象朝生夕死,所以Minor GC 频率高,速度快

​ Major GC/Full GC发生在老年代的垃圾收集动作,速度比Minor GC 慢十倍以上。

大对象直接进入老年代
长期存活的对象直接进入老年代

虚拟机给对象定义年龄计时器,没熬过一次Minor GC,年龄计时器就加1,年龄增长到一定程度就直接晋升老年代。

动态对象年龄判断

在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于等于代年龄的对象直接进入老年代。无须达到进入老年代的年龄。

空间分配担保

在发生Minor GC 之前虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,这个条件成立,Minor GC可以确保是安全的。如果不成立,虚拟机查看HandlePromotionFailure设置的值是否允许担保失败,如果允许,那么虚拟机继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于就尝试进行Minor GC,这次Minor GC 是有风险的。如果小于,或者HandlePromotionFailure设置不允许冒险,这时就要进行一次Full GC。

为何HotSpot虚拟机要实现两个不同的即时编译器?

第二部分 虚拟机执行子系统

类文件结构

class文件的头四个字节为魔数,确定这个文件是否为一个被虚拟机接收的class文件,魔数用做身份识别。

接下来的四个字节为class版本号:第五第六个字节为次版本号,第七第八字节为主版本号。高版本JDK可以兼容低版本,但是低版本不能运行高版本class文件。

紧接着为常量池入口,class文件资源仓库,最大数据项目之一,常量池数量不固定需要一个u2类型的数据代表常量池计数值。计数值从1开始。

​ 常量池两大类常量:字面量和符号引用

​ 符号引用包括:类和接口的全限定名、字段的名称和描述、方法的名称和描述

访问标志:public final super interface abstract synthetic annotation enum

类索引 父类索引 接口索引集合

字段表集合描述接口或者类中声明的变量

方法表集合

属性表集合

虚拟机类加载机制

类的生命周期:加载、连接(验证、准备、解析)、初始化、使用、卸载。

详细见java高并发详解

第三部分 程序编译代码优化

Javac编译器

HotSpot虚拟机使用c++语言实现,Javac编译器由Java语言编写。

编译的过程:

解析与填充符号表过程:词法分析和语法分析,填充符号表

插入式注解器的注解过程

语义分析字节码生成过程

HotSpot的即时编译器

HotSpot虚拟机是解释器和编译器并存架构,当程序需要迅速启动和执行使用解释器,程序运行后随之时间推移编译器之间发挥作用。

HotSpot虚拟机内部有两个即时编译器Client Compiler和Server Compiler,简化为c1、c2编译器。

主流的HotSpot虚拟机默认采用解释器和其中一个编译器配合使用的工作方式,程序使用哪种编译器主要取决于虚拟机的运行模式。HotSpot虚拟机会根据自身的版本于宿主机器的硬件性能自动选择运行的模式,用户也可通过使用-client或者-server参数强制指定虚拟机运行在哪个模块下。

什么是热点代码?
  • 被多次调用的方法
  • 被多次调用的循环体

这两种情况,编译器是以对象为编译对象。栈上替换,即方法栈帧还在栈上,方法就被替换。

方法计数器:统计方法调用的次数

回边计数器:统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为回边。

代码优化技术

语言无关的公共子表达式消除

一个表达式已经计算过,并且没有发生变化,那该表达式就是公共子表达式。对于这个表达式直接使用之前的计算结果代替该表达式即可。

如果这种优化仅限于程序基本块内就成为局部公共子表达式消除,如果优化范围覆盖多个基本块就称为全局公共子表达式消除。

语言相关的数组边界检查消除

把运行期检查提到编译期完成。

其他类似优化技术:自动装箱消除,安全点消除、消除反射等等。

方法内联

逃逸分析

逃逸分析的基本行为:分析对象的动态作用域,一个对象被定义后,可能被外部方法引用,例如通过调用参数传递到其他的方法就是方法逃逸。有可能被外部线程访问,例如赋值给类变量或者可以在其他线程中访问该实例对象,叫做线程逃逸。

别的方法或者线程无法通过任何渠道访问这个对象实例,也就是对象不会逃逸到方法或者线程之外。对这个变量进行优化:

  • 栈上分配

    Java堆中的对象对于各个线程都是共享和可见的,只要持有该对象的引用就可以访问该对象的数据。如果想要避免这个对象不会逃逸出方法之外,就将其在栈上分配内存,对象所占空间会随着方法调用返回出栈而销毁。一般应用中不会逃逸的局部对象所占的比例较大,如果栈上分配,垃圾回收系统的压力会减小。

  • 同步消除

    对这个变量实施同步措施就可以消除其逃逸到别的线程

  • 标量替换

    标量是指一个数据已经无法再分解成更小的数据来表示了,Java中的原始数据类型(int,long等数据类型,reference类型)都是不能进一步分解的。如果一个数据可以被分解称为聚合量。

    把一个Java对象拆散,根据程序访问情况将其使用到的成员变量恢复原始数据类型来访问叫做标量替换。

    如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散,程序真正执行的时候可能不创建这个对象改为直接创建它的若干个被这个方法使用的成员变量来替换。对象拆分后不仅可以在栈上分配和读写之外,还可以为后续优化创造条件。

Java编译器和C++编译器的对比

代表即时编译器与静态编译器的对比

  1. 即时编译器编译过程占用用户线程的运行时间,具有时间压力,如果编译的速度不能达到要求用户程序将会察觉到重大的延迟。严重受制于编译成本,而静态编译器不考虑编译成本。
  2. Java语言是动态类型的编译语言,虚拟机必须频繁的动态检查,对于程序没有明确的检查行为,尽管努力优化但是也会消耗运行时间
  3. Java中没有virtual关键字,但虚方法使用频率远远大于C++语言,所以运行时对方法接收者进行动态选择频率较高。
  4. Java是动态可扩展语言,运行时加载新的类可能改变程序类型的继承关系,使得全局优化难以进行,因为编译器无法看到程序全貌。许多的优化措施只能以激进优化的方式进行。
  5. Java对象大多分配在堆内存,很少分配在栈上。垃圾回收上,C++使用代码进行回收,Java中存在无用对象筛选,所以Java在垃圾回收上效率较C++低。

Java语言在性能上的劣势为了换取开发上的效率,比如动态安全,动态扩展,垃圾回收机制。

你可能感兴趣的:(秋招准备,Java虚拟机)