Java虚拟机原理

Java虚拟机

在这里插入图片描述
内存模型,类加载机制,GC是重点.性能调优部分更偏向应用,重点突出实践能力.编译器优化和执行模式部分偏向于理论基础,需了解 内存模型各部分作用,保存哪些数据.类加载双亲委派加载机制,常用加载器分别加载哪种类型的类.GC分代回收的思想和依据以及不同垃圾回收算法的回收思路和适合场景.性能调优常有JVM优化参数作用,参数调优的依据,常用的JVM分析工具能分析哪些问题以及使用方法.执行模式解释/编译/混合模式的优缺点,Java7提供的分层编译技术,JIT即时编译技术,OSR栈上替换,C1/C2编译器针对的场景,C2针对的是server模式,优化更激进.新技术方面Java10的graal编译器编译器优化javac的编译过程,ast抽象语法树,编译器优化和运行器优化.

JVM的内存区域划分

JVM知识点梳理

JVM内存分配与回收

JVM内存管理机制

Java虚拟机学习 - 垃圾收集器

类加载器详解

详解java类的生命周期

谈谈我对面向对象以及类与对象的理解
Java 如何有效地避免OOM:善于利用软引用和弱引用

1、JVM内存模型

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

1.1 程序计数器

保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器,当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免发生线程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令。如果执行的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。

1.2 虚拟机栈

在这里插入图片描述
在这里插入图片描述

线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一个栈桢(Stack Frame)来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则抛出stackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemoryError。 使用jclasslib工具可以查看class类文件的结构。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

1.3 本地方法栈

与栈类似,也是用来保存执行方法的信息.执行Java方法是使用栈,执行Native方法时使用本地方法栈.与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法(C语言)。由于规范对这块没有强制要求,不同虚拟机实现方法不同与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

1.4 堆

JVM内存管理最大的一块,对被线程共享,目的是存放对象的实例和数组,几乎所有的对象实例都会放在这里,根据对象的存活周期不同,JVM把对象进行分代管理,由垃圾回收器进行垃圾的回收管理。若堆的空间不够实例分配,则OutOfMemoryError。

在这里插入图片描述

1.5 方法区

又称非堆区,它是线程共享的,用于存放被虚拟机加载的类的元数据信息,如常量、静态变量和即时编译器编译后的代码。1.7的永久代和1.8的元空间都是方法区的一种实现。运行时常量池存放编译生成的各种常量。(如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收)

1.7 JMM模型

在这里插入图片描述

JMM是定义程序中变量的访问规则,线程对于变量的操作只能在自己的工作内存中进行,而不能直接对主内存操作.由于指令重排序,读写的顺序会被打乱,因此JMM需要提供原子性,可见性,有序性保证.
在这里插入图片描述

2、执行模式

2.1 HotSpot编译器

HotSpot内置了两个即时编译器Client Complier(C1)和Server Complier(C2),其编译过程不同。对频繁执行的代码编译为机器码,对不频繁执行的代码继续使用解释方式,可通过CompileThreshold、OnStackReplacePercentage两个计数器进行配置

C1编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序。
方法内联:方法较短时,将被调用方法的指令直接植入当前方法
去虚拟化:如发现类中的方法只提供一个实现类,则对调用方进行内联
冗余削除:根据运行时状况对代码进行折叠或削除

C2 编译器是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序。通过运行时信息,如分支判断(优先执行频率高的分支)和逃逸分析(变量是否被外部读取)等进行优化
标量替换:未用到对象的全部变量时,用标量替换聚合量,避免创建对象,节省内存,优化执行
栈上分配:对象未逃逸时,直接在栈上创建对象,优化执行
同步削除:对象未逃逸时,C2直接去掉同步

在java7以前需要根据程序特性选择不同编译器。java7引入了分层编译,可以通过参数 -client或-server强制指定虚拟机的编译模式,分层编译将jvm虚拟机状态分为5层。

