Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)

目录

1.类的加载以及加载过程

1.1类加载的过程 

 1.2类加载器的分类

1.3 启动类加载器BootstrapClassLoader

1.4  扩展类加载器(ExtensionClassLoader)

1.5应用程序类加载器(AppClassLoader)

1.6用户自定义类加载器

1.7双亲委派机制

 1.8 补充

2.运行时数据区和jvm的基本结构

2.1 jvm结构

2.2 HotSpotVm 的运行时数据区

2.3 jvm的程序计数器(pc)

2.4虚拟机的栈 

2.4.1 操作数栈

3.JVM的堆

3.1堆的内存细分

3.2 新生区老年区

3.3对象分配的过程

3.4堆的垃圾回收分类(GC)

3.5内存的分配策略

3.6 TLAB分配

 3.7 一些分配堆空间的设置参数

4.方法区

 4.1 概念

 4.2方法区的参数设置

4.3方法区(元空间)的内部结构

4.4运行时常量和常量池

 4.5方法区到元数据区(元空间)的演变

4.6方法区的垃圾回收

5.对象的实例化和执行引擎

5.1.对象的内存布局

 5.2对象的访问定位

5.3执行引擎的概念 

 5.4解释器和JIT编译器

 5.4.1解释器

5.4.2 JIT编译器

5.5热点代码及其探索方式。

6. 垃圾回收

6.1 垃圾回收概述

6.2java垃圾回收的机制

6.3 垃圾回收算法

6.3.1引用记数算法(垃圾的标记)

6.3.2 可达性分析算法(根搜索、追踪性)(垃圾标记)

6.4.对象的Finalization机制。

建议不主动调用finaliz()方法,应该交给垃圾回收机制调用

6.5 垃圾清除算法

6.5.1标记清除算法

6.5.2 复制算法

6.5.3 标记压缩(整理)算法

6.5.4对比

 6.6分代收集算法

 6.7 增量收集算法

6.8分区算法


简单介绍一下java程序的运行流程

  1. .java文件
  2. .class文件
  3. jvm识别.class

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第1张图片

下面就是jvm虚拟机 

正是有了jvm虚拟机才体现了java语言的跨平台性

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第2张图片

         所以说jvm的作用如下:

java虚拟机是二进制的字节码运行环境,可以运行.class文件。并且解释编译为对应平台的机器指令进行执行。

        特点:

  • 一次编译处处运行
  • 自动内存管理
  • 自动垃圾回收功能

目前市面上最常见运用最广泛的是HotSpotVm虚拟机

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第3张图片

 在虚拟机发展的历程中还有如下的JVM:sun ClassicVM、ExactVm、IBMJ9、AzulVM等等感兴趣的可以去搜索一下,我们主要根据HotSpotVm讲解JVM。

1.类的加载以及加载过程

        

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第4张图片

 通过javac的编译会生成.class

        我们的类加载器就会加载我们电脑磁盘上的class文件,加载到了JVM虚拟机中被称之为DNA原数据模板,存放在方法区中。

        就是这个class文件到达jvm成为原数据模板的过程之中需要通过类加载器实现(ClassLoader)。我们的类加载器相当于运输设备。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第5张图片

1.1类加载的过程 

对于加载的过程细分如下

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第6张图片

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第7张图片

我们重点解读一下链接 的过程

  • 验证

目的在于确保class文件的字节流中的信息符合虚拟机的要求,保证正确性不会危害jvm虚拟机的运行。

4种验证方式:文件格式验证,原数据验证,字节码原则,符号应用验证

  • 准备

为类的变量分配内存并且设置默认值

这里不包含final修饰的static,因为final在编译的时候就会分配内存

这里不会为实例变量分配初始化,类的变量会分配在方法区中,而实例化的变量会随着对象一起分配到java的堆中

  • 解析

将常量池中的符号引用转换为直接引用过程。

事实上,解析操作往往会伴随着jvm在执行完成初始化之后进行

解析的动作主要是针对类或者接口、字段、类方法、接口方法、方法类型等。

  • 初始化

初始化阶段就是执行类构造器方法的过程

此方法不需要被定义,是javac编译器字段收集类中的所有类变量的赋值动作和静态代码中的语句进行合并。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第8张图片

 1.2类加载器的分类

JVM虚拟机支持2中类加载器如下:

  • 引导类加载器(BootstrapClassLoader)
  • 自定义类加载器(User-definedClassloader)

自定义类加载 器一般指的是由ClassLoader派生出来的类加载器称之为自定义。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第9张图片

1.3 启动类加载器BootstrapClassLoader

        启动类的加载器Jvm虚拟机自带BootstrapClassLoader

这个类加载器是用c语言实现的,嵌套在jvm的内部

他用来加载java的核心类库。

并没有继承java.lang.classLoader没有父类加载器

出于安全考虑Bootstarap启动类加载器只会加载包名为java  javax sun等开头的类

1.4  扩展类加载器(ExtensionClassLoader)

java语言编写的加载器

派生于Classloader

父类是启动类加载器

从java.ext.dirs系统属性所指向的目录加载类库,或者从jdk的安装目录的jre/lib/ext子目录下加载类库。如果用户自己创建的jar放在此目录下,也会自动由扩展类加载器进行加载。

1.5应用程序类加载器(AppClassLoader)

由java语言编写

派生于ClassLoader

负责加载变量classpath或者系统属性java.class.path指定路径下的类库

该类加载器是陈故乡默认的类加载器,一般的java应用类都是又他完成加载的

1.6用户自定义类加载器

开发人员可以通过继承ClassLoader类的方式实现

在jdk1.2之前要求开发人员继承了ClassLoader的时候要重写loaderClass()方法,但是在jdk1.2之后不建议覆盖loaderClass()方法,而是讲加载的逻辑方法写在FindClass()方法之中。

1.7双亲委派机制

        java虚拟机对class文件采用按需加载的方式,也就是说当需要用到这个类的时候才会将他的class文件加载到内存生成class对象。而且在加载这个对象的class文件的时候,jvm采用的是双亲委派的模式,就是把请求交由父类处理。

工作原理:

  1. 如果一个类加载器收到请求,他并不会自己去加载,而是把这个请求委托给父类加载器去执行。
  2. 如果父类加载器还有更上一层的父类加载器继续委托,直到最顶层的启动类加载器
  3. 如果父类加载器可以完成加载任务,就返回成功,只有在父类加载器无法完成任务的时候子类加载器才会尝试自己去完成加载任务。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第10张图片

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第11张图片

 1.8 补充

 Jvm中判断2个class对象是否为同一个类存在以下条件

  • 类的完整类名一致,包名一致
  • 加载这个类的ClassLoader必须相同

jvm必须知道一个类型是由启动类加载器加载的还是有用户类型加载器加载的。

       1. 如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存着方法区。

        2.当解析一个类型得到另一个类型的引用的时候,jvm需要保证这两个类型的加载器是相同的。

java如何使用类

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第12张图片

2.运行时数据区和jvm的基本结构

2.1 jvm结构

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第13张图片

        内存是非常重要的系统资源,建立cpu和硬盘中间仓库桥梁。jvm内存布局规定了java在运行过程中的内存申请、分配、管理的策略。保证了jvm的高效稳定,不同的jvm对于内存的划分和管理机制不同这样我们主要也是HotSpotVm。

2.2 HotSpotVm 的运行时数据区

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第14张图片

java的虚拟机定义了若干中程序运行时会使用到的运行时数据区,其中有的一线会随着虚拟机的启动二创建,伴随着虚拟机的退出而销毁。另外一些则是与线程一一对应,这些与线程对应的数据区域会随着线程的生命周期而创建消失。

如下图:

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第15张图片

灰色表示为单独现场私有,红色表示多个线程的共享

每个线程:独立包括程序计数器,栈、本地栈。

线程空间共享:堆、堆外内存

  • jvm线程

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第16张图片

在hotspot jvm中,每个线程都与操作系统的本地线程直接映射,当java虚拟线程机准备好,此时本地操作系统同时创建一个线程。java虚拟机的线程结束,本地也回收线程。

2.3 jvm的程序计数器(pc)

        jvm中的程序计数器(pc)并非我们真正计算机cpu中的pc,jvm中的pc是对物理oc寄存器的一种抽象模拟。cpu只有把数据装载到寄存器才能运行。

        学过操作系统的都知道pc是用来存放下一条指令的地址,也就是将要执行的代码。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第17张图片

        在jvm中他是很小的一个空间,但是是速度最快的区域,jvm中没个线程拥有自己的pc,是线程私有的,生命周期与线程一致。 

        它是程序控制的指示器,分支,循环,跳转,异常处理,线程恢复的基础。

        字节码解释器工作是就是通过改变这个计数器的值来选取下一个指令

  •  好处:

cpu来回去换线程的情况下cpu知道从哪里开始哪里结束

jvm解释器通过pc的值了解下一条命令

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第18张图片

2.4虚拟机的栈 

        由于jvm的跨平台性,java的指令都是更具栈来设计的,因为不同的cpu框架不同,因此不能通过基于寄存器来设计

        优点:跨平台,指令集小,编译器容易实现

        缺点:性能下降,指令更多

  • 内存的栈和堆是不同的

        栈是运行时的单位,堆是存储的单位

  • java的栈是什么?

早期也叫做java栈,每个线程在创建的时候都会创建一个虚拟机的栈,其内部报错一个一个栈帧,对应一次java的方法调用。

生命周期和线程一致

主要管理程序运行,保存方法的局部变量,部分结果,参与方法的调用和返回。

  • 优点

速度快仅次于pc

操作简单----出栈/入栈

不存在垃圾回收的问题

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第19张图片

 栈可能出现的异常:栈满内存溢出,内存泄漏等等

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第20张图片对于栈内存不足我们可以在idea工具设置内存大小 修改-Xss 

 Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第21张图片

 但是我们不建议修改过大

  • 栈中存储什么呢

每个线程都要自己栈,栈中的数据都是根据栈帧(stackFrame)的格式存储

在线程上每个正在执行的方法都对应于一个栈帧

栈帧是内存的一颗内存区,是一个数据集,维系方法运行的数据信息

  • 运行的原理

jvm对栈的操作只有出入栈

在一条活动线上,一个时间点上,只会有一个活动的栈帧。只有当前正在执行的方法的栈帧在栈顶才是有效的,成为当前栈。

执行引擎运行的所以直接骂指令只针对当前栈帧进行操作

如果这个方法调用其他方法,对应的新的栈帧会被创建出来,这个新的栈帧位于栈顶。

不同线程的栈帧是不允许相互引用的

当前方法调用其他方法返回的时候,当前栈帧回传回此方法的结果给前一个栈帧,接着虚拟机丢弃当前栈帧,使得被调用的方法栈帧位于顶端(即是前一个栈回归顶部) 

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第22张图片

  •  局部变量

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第23张图片

 方法嵌套的次数由栈的大小决定,栈越大嵌套越多。对于一个方法而言参数和局部变量越多,栈帧就越大。

局部变量表中的变量旨在当前方法有效,方法调用后随着方法的栈帧销毁,局部变量表也随之销毁

  • slot的理解

​​​​Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第24张图片

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第25张图片

slot的重复利用

 

  • 变量的分类
按照数据类型分:① 基本数据类型  ② 引用数据类型
① 成员变量:在使用前,都经历过默认初始化赋值
类变量: linking的prepare阶段:给类变量默认赋值  ---> initial阶段:给类变量显式赋值即静态代码块赋值。(前面讲到类加载的过程)
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值。
② 局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过

补充:

在栈帧中,与性能调优最密切的部分就是签名提到的局部变量表。在方法执行是,虚拟机使用局部变量表完成方法的传递。

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表直接或者间接引用的对象都不是回收的对象。

2.4.1 操作数栈

每一个独立的栈帧除了上述提到的局部变量外,还有后进先出的操作数栈。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第26张图片

操作数栈主要用于保存计算机计算过程的中间值,同时作为计算过程变量的中间存储空间。

如果被调用的方法带有返回值的话,其返回值会被压入当期栈帧的操作数栈中,并且更新下一个pc执行的字节码命令。

  •  栈顶缓存

由于操作数是存在内存中的,因此频繁的执行内存读写是必然会影响执行的速度。为了解决这个问题,HotspotVm 提出了栈顶缓存技术。将栈顶元素全部缓存在无物理cpu的寄存器中,以此达到降低对内存读写的次数。提高执行效率。

  • 动态链接

每一个栈帧内部都包含了一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用就是为了支持当前方法的代码能够实现动态链接。

当一个方法调用另外的其他方法时,就是通过常量池中指向方法的符号引用表示,动态链接的作业就是为了将这些符号引用转换为调用方法的直接引用。

  • 方法调用

jvm中,将符号引用转换为调用方法的变得机制有关

静态链接:

动态链接:

 总结:简而言之在编译前由程序员显示指明的绑定方法称之为静态,根据运行时才知道的方法绑定称之为动态。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第27张图片Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第28张图片

  •  方法返回地址

方法结束有2种方式:

  1. 正常完成
  2. 出现异常,非正常退出

无论哪种方式退出,方法在退出后都返回该方法被调用的位置。方法正常退出的时候,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。

然后异常退出,返回的地址是要异常处理表来确定的,栈帧一般不报错异常处理表的信息。

 

  • 栈的执行流程

结合上文介绍栈的运行原理+方法的调用变量的调用,大概的流程如下

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第29张图片

3.JVM的堆