第 0 层:程序解释执行,默认开启性能监控功能(profiling),如果不开启,可触发第2层编译。
第 1 层:将字节码编译为本地代码,进行简单可靠的优化,不开启 profiling的C1编译。
第 2 层:开启 profiling,仅执行带方法调用次数和循环回边执行次数 profiling的 C1 编译。
第 3 层:执行所有带 profiling的 C1 编译。
第 4 层:执行C2编译。

通常情况下C2编译效率高于C1,C1编译中由于使用Profiling其效率为第1层>第2层>第3层。在java8对分层编译做了进一步优化,默认开启分层编译,如果只想使用C1编译可以在开启分层编译的同时设置参数-XX:TieredStopAtLevel=1。如果想只使用C2编译可以通过-XX:-TieredCompilation关闭分层编译。C1、C2不满足优化条件时,进行逆优化回到解释执行模式。反射执行

  1. 由于权限校验、所有方法扫描及Method对象的复制,getMethod()方法比较消耗性能,应该缓存返回的Method对象;
  2. Method.invoke()的性能瓶颈:参数的数组包装、方法可见性检查、参数的类型检查。可通过JDK7的MethodHandle提高性能;

2.2 即时编译

即时编译依赖于编译器执行,与解释器执行流程不同,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为热点代码,为了提高热点代码的执行效率,在运行时,即时编译器会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后保存到内存中。

jvm根据方法调用计数器(Invocation Counter)以及回边计数器(Back Edge Counter)来触发即时编译。触发条件为方法计数器和回边计数器之和超过 -XX:CompileThreshold 指定的阈值时,则会触发即时编译。在分层编译中会通过profiling统计这两项指标。

方法调用计数器:用于统计方法被调用的次数,通过-XX: CompileThreshold 指定阈值,默认阈值在 C1编译器中为1500 次,在 C2 编译中中为10000 次,启用分层编译后,-XX: CompileThreshold 指定的阈值将失效。此时将会根据当前待编译的方法数以及编译线程数动态计算。

回边计数器:用于统计一个方法中循环尾到循环头的次数,通过-XX: OnStackReplacePercentage指定阈值,默认阈值在C1编译器中为为 13995次,C2编译器中为为 10700次。启用分层编译后,-XX: OnStackReplacePercentage 指定的阈值将失效。通用会根据当前待编译的方法数以及编译线程数动态计算。

2.3 OSR编译

OSR(On-Stack-Replacement)编译指在程序执行过程中,动态地替换掉 Java 方法栈桢,从而使得程序能够在非方法入口处进行解释执行和编译后的代码之间的切换。OSR是一种以循环为单位的即时编译通过回边计数器触发。其阈值为-XX:CompileThreshold的倍数,默认阈值在C1编译器中为13500,C2编译器中为10700。

2.4 四种执行模式

JVM目前支持四种执行模式:解释模式,编译模式,混合模式,AOT(Ahead-of-Time Compilation)

2…41 解释执行

JVM启动时,指定-Xint参数,就是告诉JVM只进行解释执行,不对代码进行编译。这种模式抛弃了JIT可能带来的性能优势。毕竟解释器是逐条读入,逐条解释执行的。

  1. 栈顶缓存:将操作数栈顶中值直接缓存在寄存器上,计算后放回操作数栈
  2. 部分栈帧共享:调用方法时,后一方法可将前一方法的操作数栈作为当前方法的局部变量,节省数据拷贝消耗
2.4.2 编译执行

JVM启动时,指定-Xcomp参数,就是告诉JVM关闭解释器,使用编译模式(或者叫最大优化级别),不进行解释执行。这种模式并不表示执行效率最高,它会导致JVM启动变得非常慢,同时有些JIT编译器的优化操作(如分支预测)并不能进行有效的优化。

2.4.3混合模式

混合模式,就是解释和编译混合的一种模式,新版本的JDK(例如JDK8)默认采用的是混合模式(JVM参数为-Xmixed)。

server模式的JVM,会进行上万次调用以收集足够的信息进行高效的编译,

client模式这个限制是1500次。Hotspot JVM内置了两个不同的JIT编译器,

C1对应client模式,适用于对于启动速度敏感的应用(如java桌面应用);

C2对应server模式,它的优化是为长时间运行的服务器端应用设计的。