堆在jvm中的位置

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第30张图片

 概念:

  1. 一个jvm虚拟机实例只存在一个堆内存,堆是java内存管理的核心
  2. 堆在jvm启动的时候就创建,大小也确定了
  3. 堆位于物理内存上不连续的内存空间,但在逻辑上视为连续
  4. 所有的线程共享堆的数据,还可以划分线程私有的缓冲区
  5. 所有的对象实例、数组在运行时分配在堆上
  6. 在方法执行后,堆中的对象不会立马被移除,仅仅被垃圾收集的时候移除。
  7. 堆是 GC(垃圾回收)的重点区域

3.1堆的内存细分

在jdk7前堆内存的逻辑上分为:新生区、养老区、永久区。

在jdk8之后划分方式如下:

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第31张图片

  • 结构图 

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第32张图片

  •  堆空间的大小设置

因为在jvm启动的时候我们就为jvm开辟了内存空间我们可以通过设置jvm参数进行设置

-Xms 用于表示堆的起始内存,等价于 -XX:InitialHeapSize

-Xmx 用于表示堆区的最大内存,等价于 -XX:MaxHeapSize

/**
 * 1. 设置堆空间大小的参数
 * -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
 *      -X 是jvm的运行参数
 *      ms 是memory start
 * -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
 *
 * 2. 默认堆空间的大小
 *    初始内存大小:物理电脑内存大小 / 64
 *             最大内存大小:物理电脑内存大小 / 4
 * 3. 手动设置:-Xms600m -Xmx600m
 *     开发中建议将初始堆内存和最大的堆内存设置成相同的值。
 * 4. 查看设置的参数:方式一: jps   /  jstat -gc 进程id
 *                  方式二:-XX:+PrintGCDetails
 */

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第33张图片

一旦堆区的内存大小超过 -Xmx的上线的时候就会抛出 OutOfMemoryerror异常

我们通常将 Xms和Xmx设置为相同的内存大小,目的为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。

3.2 新生区老年区

存储在jvm中的java对象可以划分为2类:

  1. 生命周期短的瞬时对象,这类对象创建消失都很快。
  2. 生命周期长,某种情况下和jvm同步生存

        在这更进一步划分 年轻代(YoungGen)和老年代(OldGen),年轻代又可以划分为 Eden空间和S0空间和S1空间(from区或者to区)。见下2图

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第34张图片

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第35张图片

我们可以在jvm创建之前为我们分配新生区和老年区的空间比例;

  1. 默认-XX:NewRatio=2,表示新生区占1,老年区占2。
  2. 自定义设置-XX:NewRatio=4,表示新生区占1,老年区占4。

默认下在Hotspot中 Eden空间和另外2个S空间的缺省占比8:1:1

同样的开发人员也是可以进行设置的

-XX:SurvivorRatio=8

几乎所有的对象都是在Eden区被new出来的。

绝大多数java对象的销毁都在新生区。

我们也可以通过 -Xmm设置新生区的内存大小

3.3对象分配的过程

为新的对象分配内存,要考虑如何分配,在哪里分配等问题,还需要考虑分配完成Gc执行后的内存碎片问题。

  • 分配执行的步骤:
  1. new的对象存放在Eden(伊甸园区)
  2. 当Eden空间满了,程序需要创建对象,jvm的垃圾回收将对Edun(伊甸园)区进行垃圾回收(MinorGC),将伊甸园区中不再被其他对象引用的对象进行销毁。在加载新的对象放入Eden区。
  3. 然后将伊甸园区剩余的对象移动到S0区
  4. 如果再次触发GC(垃圾回收),S0的移动对象没有被清除,就会再次移动到s1区
  5. 如果再次触发GC后幸存对象会返回s0区(也就是说不会被清除的对象会在s0,s1来回移动)
  6. 每次的幸存对象在s0,s1区移动的时候会记录次数大于15次的幸存对象移动到到永久区(养老区)这个默认的对象次数可以设置 默认15次
    -XX:MaxTenuringThreshold=
  7. 在永久区的对象几乎不进行Gc但是内存满的时候回触发 MajorGC进行养老区清理8,
  8. 如果养老区Gc后还是无法保存对象报错OOM
  • 大致的流程图

 正常的对象分配流程如上述但是还是会有特殊情况入下图

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第36张图片

 在按照正常的分配流程下如果当前分配的空间不够的时候可以分配到 s区或者永久区,或者直接报错。

3.4堆的垃圾回收分类(GC)

根据回收的区域而言我们把GC分为3类,也分为部分收集和整堆收集

  1. MinorGC(YoungGC):新生代(区)垃圾回收
  2. MajorGC(OldGC):老年代(区)垃圾回收
  3. FullGC:收集整个java堆和方法区。
  • MinorGC(YoungGC)触发机制
  1. 当前新生区空间不足的时候触发  MinorGC,这里指的的是Eden空间满,S区满不会触发MinorGC,每次MinorGC会清理整个新生区Eden+S区。
  2. java对象大多数消灭的很快所以MinorGC执行的非常频繁,但是这种的回收速度也很快。
  3. MinorG会引起STW,暂停用户的线程,等待垃圾回收结束,用户线程才回复
  • MajorGC机制
  1. 老年区发送对象的移除的时候我们认为是,majorGC和FullGC发生了。
  2. 出现了majorGC,经常会伴随至少一次minorGC(非绝对的)。通常来说老年空间不足会先进行MinorGC再进行MajorGC
  3. majorGC执行的比MinorGC慢10倍以上,STW的时间更长
  • FullGC触发机制
  1. 调用system.gc()方法 执行FullGC
  2. 老年代空间不足
  3. 方法区空间不足
  4. MinorGC后进入老年区的平均大小大于老年区

为什么堆要进行分区工作(新老区)

对于java对象而言百分之八十的对象都是临时对象,分区的唯一的好处就是优化GC的性能,可以快速GC临时对象。对于长期有效的对象进行保存,逼不得已再进行清查。

3.5内存的分配策略

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第37张图片

 Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第38张图片

 这就是我们上面将的每次幸存的次数都会累加,达到我们规定的次数之后进行转移区域。

3.6 TLAB分配

为什么用tlab

  1. 堆区是线程共享的,任何线程都可以访问堆区数据。
  2. 对象的创建在jvm中非常频繁,因此在多线程高并发下从堆区分配内存空间是线程不安全的。
  3. 为避免多线程操作同一地址,需要加锁降低分配速度。

tlab的作用

  1. 在内存模型区域的角度 进一步细化Eden区(伊甸园),jvm为每个线程分配一个私有的缓存区。
  2. 多线程同时分配内存的时候,使用tlab可以避免一系列非线程安全问题,同时提升分配的吞吐量,我们称之为快速分配策略。
  3. 目前来说OpenJDK衍生出来的JVM都提供TLAB。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第39张图片

 tlab的过程

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第40张图片

 3.7 一些分配堆空间的设置参数

 * 测试堆空间常用的jvm参数:
 * -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
 * -XX:+PrintFlagsFinal  :查看所有的参数的最终值(可能会存在修改,不再是初始值)
 *      具体查看某个参数的指令: jps:查看当前运行中的进程
 *                             jinfo -flag SurvivorRatio 进程id
 *
 * -Xms:初始堆空间内存 (默认为物理内存的1/64)
 * -Xmx:最大堆空间内存(默认为物理内存的1/4)
 * -Xmn:设置新生代的大小。(初始值及最大值)
 * -XX:NewRatio:配置新生代与老年代在堆结构的占比
 * -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
 * -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
 * -XX:+PrintGCDetails:输出详细的GC处理日志
 * 打印gc简要信息:① -XX:+PrintGC   ② -verbose:gc
 * -XX:HandlePromotionFailure:是否设置空间分配担保

总结

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第41张图片

4.方法区

方法区也是jvm内存结构的一部分。

根据我们已经讲解的栈,堆来讲解和方法区的关系

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第42张图片

 Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第43张图片

 4.1 概念

在java虚拟机规范中明确的说明:尽管所有的方法区都属于堆的一部分,但是一些简单的实现可能不会再选择进行垃圾回收或者压缩。对于jvm而言方法区也叫非堆区。所以方法区看作是独立于堆的区域。

  1. 方法区(Method Area)与java堆一样是各个线程的共享区域。
  2. 方法区在jvm启动的时候就创建了,他的 实际内存空间和堆一样都是不连续的。
  3. 方法区的空间大小和堆一样,可以扩展、可以固定。
  4. 方法区的大小决定了jvm可以保存多少个类,系统的类太多了会导致方法区溢出。
  5. 关闭jvm会释放这个区域的内存。
  6. 在jdk7之前方法区称之为永久代,jdk8之后使用元空间代替永久代
  7. 元空间的本质和永久代类似,都是对jvm方法区的实现,区别在于元空间采用本地系统内存,而不是jvm的虚拟内存。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第44张图片

 4.2方法区的参数设置

jdk7之前:

  1. 通过 -XX:PermSize来设置永久代的起始空间大小 ,默认是20.75M
  2. 通过-XX:MaxPermSize来设置最大永久代空间大小,64位机器默认82M
  3. 如果jvm加载信息的时候内存大于上述的值会报错OurOfMemoryError:PermGen

jdk8之后:

  1. 元空间(元数据)区的起始大小: -XX:MetaspaceSize
  2. 最大空间:-XX:MaxMetaspaceSize
  3. 默认情况下windows下的起始值大小21m,最大值为-1(无限制)
  4. 和方法区不同的是如果不指定最大空间jvm可能会耗尽系统的内存。元数据去发送溢出也是会报错
  5. 对于系统而言jvm的方法区最大空间,就是系统的初始的高水位线。但触发了这个水位(内存上限)。FullGC就会触发并且回收卸载没有用的类。并且这个高水位线会被重置,但是新的高水位线的数值取决于GC释放了多少元空间,根据这个弹性的上调或者下降高水位线。
  6. 如果一开始 高水位线设置太低了,上述的过程执行多次。
  • 如何解决OOM的一些方案

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第45张图片

4.3方法区(元空间)的内部结构

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第46张图片

 方法区存储的内容主要如下:

它用于存储已被加载的类型信息,常量、静态变量、即时编译器编译后的低吗缓存。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第47张图片

  • 类型信息:

 对于每个加载的类、接口、枚举、注解JVM都需要存储他们的

完整有效名称、直接父类名的完整有效名称、类的修饰符,类的直接接口的有序列表。

  • 方法信息:

方法名称、返回类型、方法参数的数量和类型、方法的修饰符、异常表、方法的字节码信息

4.4运行时常量和常量池

结构图

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第48张图片

 在方法区内部包含 了运行时常量池

字节码文件中包含了常量池

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第49张图片

一个有效的字节码文件除了包含类的信息、字段、方法、接口描述、还包含意向信息就是常量池表,包含了各种字面量和对类型、域和方法的符号引用。 

为什么需要常量池

      一个java源文件的类、接口、编译后会产生一个字节码源文件。然而java的字节码需要数据支持。通常这种数据会很大以至于不能直接存到字节码中。换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用,在动态链接的时候会链接运行时常量池(类的加载介绍过)。

  •        总结

所以说常量池可以看做是一张表,虚拟机根据这张表找到要自信的类名、方法名、参数类型、字面量等类型。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第50张图片

 4.5方法区到元数据区(元空间)的演变

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第51张图片

  •  jdk8的结构

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第52张图片

  •  jdk7的结构Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第53张图片
  •  jdk6的结构

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第54张图片

  •  永久代被淘汰的原因

对于jdk8而言元空间的数据直接分配带系统的内存中,这样做在某些场景下都给他加载类过多也有足够的空间。不用像使用永久区一样对于庞大的业务来说需要我们对永久区进行调优。

  • 对于StringTable的调整

jdk7中将StringTable放到了堆空间中,因为永久代的回收效率很低,在FullGc的时候才会触发,,FuulGC只有在老年代和永久代不足才触发,并且速度慢。这样就到这StringTablle回收效率低下,在我们的开发中大量的字符串被创建,放到堆空间中回收及时不容易引起空间不足。

4.6方法区的垃圾回收

方法区的垃圾回收主要是2个部分:常量池中飞起的常量和不在使用的类型。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第55张图片

 方法区主要存放的2大类型常量:字面量和符号引用。

字面量类似于常量的概念,而符号引用则接近于编译原理的概念。

  • 三大常量:
  1. 类和接口的全限定名
  2. 字段的名称和描述符
  3. 方法的命名和描述符

Hotspot虚拟机对常量池的回收策略是比较明确的,只要在常量池中常量没有被任何地方引用就可以被回收。与堆回收对象类似。

  • 方法区判断回收的条件、

对于判断常量是否销毁相对简单,但是判断一个类型是否销毁的条件相对苛刻。必须满足3个条件

  1. 该类的所有实例都被回收了,也就是java堆不存在该类的任何派生子类
  2. 加载该类的加载器已经被回收,
  3. 该类的对用的java.lang.class对象没有在任何地方被引用。

只有满足上面的3个标准的无用类进行回收处理。初次之外这里的回收并不是绝对的,关于这样的回收还收到jvm的控制。

5.对象的实例化和执行引擎

创建对象的方式:

new、Class的newInstance()方法(反射)、反序列化、clone()....等等

创建对象的步骤:

  1. 判断对象是否加载,链接,初始化。
  2. 对象的内存分配
  3. 处理并发的安全问题
  4. 初始化分配到空间
  5. 执行init方法

5.1.对象的内存布局

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第56张图片

 5.2对象的访问定位

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第57张图片

 栈帧的对象引用

通过栈帧指向堆区在通过堆区指向方法区(元空间)

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第58张图片

5.3执行引擎的概念 

        执行引擎是java虚拟机核心的组成成分之一。

        jvm虚拟机就是相对我们物理机器(操作系统机器)的概念,他们的区别在于物理机的执行引擎直接建立在处理器、缓存、指令集和操作系统上,然而虚拟机的执行引擎是建立在软件自行实现的。因此jvm的执行引擎不受指令集的约束,可以执行一些不能够被硬件直接支持的指令格式。

  • 作用