2.4.4 AOT(Ahead-of-Time Compilation)

AOT就是将javac编译器编译后的字节码直接编译成机器代码,避免了JIT预热等各方面的开销。Oracle JDK 9就引入了实验性的AOT特性,并增加了新的jaotc工具。

3 生命周期

在这里插入图片描述
在这里插入图片描述

3.1 编译机制

在这里插入图片描述

  • 分析和输入到符号表:

    词法分析:将代码转化为token序列
    
    语法分析:由token序列生成抽象语法树
    
    输入到符号表:将类中出现的符号输入到类的符号表
    
  • 注解处理:

    处理用户自定义注解,之后继续第一步
    
  • 根据符号表进行语义分析并生成class文件,并进行相关优化

    虚拟机数据类型、字节码文件格式、虚拟机指令集
    

在这里插入图片描述

3.2执行机制

JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。 由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。 类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了双亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明:
Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。

3.2.1加载Loading:

通过一个类的全限定名来获取一个二进制字节流、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

在这里插入图片描述
通过类的完全限定名,查找此类字节码文件,利用字节码文件创建Class对象.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D4VKUVir-1680097718190)(http://note.youdao.com/yws/res/162/06BA868BC76E4C3382B1FA8877454ED9)]
双亲委派模式,即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器.父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载.

优点:

  1. 避免类的重复加载
  2. 避免Java的核心API被篡改
3.2.2验证Verification:

确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的自身安全。校验二进制字节码格式是否符合Java Class File Format规范,确保Class文件符合当前虚拟机的要求,不会危害到虚拟机自身安全.

3.2.3准备Preparation:

正式为类变量分配内存并设置类变量初始值。为类的静态属性分配内存和默认值,并加载引用的类或接口,不包含final修饰的静态变量,因为final变量在编译时分配.

3.2.4解析Resolution:

虚拟机将常量池内的符号引用替换为直接引用的过程。将运行时常量池中的符号引用替换为直接引用(静态绑定),直接引用为直接指向目标的指针或者相对偏移量等.

3.2.5初始化Initialization:

类加载过程的最后一步,到了这个阶段才真正开始执行类中定义的Java程序代码。主要完成静态块执行以及静态变量的赋值.先初始化父类,再初始化当前类.只有对类主动使用时才会初始化.触发条件包括,创建类的实例时,访问类的静态方法或静态变量的时候,使用Class.forName反射类的时候,或者某个子类初始化的时候.
类的初始化时机:

  1. 创建类的实例
  2. 初始化某个类的子类(满足主动调用,即访问子类中的静态变量、方法)
  3. 反射(Class.forName()会触发,ClassLoader.loadClass()及X.class不会触发)
  4. 访问类或接口的静态变量(static final常量除外,static final变量可以)
  5. 调用类的静态方法
  6. java虚拟机启动时被标明为启动类的类

初始化顺序:
父类静态成员、静态代码块—>子类静态成员、静态代码块—>父类和子类实例成员内存分配—>父类实例成员、代码块—>父类构造函数—>子类实例成员、代码块—>子类构造函数

3.2.6使用Using:

根据你写的程序代码定义的行为执行。

3.2.7卸载Unloading:

GC负责卸载,这部分一般不用讨论。
Java自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类才可以被卸.

4、对象的创建使用过程

Java是一门面向对象的编程语言,Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象通常(例外:克隆、反序列化)仅仅是一个new关键字而已,而在虚拟机中,对象(本文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样一个过程呢?
  虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过的。如果没有,那必须先执行相应的类加载过程。
  在类加载查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务具体便等同于一块确定大小的内存从Java堆中划分出来,怎么划呢?假设Java堆中内存是绝对规整的,所有用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer)。如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,就通常采用空闲列表。
  除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存。解决这个问题有两个方案,一种是对分配内存空间的动作进行同步——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,(TLAB ,Thread Local Allocation Buffer),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完,分配新的TLAB时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB的话,这一个工作也可以提前至TLAB分配时进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  在上面工作都完成之后,在虚拟机的视角来看,一个新的对象已经产生了。但是在Java程序的视角看来,对象创建才刚刚开始——方法还没有执行,所有的字段都为零呢。所以一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

4.1对象结构

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。

表1 HotSpot虚拟机对象头Mark Word
在这里插入图片描述
  对象头的另外一部分是类型指针,即是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身,这点我们在下一节讨论。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。
  接下来实例数据部分是对象真正存储的有效信息,也既是我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的都需要记录袭来。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那子类之中较窄的变量也可能会插入到父类变量的空隙之中。

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。对象头部分正好似8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
在这里插入图片描述

4.2对象的访问定位

建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范里面只规定了是一个指向对象的引用,并没有定义这个引用应该通过什么种方式去定位、访问到堆中的对象的具体位置,对象访问方式也是取决于虚拟机实现而定的。主流的访问方式有使用句柄和直接指针两种。

如果使用句柄访问的话,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体各自的地址信息。如图1所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MnVkSWqg-1680097718199)(http://note.youdao.com/yws/res/177/AA4CB773AED54CE1BB8104206C8A0C32)]
图1 通过句柄访问对象

如果使用直接指针访问的话,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如图2所示。
在这里插入图片描述

图2 通过直接指针访问对象

这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问的在Java中非常频繁,因此这类开销积小成多也是一项非常可观的执行成本。从上一部分讲解的对象内存布局可以看出,就虚拟机HotSpot而言,它是使用第二种方式进行对象访问,但在整个软件开发的范围来看,各种语言、框架中使用句柄来访问的情况也十分常见。
在这里插入图片描述
对象头的MarkWord用于存储对象的各种标记信息,实现锁、 哈希算法、垃圾回收等。
后续为指向类方法区的引用及数组长度(若为数组)。

4.2.对象分配方式

a. 堆上分配:指针碰撞、间隙列表

b. 栈上分配:基于逃逸分析

c. 堆外分配:Unsafe.allocateMemory()、DirectByteBuffer、ByteBuffer.allocateDicrect()或MappedByteBuffer

d. TLAB分配:Thread Local Allocation Buffer,多线程环境中JVM在Eden区分配一块内存为TLAB,在TLAB创建对象时不需要加锁,所以JVM首先在TLAB上创建对象,不够则在堆上创建。可通过-XX:TLABWasteTargetPercent设置TLAB和Eden的比例,可通过-XX:+PrintTLAB查看TLAB的使用情况。

对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。

动态判断对象的年龄:如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
空间分配担保:每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果大于老年区的剩余值大小则进行一次Full GC,如果小于则检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
年轻代->标记-复制
老年代->标记-清除

5.垃圾回收

5.1 垃圾回收器

垃圾回收器的类型:

  • 串行垃圾回收器(Serial Garbage Collector)
  • 并行垃圾回收器(Parallel Garbage Collector)
  • 并发标记扫描垃圾回收器(CMS Garbage Collector)
  • G1垃圾回收器(G1 Garbage Collector)

在这里插入图片描述
Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。1.9后默认的垃圾回收算法,特点保持高回收率的同时减少停顿.采用每次只清理一部分,而不是清理全部的增量式清理,以保证停顿时间不会过长其取消了年轻代与老年代的物理划分,但仍属于分代收集器,算法将堆分为若干个逻辑区域(region),一部分用作年轻代,一部分用作老年代,还有用来存储巨型对象的分区.同CMS相同,会遍历所有对象,标记引用情况,清除对象后会对区域进行复制移动,以整合碎片空间.
年轻代回收: 并行复制采用复制算法,并行收集,会StopTheWorld.
老年代回收: 会对年轻代一并回收初始标记完成堆root对象的标记,会topTheWorld. 并发标记:GC线程和应用线程并发执行. 最终标记完成三色标记周期,会topTheWorld. 复制/清除会优先对可回收空间加大的区域进行回收。

**ZGC (Z Garbage Collector)**是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器。它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器。在 JDK 11 新加入,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms。
前面提供的高效垃圾回收算法,针对大堆内存设计,可以处理TB级别的堆,可以做到10ms以下的回收停顿时间.
在这里插入图片描述
roots标记:标记root对象,会StopTheWorld.
并发标记:利用读屏障与应用线程一起运行标记,可能会发生StopTheWorld. 清除会清理标记为不可用的对象.
roots重定位:是对存活的对象进行移动,以腾出大块内存空间,减少碎片产生.重定位最开始会StopTheWorld,却决于重定位集与对象总活动集的比例. 并发重定位与并发标记类似
优点:低停顿,高吞吐量, ZGC 收集过程中额外耗费的内存小。
缺点:浮动垃圾目前使用的非常少,真正普及还是需要写时间的。
在这里插入图片描述年轻代->标记-复制 老年代->标记-清除
新生代收集器:Serial、 ParNew 、 Parallel Scavenge
老年代收集器: CMS 、Serial Old、Parallel Old
整堆收集器: G1 , ZGC (因为不涉年代不在图中)。

5.2 如何选择垃圾收集器?

  1. 如果你的堆大小不是很大(比如 100MB ),选择串行收集器一般是效率最高的。
    参数: -XX:+UseSerialGC 。
  2. 如果你的应用运行在单核的机器上,或者你的虚拟机核数只有单核,选择串行收集器依然是合适的,这时候启用一些并行收集器没有任何收益。
    参数: -XX:+UseSerialGC 。
  3. 如果你的应用是“吞吐量”优先的,并且对较长时间的停顿没有什么特别的要求。选择并行收集器是比较好的。
    参数: -XX:+UseParallelGC 。
  4. 如果你的应用对响应时间要求较高,想要较少的停顿。甚至 1 秒的停顿都会引起大量的请求失败,那么选择 G1 、 ZGC 、 CMS 都是合理的。虽然这些收集器的 GC 停顿通常都比较短,但它需要一些额外的资源去处理这些工作,通常吞吐量会低一些。
    参数:
    -XX:+UseConcMarkSweepGC 、
    -XX:+UseG1GC 、
    -XX:+UseZGC 等。
    从上面这些出发点来看,我们平常的 Web 服务器,都是对响应性要求非常高的。选择性其实就集中在 CMS 、 G1 、 ZGC 上。而对于某些定时任务,使用并行收集器,是一个比较好的选择

5.3 垃圾回收算法

分代收集算法,增量收集算法,分区算法

5.1.1 引用计数器:

为每个对象分配一个引用计数器,当计数器为0时回收对象,缺点:循环引用问题

5.1.2 复制

从根集合扫描存活对象,复制到一块全新内存空间,缺点:需要2倍内存空间,存活对象较多时开销较大
在这里插入图片描述

5.1.3 标记-清除:

从根集合扫描并标记存活对象,扫描完成后清除未标记对象,缺点:存活对象较少时内存碎片较多
在这里插入图片描述

5.1.3 标记-清除-压缩

从根集合扫描并标记存活对象,扫描完成后将存活对象移动并对齐
在这里插入图片描述

5.3 回收处理流程

GC何时开始:

所有的回收器类型都是基于分代技术来实现的,那就必须要清楚对象按其生命周期是如何划分的。

  • 年轻代:划分为三个区域:原始区(Eden)和两个小的存活区(Survivor),两个存活区按功能分为From和To。绝大多数的对象都在原始区分配,超过一个垃圾回收操作仍然存活的对象放到存活区。垃圾回收绝大部分发生在年轻代。
  • 年老代:存储年轻代中经过多个回收周期仍然存活的对象,对于一些大的内存分配,也可能直接分配到永久代。
  • 持久代:存储类、方法以及它们的描述信息,这里基本不产生垃圾回收。

有了以上这些铺垫之后开始回答GC何时开始:

Eden内存满了之后,开始Minor GC(从年轻代空间回收内存被称为 Minor GC);升到老年代的对象所需空间大于老年代剩余空间时开始Full GC(但也可能小于剩余空间时,被HandlePromotionFailure参数强制Full GC)

对什么东西操作,即垃圾回收的对象是什么:

从root开始搜索没有可达对象,而且经过第一次标记、清理后,仍然没有复活的对象。

做了什么东西:

主要做了清理对象,整理内存的工作。

除直接调用System.gc外,触发Full GC执行的情况有如下四种。

  1. 旧生代空间不足 旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起
    的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
  2. Permanet Generation空间满 PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息: java.lang.OutOfMemoryError: PermGen space 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
  3. CMS GC时出现promotion failed和concurrent mode failure 对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。 promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。 应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
  4. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间 这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行MinorGC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。 例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。 当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。 除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- javaDsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+DisableExplicitGC来禁止RMI调用System.gc。

判断对象是否存活一般有两种方式
引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。

6 性能调优

6.1 配置

-Xmx:设置最大堆内存,即新生代、老年代之和的最大值,该参数设置过小会触发OOM

-Xms:设置最小堆内存,即JVM启动时的初始堆大小,一般设置和-Xmx相同,避免垃圾回收后JVM内存重分配

-XX:NewSize:设置新生代的初始值

-XX:MaxNewSize:设置新生代最大值

-Xmn:等同于设置相同的-XX:NewSize和-XX:MaxNewSize,该参数设置过小会频繁GC

-XX:PermSize:设置持久代初始值

-XX:MaxPermSize:设置持久代最大值

-Xss:设置线程栈大小

-XX:NewRatio:设置老年代与新生代的比例

-XX:SurvivorRatio:设置Eden区与的比S区例

-XX:MaxTenuringThreshold:设置垃圾回收最大年龄,即新生代中的对象经过多少次复制进入老年代

-XX:PretenureSizeThreshold:设置大于指定大小的较大对象直接进行老年代

-XX:TargetSurvivorRatio:设置S区的可使用率,当S区的空间使用率达到这个数值,会将对象送入老年代

-XX:MinHeapFreeRatio:设置堆空间的最小空闲比例,当堆空间的空闲内存小于这个数值时,JVM便会扩展堆空间

-XX:MaxHeapFreeRatio:设置堆空间的最大空闲比例,当堆空间的空闲内存大于这个数值时,JVM便会压缩堆空间

在这里插入图片描述

在这里插入图片描述

新生代串行GC:使用复制算法,单线程STW

新生代并行回收GC:使用复制算法,多线程STW,吞吐量优先:自动调整新生代Eden、S0、S1大小

新生代并行GC:使用复制算法,多线程STW,新生代串行GC的多线程版本

老年代串行GC:使用标记压缩算法,单线程STW

老年代并行回收GC:使用标记压缩算法,多线程STW ,压缩方式比较特别,内存按线程数划分成不同区域,压缩时根据区域存活对象比例决定是否整块压缩

老年代并发GC:使用标记清除算法。问题:① 占用更多CPU;② 浮动垃圾;③ 内存碎片:支持Full GC后的碎片整理清除,多线程不STW,但是碎片整理是STW;
在这里插入图片描述

说明:

① -XX:+UserSerialGC为client默认方式,-XX:+UseParallelOldGC为server默认方式;

② 分存分配方式:指针碰撞(bump-the-pointer)、空闲列表(free list)、TLAB;

③ 根集合扫描加速:Card Table、Mod Union Table、Remembered Set;

④ 新生代并行回收GC没有对Mod Union Table进行处理,因此不能和老年代并发GC一起工作;

⑤ 使用 -XX:+HeapDumpOnOutOfMemoryError开启堆Dump;

优化方案:

① 给新生代分配较大空间,因为Full GC比Mirror GC成本高;

② 新生代进入老年代的年龄设置较大值,原因同上;

③ 设置大对象直接进入老年代,因为新生代使用复制算法,并且占用两倍空间,大对象成本高;

④ 最大和最小堆大小设置成一样,避免堆的调整;

⑤ 吞吐量优先模式:并行回收GC;

⑥ 响应时间优先模式:并发GC;
在这里插入图片描述

「堆栈内存相关」
-Xms 设置初始堆的大小
-Xmx 设置最大堆的大小
-Xmn 设置年轻代大小,相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值
-Xss 每个线程的堆栈大小
-XX:NewSize 设置年轻代大小(for 1.3/1.4)
-XX:MaxNewSize 年轻代最大值(for 1.3/1.4)
-XX:NewRatio 年轻代与年老代的比值(除去持久代)
-XX:SurvivorRatio Eden区与Survivor区的的比值
-XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。
-XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到
老年代「垃圾收集器相关」
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。
-XX:ParallelGCThreads=20:配置并行收集器的线程数
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。
-XX:CMSFullGCsBeforeCompaction=5 由于并发收集器不对内存空间进行压缩、整理,
所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行5次GC以后对内
存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是
可以消除碎片
「辅助信息相关」
-XX:+PrintGCDetails 打印GC详细信息
-XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,
排查问题用
-XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题.
-XX:+PrintTLAB 查看TLAB空间的使用情况

6.2 性能调优命令

Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfojps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jmap,JVM Memory Map命令用于生成heap dump文件
jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
jstack,用于生成java虚拟机当前时刻的线程快照。
jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

6.3 调优工具

常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(MemoryAnalyzer Tool)、GChisto。
jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控
jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
GChisto,一款专业分析gc日志的工具

7 编译器优化

7.1 公共子表达式的消除

7.2 指令重排

7.3 内联

7.4 逃逸分析技术

「对象一定分配在堆中吗?」 不一定的,JVM通过「逃逸分析」,那些逃不出方法的对象会在栈上分配。
「什么是逃逸分析?」
逃逸分析(Escape Analysis),是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。
逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。
「逃逸分析的好处」
栈上分配,可以降低垃圾收集器运行的频率。
同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并且GC频率也会减少

22、虚拟机为什么使用元空间替换了永久代?
「什么是元空间?什么是永久代?为什么用元空间代替永久代?」 我们先回顾一下「方法区」吧,看看虚拟机运行时数据内存图,如下:
在这里插入图片描述
方法区和堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
「什么是永久代?它和方法区有什么关系呢?」
如果在HotSpot虚拟机上开发、部署,很多程序员都把方法区称作永久代。可以说方法区是规范,永久代是Hotspot针对该规范进行的实现。在Java7及以前的版本,方法区都是永久代实现的。
「什么是元空间?它和方法区有什么关系呢?」
对于Java8,HotSpots取消了永久代,取而代之的是元空间(Metaspace)。换句话说,就是方法区还是在的,只是实现变了,从永久代变为元空间了。
「为什么使用元空间替换了永久代?」
永久代的方法区,和堆使用的物理内存是连续的。
在这里插入图片描述
「永久代」是通过以下这两个参数配置大小的~
-XX:PremSize:设置永久代的初始大小
-XX:MaxPermSize: 设置永久代的最大值,默认是64M
对于「永久代」,如果动态生成很多class的话,就很可能出现「java.lang.OutOfMemoryError:PermGen space错误」,因为永久代空间配置有限嘛。最典型的场景是,在web开发比较多jsp页面的时候。
JDK8之后,方法区存在于元空间(Metaspace)。物理内存不再与堆连续,而是直接存在于本地内存中,理论上机器「内存有多大,元空间就有多大」。

在这里插入图片描述
可以通过以下的参数来设置元空间的大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
「所以,为什么使用元空间替换永久代?」
表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制啦。

什么是Stop The World ? 什么是OopMap?什么是安全点?

进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW。
在HotSpot中,有个数据结构(映射表)称为「OopMap」。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过程中,也会在「特定的位置」生成 OopMap,记录下栈上和寄存器里哪些位置是引用。
这些特定的位置主要在:
1.循环的末尾(非 counted 循环)
2.方法临返回前 / 调用方法的call指令后
3.可能抛异常的位置这些位置就叫作「安全点(safepoint)。」 用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。

33、什么是 tomcat 类加载机制?
在 tomcat 中类的加载稍有不同,如下图:
在这里插入图片描述
当 tomcat启动时,会创建几种类加载器: Bootstrap 引导类加载器加载 JVM启动所需的类,以及标准扩展类(位于 jre/lib/ext 下) System 系统类加载器 加载 tomcat 启动的类,比如bootstrap.jar,通常在 catalina.bat 或者 catalina.sh 中指定。位于 CATALINA_HOME/bin 下。
在这里插入图片描述
Common 通用类加载器

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值