将字节码指令解释/编译为对应平台的本地机制指令。

  • 原因

因为jvm的主要任务是负责庄重字节码到其内部结构、单字节码不能直接运行在操作系统之上。这个时候的字节码不等价本地机器的指令,他只能被jvm识别,这个时候就需要执行引擎翻译为我们本地机器认识的机器语言。

  • 执行引擎工作流程

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第59张图片

  • java代码编译执行的过程。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第60张图片

 大部分的程序代码转换成目标代码或者虚拟机执行的指令集之前都需经过上述步骤。

  • java代码编译和执行的过程

 Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第61张图片

  •  java字节码的执行是有jvm的执行引擎完成的流程图如下:

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第62张图片

 5.4解释器和JIT编译器

  • 解释器:

当java虚拟机启动是会根据定义的范围对字节码采用逐行解释的方式执行。将每条字节码文件的内容“翻译”为对应平台的本地机器执行

  • JIT编译器:

就是将细腻及的源代码直接编译成和本地机器相关的机器语言。

  • 半解释半编译的特点源于此

现在的jvm执行java代码的时候,通常都会解释执行与编译执行二者结合,也就是上述2个方案都保留了。

  • 解释/编译过程的架构

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第63张图片

 5.4.1解释器

jvm的设计者初衷只是为了实现java的跨平台性,因此避免通过静态编译的方式直接生成本地机器指令,从而诞生了实现编译器在运行时采用解释字节码的想法。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第64张图片

  • 工作机制
  1. 解释器就是在jvm上担任一个翻译的工作,将字节码内容翻译为对应平台(操作系统)的本地机器指令。
  2. 当一条字节码执行被解释器执行完成后,在根据pc寄存器中记录的下一条指令地址执行字节码的执行解释操作 
  • 解释器分类:

字节码解释器;执行纯软件代码,效率低

模板解释器:每个字节码指令和一个函数关联,从模板中直接生成字节码对于的机器码

  • 特点

基于解释器的执行是非常低效的。

5.4.2 JIT编译器

编译器的机制就是编译执行。这种方式就是将方法编译成机器码后直接执行。

在HotspotJVM中依旧是保留了 解释器与即时编译器并存的架构体系。

  • 为什么保留2种体系

因为在程序启动后解释器就可以立马发挥作用,省去编译的时间,立即执行。

JIT编译器想要发挥作用,把代码编译成本地代码是需要时间,但是编译为本地代码后,执行速度很快。简单的理解 一个起步快 加速慢,另一个起步慢加速快。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第65张图片

 因此在JVM刚刚启动的时候可以发挥解释器的作业,不必等等编译器启动。随着时间的推移,JIT逐步发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,提高程序效率。

  • 概念介绍

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第66张图片

5.5热点代码及其探索方式。

关于是否启动JIT需要根据代码被调用的执行频率而定。一般来说被JIT调用的代码称之为“热点代码”JIT会对热点代码进行深度优化,直接编译成本地机器指令,提升java程序执行的性能。

识别为热点代码的主要是根据热点探测功能。

HotspotJVM采用热点探测的功能是基于计数器的热点探测。主要的计数方式有方法调用计数器和回边计数器。

  1. 方法调用计数器就是统计方法调用的次数。
  2. 回边计数器就是统计循环体执行的循环次数。

方法计数器

  1. 默认在Client模式下1500次,Server模式下10000次触发JIT编译。
  2. 这个默认值可以认为设置:-XX:CompileThreshold 设置
  3. 如果一个方法被调用先检查是否被JIT编译过,如果存在则优先使用编译后的本地代码执行,不存在被编译过的版本。这此方法调用JIT计数器累加,判断是否超过默认值,如果默认值超过会提交一个即时编译的请求。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第67张图片

 回边计数器

简单的统计方法体的循环次数触发OSR编译。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第68张图片

 在JVM中可以设置执行方式设置成完全解释器或者完全即时编译器的方法

  • -Xint   :完全解释器方式
  • -Xcomp :完全编译器方式
  • -Xmixed :混合执行(默认)

上面我们提到了客户端(Client)和服务端(Server)模式这个是什么呢? 

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第69张图片

总结来说:

Clenit优化方式简单可靠 耗时短,编译速度快。

Server 优化时间长达到激进优化的目的,但是这个优化后的代码执行更快。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第70张图片

6. 垃圾回收

6.1 垃圾回收概述

        垃圾回收早在Lisp语言诞生了,并不是java专属的概念。

最主要关注的问题:

  1. 那些内存需要回收
  2. 什么时候回收
  3. 如何回收

但是垃圾回收也是java的招牌能力,极大提高了开发的效率。

  • 什么是垃圾回收

垃圾回收知道在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的对象。如果不及时的回收这些对象那么垃圾会一直占用空间直到程序结束,甚至导致程序被动结束。

  • 什么是GC

对弈一个程序来说不进行垃圾回收,内存是迟早会被消耗殆尽。

除了释放对象,垃圾回收还可以清除内存的记录碎片。碎片整理将占用的堆内存移动到一边,以便JVM回收将整理出来的对象分配给新的对象。

有了GC的操作才保证了应用程序的正常进行。

6.2java垃圾回收的机制

        自动内存管理,无需开发人员手动暗语内存的分配与回收,这样降低了内存泄漏和内存溢出的风险。

        自动内存管理机制,将开发人员从繁重的内存管理中释放专注于业务的开发。

  • 内存回收主要的区域

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第71张图片

 垃圾回收器可以对年轻代进行回收也可以对老年代回收甚至回收方法区的垃圾。

  1. Young区(新生区)回收频繁。
  2. Old区(老年区)少量回收
  3. 方法区永久代几乎不动

6.3 垃圾回收算法

        在堆区存放的所有java对象的实例,在GC之前,首先需要区分内存中安歇是存活的对象那些是需要回收的垃圾对象。只有被标记的已经死亡的对象(清除被标记的垃圾)。GC才会在执行垃圾回收的时候,释放占用的内存的空间,因此这个阶段也称之为垃圾标记阶段。

        jvm中规定当一个对象已经不再被任何 存活的对象继续引用的时候,就可以宣布此对象是垃圾。

判断是垃圾对象的2种方式,引用计数算法和可达性分析算法。

6.3.1引用记数算法(垃圾的标记)

        每个对象报错一个整型的引用计数器,用户记录对象被引用的情况。

当有新的对象指向这个对象计数器加一反之减一,当计数器为0表示该对象为垃圾可以进行回收。

  • 优点:

实现简单,垃圾对象便于识别,判断效率高,回收没有延迟。

  • 缺点:

定义单独的计数器增加内存开销

计数器进行 +-操作增加程序的时间开销

当2个垃圾对象互相引用无法识别

因此java并没有使用引用计数算法作为垃圾回收的方式。

6.3.2 可达性分析算法(根搜索、追踪性)(垃圾标记)

        可达性分析算法同样具备容易实现和执行高效的特点,除此之外还解决了循环引用的问题(垃圾互相引用)防止内存泄漏的发生。        

        所谓的根集合“GC root”就必须是一组引用活跃的对象。

基本判断思路:

  1. 可达性算法以root为起始点,按照上下文的方式搜索根对象集合链接的目标是否可达
  2. 使用可达性算法之后,内存中的存活对象都会被根对象集合直接或者间接的引用、链接。搜索的路径称之为引用链。
  3. 如果对象没有被引用链相连,意为不可达对象,称之为垃圾。
  4. 只有可达的对象才是存活对象。 

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第72张图片

如何选取GCroot(根对象集合):

  1. 线程调用的方法使用的参数,局部变量。
  2. 本地方法栈内的引用的对象
  3. 方法区的静态属性引用的对象
  4. 方法区常量的引用
  5. 被synchronieed持有的对象
  6. jvm的内部的引用

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第73张图片

 小技巧:

由于root采用栈的方式存储变量和指针,所以如果是一个指针,那么他保存了对内存里面的对象,但是他自己又不放在对内存里面那么他也是一个Root。

注意点:

如果使用根可达分析算法判断内存是否回收,那么分析工作必须在一个能保证一致性的快照中进行。如果不能保证这点分析的准确性无法保证。

这个注意点也是导致GC过程中 必须终止程序工作的原因。

6.4.对象的Finalization机制。

  1. java提供了对象的终止(Finalization)机制,来允许开发人员提供对象被销毁之前的自定义操作
  2. 当垃圾回收器发现没有引用指向一个对象的时候(垃圾回收之前)总会先调用这个方法finalize()
  3. 在这个方法运行子类中被重写,用于咋对象被回收进行之前进行资源释放。通常这个方法进行一些资源释放和清理工作。例如关闭文件。套接字和数据库连接。
    //此方法只能被调用一次
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("调用当前类重写的finalize()方法");
        obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
    }

建议不主动调用finaliz()方法,应该交给垃圾回收机制调用

finalize方法被调用的时候回导致垃圾对象复活

finalize方法执行的实际是没有保障的,都是由GC线程决定的,不发生GCfinaliez方法将不会被调用。

finalize会影响GC的性能。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第74张图片

 如何判断一个对象是否被回收

判断回收至少需要如下2次标记的过程

  1. 如果对象没有到GCroot的引用链,标记一次
  2. 进行筛选判断对象是否执行终止方法(finalize)
  • 如果对象没有终止方法或者已经执行过一次终止方法,那么此时的对象标记不可触及。
  • 如果对象重写了终止方法并且没有执行过,那么对象会被插入到一个队列中,由JVM创建的,低优先级的Finalizer线程触发finalize方法
  • finalize方法是对象逃逸死亡的最后机会,GC会在队列这个低优先级队列之中进行二次标记,如果在这个过程之中对象及时返回可达链路(和root有链接)那么在二次标记的时候这个对象可以被移除“即将被回收的集合”。

6.5 垃圾清除算法

        区别与上述的垃圾标记算法,接下来讲的算法是如何实现垃圾的清除。

        当我们成功的区分了内存之中存活和死亡的对象之后,GC接下来的任务就是执行垃圾的回收。释放无用对象占用的空间,以便有足够的空间为新对象分配空间。

目前jvm主流常见的垃圾回收算法有三种:

标记清除算法(Mark-Sweep)、复制算法(Copying)、标记压缩算法(Mark-Compact)

6.5.1标记清除算法

是一种常见的垃圾回收算法,在1960年提出运用在Lisp语言。

执行过程:

当堆中的有效空间被消耗殆尽的时候们就会停止程序(stop the word)然后进行2个工作:标记-清除。

  • 标记:Collector从引用根节点开始遍历,标记内引用的对象,一般这个对象的Header中记录为可达对象。
  • 清除:Collector对堆内存从头到尾进行线性遍历。如果发现对象没有在Header中标记可达,进行回收。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第75张图片

 缺点:

  1. 效率不算高
  2. 在GC的时候停止整个程序
  3. 清理的空间内存是不连续的,产生内碎片,需要维护一个空闲列表。

注意:

这里的清除并不是真真的清除置空,而是吧需要清除的对象地址保存着限制空闲地址列表,下次有新的对象进行分配空间的时候,判断垃圾列表是否足够,足够就放。

适用于开销数量和存活数量成正比的情况

6.5.2 复制算法

 思想:

在活的内存空间中将内存空间分为2各部分,使用其中一个区域。在垃圾回收的过程之中将正在使用的内存的存活对象复制到未使用的空间,之后清理正在使用的内存中剩余的垃圾对象。最后完成回收

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第76张图片

 优点:

  1. 实现简单;运行效率高
  2. 复制的过程没有内存片,空间连续

缺点:

  1. 内存开销大
  2. 需要频繁的复制对象GC的过程还要维护 交换区域的对象引用

补充:

适合垃圾对象多,存活对象少的场景。

运用区域 新生代的S0、S1区

6.5.3 标记压缩(整理)算法

上述说道 复制算法合适多垃圾的空间进行回收,但是在我们的老年区或者方法区都是垃圾较少,存活对象较多的区域,基于这一特点我们得使用其他算法。

标记清除算法也可运用在老年代和方法区,但是执行的效率低,而且产内碎片,因此产生了标记压缩算法。、

执行过程:

  1. 第一阶段和标记清除算法的过程一致,从root节点进行标记被引用的对象。
  2. 第二阶段将所有的存活对象压缩到内存的同一端,顺序排放。之后清理处这一段的其他象。

标记压缩算法等同于标记清除算法多了一步内碎片的处理。标记清除没有对象的移动,然而标记压缩有对象的移动。

指针碰撞:

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第77张图片

优点:

  1. 内存开销小
  2. 没有内碎片

缺点:

  1. 效率适中
  2. 同样需要维护地址的引用(移动的时候)
  3. 过程需要暂停程序。

6.5.4对比

       

 6.6分代收集算法

        因为上述的3种算法没有哪一种真实能替代另一种,各有优势。

分代收集算法就由此诞生。它基于一个这样的事实,不同的对象什么周期不一样,因此,不同生命周期的对象可以采用不同的收集方式,以便提高效率。一般JVM堆区分为新生去老年区,这样根据各个特点执行不同的回收算法,提高回收效率。

        Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第78张图片

目前所有的GC都是采用分代 收集算法(Generation Collecting)执行回收处理。

在Hotspot中,基于分代的概念,GC所有使用的内存回收算法必须结合新生区和老年区的特性。

  • 新生区(YoungGen)

内存较小,生命周期短,回收频率高

适用复制算法

  • 老年区(OdlGen)

内存大。生命周期长。存活率高,回收不频繁。

适用标记清除(压缩)算法或者混合使用

在Hotspot中的CMS回收器为例子,CMS垃圾回收器基于Mark-Sweep(标记清除)算法实现,对于对象的回收率很高,对于碎片问题,CMS采用了Mark-Compact算法的SerialOdl回收器作为补偿措施。当CMS执行回收的时候回收的效果不佳碎片严重,将采用SerialOdl执行FullGC达到老年代的内存整理。

分代思想运用广泛的虚拟机中,几乎所有的垃圾回收器都区分新生区和老年区。

 6.7 增量收集算法

上述提及的算法在处理垃圾回收的过程的时候都会出于stop the world 的状态,在这个状态下应用程序的所有线程都挂起,暂停工作。

为了解决这个问题诞生了 增量收集算法(IncrementalCollecting)

基本思想:

如果一次性的所有垃圾回收处理,需要造成很长的时间停顿,那么可以让垃圾回收线程和应用程序的线程交替执行。每次垃圾收集线程值收集一小片区域的内存空间。接着切换应用程序,直到垃圾收集完成。

总的来说,增量手机算法的基础任然是传统的标记清除和复制算法。增量收集算法通过对线程的重提的妥善处理,允许垃圾收集线程以分阶段的方式完成标记,清理或者复制工作。

缺点:

吞吐量下降,回收成本上升

6.8分区算法

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第79张图片

7. 垃圾回收器

根据线程数量划分,可以分为串行垃圾回收器并行垃圾回收器。        

  • 串行垃圾回收器:

        指的是在同一个时段内只允许一个cpu执行垃圾回收操作,其他任务线程停止。直到垃圾回收结束       。(适用于处理器较小,较小应用内存的场合)

  • 并行垃圾收集:

        多个cpu同时执行垃圾回收,提升了吞吐量。不过同样也需要STW(stop the world)停止任务线程。

按照工作模式划分,可以分成并发式垃圾回收器独占式垃圾回收器

  • 并发式垃圾回收器:

        垃圾回收线程和应用程序线程交互执行,减少应用程序的停顿时间。

  • 独占式垃圾回收器:

        垃圾回收器启动,停止其他的应用程序工作,知道垃圾回收结束

除了以上的划分方式更具不同的特点还有如下的划分方式。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第80张图片

 7.1 垃圾回收的性能指标

吞吐量:用户代码运行时间占总运行时间的比例(程序执行时间+垃圾回收时间)。

暂停时间:执行垃圾回收线程,程序暂停运行的时间。

垃圾收集开销:垃圾回收时间与众运行时间的比率。

收集频率:相对与运行程序,垃圾回收操作发生的频率。

内存占用:java堆区占用大小。

快速:一个对象从诞生到被会受到时间。

重点关注吞吐量和暂停时间。

  • 吞吐率:

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第81张图片

  • 暂停时间:

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第82张图片

  • 比较总结:

高吞吐量的较好的原因回让用户觉得只有应用程序在执行任务。,直觉上吞吐量越高程序执行越快。 

低延迟较好的原因是,对于用户而言不管是暂停回收垃圾任务还是程序任务都是不友好的。低延迟就会降低用户感知程序挂起的状态。

但是低延迟和高吞吐量是一个矛盾关系,此消彼长。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第83张图片

7.2 垃圾回收器的分类

串行回收器:Serial、Serial Old

并行回收器:ParNew、Parallel Scavenge 、Parallel Old

并发回收器:CMS、G1。

  • 回收器作用的区域:

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第84张图片

  •  各个垃圾回收器的组合关系:

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第85张图片

 Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第86张图片

#查看命令行的相关参数
-XX:+PrintCommandLineFlages:
#相关垃圾回收器的参数
jinfo -flag

 7.3Serial 串行回收器

Serial是一款最基本/历史最悠久的垃圾收集器。

是在HotSpotJVM下的CLient模式的新生代的默认收集器。

Serial 收集器使用的是 复制算法,兼顾串行和STW机制。

Serial Old提供了老年代的垃圾回收,同样兼顾了串行和STW的机制不同的是Serial Old使用的栓发是标记压缩算法。

这个Serial收集器是一个单线程的收集器,意味者只能使用一个cpu或者哟个线程去完成垃圾回收的工作

  •  流程:

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第87张图片

 优势:

简单高效

单线程执行,没有其他线程干扰。

可以使用如下指令知道新生代老年代使用Serial回收器。

-XX:+USerSerialGC

缺点:

单核,交互能力差,因此现在几乎不适用这个拉萨回收器了。

7.4 ParNew 回收器 并行回收

ParNew就是多线程版本的Serial收集器。

全称 Parallel new,只作用在新生区。

同样是使用复制算法和STW机制 不用同的是使用并行回收方法。

ParNew是很多jvm运行在Server模式下的新生代默认的收集器。

流程:

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第88张图片

对于新生区,回收次数频繁,并行效率高。

对于老年代,回收次数少,串行方式节省资源

 特点:

由于ParNew收集器是基于并行回收,因此在多cpu的环境下,可以发挥cpu数量多的优势更快完成垃圾回收任务。,提升了吞吐量。

但是在单cpu的环境下,ParNew收集器需要频繁的切换线程产生额外的开销。

# 收到指定使用ParNew回收器 在新生区不影响老年代
-XX:+UserParNewGC
# 限制线程数量,默认和cpu相同
-XX:ParallelGCThreads

7.5 parallel 回收器 吞吐量优先

        Hotspot的新生区也是可以使用ParallelScavenge,这款回收器采用了复制算法,并行回收和STW机制。

但是Parallel Scavenge收集器的目标是达到一个可以控制的吞吐量,因此称之为吞吐量优先收集器。

自适应的策略是区分ParNew和parallel Scavenge的重要区别。

高吞吐量可以高效的运用cpu资源。

parallelOld 采用标记压缩算法 并行回收和STW机制 作用在老年代区域

流程:

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第89张图片

 在java8默认使用的是Paralled回收器。

#指定使用parallel 回收器
-XX:USerParallelGC

# 指定老年代是使用并行回收器
-XX:+UserParallelOldGC

# 设置新生代并行收集器的线程数量
-XX:ParallelGCThreads

# 设置垃圾回收最大停顿时间
-XX:MAxGCPauseMillis

#设置垃圾收集器时间占总时间比例
-XX:GCTimeRatio

# 设置自适应策略
-XX:+UserAdaptiveSizePolicy

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第90张图片

 7.6 CMS回收器 (低延迟)

        在jdk1.5 HotspotJVM提出的强交互应用的垃圾收集器。CMS(Concurrent-mark-sweep)

收集器。这款收集器让垃圾收集线程和任务线程同时工作。

cms收集器关注点在于尽可能缩短垃圾收集线程时的停顿时间。提高用户的体验,不会让用户感受到系统的停顿。

cms使用的是标记清楚算法,也是回STW的只是经可能的降低。

但是在CMS作为老年代收集器的时候无法和新生区的ParallelScavenge配合工作,因此新生代只能选则 ParNew或者Serial。

工作流程: 

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第91张图片

主要分为4个阶段--初始标记--并发标记--从新标记--并发清理 

初始标记阶段:

        在这阶段程序的线程都会STW ,标记出没有和ROOTGC关联的对象 速度较快

并发标记:

        从ROOTGC的直接关联对象进行遍历,时间较长但是 不会STW可以和任务线程同时发生

从新标记:

       修正并发标记期间,因为用户程序继续运行而产生导致的标记变得的那一部分对象的标记记录。这个时间停顿<并发标记,>初始标记。

并发清理:

        清理删除标记判断的已经死亡的对象,释放空间。不用改变对象地址或者指向,因此是并发执行的。

  •  介绍:

尽管cms是并发回收的,但是在初始化标记阶段和再次标记阶段都需执行STW机制。只是说尽可能减少了暂停的时间,因为并发标记和并发清理都不需要停顿所以说是低停顿的。

在垃圾回收阶段也没有停止用户线程,所以在cms的垃圾回收阶段,应该尽可能的保证应用程序的用户线程有足够的空间。所以说cms不会像其他收集器一样等待老年代空间满了才进行垃圾回收,而是当内存使用到了一定的峰值,就开始进行垃圾回收。确保程序在cms阶段工作中的程序依然有和足够的内存空间使用。但是如果还是堆空间不够的这个时间JVM会启动SerialOld进行老年代的回收,消耗大量的时间

因为cms使用的标记清楚算法,意味着清除对象之后的空闲地址是不连续的,可能会产生内碎片的问题。这样就需要引入空闲表来管理空闲地址。        

  • 优点:

并发执行

低延迟

  • 缺点:

产生内碎片

cms非常依赖cpu资源

无法清理浮动垃圾,在垃圾收集阶段和程序运行阶段,如果在并发标记的时候程序产生垃圾cms无法回收这部分垃圾只能等待下一次。

# 指定使用cms
-XX:+UserConcMarkSweepGC

#设置内存使用的峰值 处罚执行cms
-XX:CMSlnitiatingOccupanyFraction

# 指定在执行完成fullCG后堆空间进行压缩
-XX:CMSFullGCsBeforeCompaction

#用于执行完成Fullgc后对内存进行压缩
-XX:+UserCMSCompactionAtFullCollection

#设置cms线程数量
-XX:ParallelCMSThreads
  • 总结:

最新化使用内存和并行开销 选择 serialGC

最大化程序应用吞吐量ParallelGC

最小话GC停顿时间 cmsGC

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第92张图片

 7.7 G1垃圾回收器(区域化分代)

        G1的目标是在延迟可控的情况下获得尽可能高的吞吐量,是全功能收集器的开始。

G1是一个并行回收器,他把内存划分为很多不相干的区域(Region)。使用不同的区域标识Eden。

G1避免了整个java堆的全区域垃圾回收。G1跟踪Region里面的垃圾堆提交的价值大小在后台维护一个优先级列表,根据优先级,进行优先回收最有价值的Region

G1是面向服务端的垃圾回收器,主要正对多喝cpu和大容量内存的机器。 兼备高吞吐量的性能。

jdk7后正式启用,在jdk9成为默认的垃圾收集器取代了cms,称之为全功能收集器。

在jdk8我们需要手动启动G1

-XX:+UserG1GC
  • 优点:

并行并发:g1的时候多个GC线程同时工作,g1有着和应用程序交替执行的能力。不会发生阻塞

分代收集:依然属于分代收集器,区分老年代和新生代,将堆区划分多个R区,可以回收老年代和新生代。

空间整合:剔除了CMS内碎片的问题,G1内存回收以Region为单位Region之间是复制算法,但是整体又是标记压缩算法。可以避免内碎片的产生。

可预测的停顿时间模型:处理尽可能降低停顿时间,还可以简历预测停顿时间。

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第93张图片

  • 缺点:

#启动G1收集
-XX:+UserG1GC

# 设置每个R区大小  值是2的平方 1-32mb
-XX:G1HeapRegionSize

#设置最大的gc停顿时间指标
-XX:MAxGCPauseMillis

#设置stw的gc线程数量 <8
-XX:ParallelGCThread

#设置并发标记线程数量 
-XX:ConGCthreads

#设置出发并行GC的周期java堆的占有率的峰值 默认45
-XX:InitiatingHeapOccupancyPercent
  •  G1的使用场景
  1. 面向服务的,内存大,处理器多
  2. 需要降低gc延迟,并且具有答对的应用
  3. 针对CMS收集器出现 超过1/2的java堆被占用后,分配律或者老年代提升率高的,gc延迟高的使用G1。
  • 化整为零

使用G1收集器,他会将堆划分为2048个大小相同的Region,可以人为的设置大小。

保留了老年代和新生代的概念,但是不再是物理的隔离,他们是一部分的region。

  • G1回收的3个环节及其过程
  1. 年轻代(YoungGC)
  2. 老年代的并发标记
  3. 混合回收

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第94张图片

 Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第95张图片

 Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第96张图片

 上提到的Remebered Set就是上述Reset,上页提到的Reference类型就是引用类型,其中Reset的作用是记录当前Region中哪些对象被外部引用指向,比如Old区中的对象会指向Eden区的对象,然后当我们要回收某个Region的时候,直接遍历遍历当前Region中的所有对象就可以了,然后针对性的去找到那些指向当前对象的其他对象,最终发现当前对象是否是根可达的,如果不是,那就应该被删除,其实之前的垃圾回收器都涉及到这个问题,当进行Minor GC的时候,通过GC Roots查找的时候还需要遍历Old区的对象,毕竟Old区对象也可能会指向Eden区对象,但是G1通过Rset避免了全堆的扫描,当引用类型数据写操作时,先暂时中断,然后判断当前引用类型数据是否被其他对象所指向,如果不被指向,那就直接放在Region中就可以了;如果被其他对象指向,那么还要判断这个对象是在当前要插入的Region中,还是在其他Region中;如果在其他Region中,那就需要使用CardTable把当前引用类型数据的指向信息放在Rset中,也就是形成上面的虚线连线,如果在当前Region中,那就不需要指向了,毕竟到时候我们会进行遍历查找根可达对象,那肯定会找到的,所以这种情况也是直接放在Region中就可以了;

 Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第97张图片

  •  G1对年轻代的回收:

        G1先准备好对eden区,程序在运行过程中不断创建对象到eden区,当空间耗尽就会出发G1回收年轻代。

年轻代对Eden和S区进行回收

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第98张图片

  •  G1的并发标记过程

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第99张图片

 7.8 各个回收器的总结

Java的虚拟机JVM介绍(类的加载、内存结构、垃圾回收)_第100张图片

你可能感兴趣的:(java基础,jvm,java,面试)