JVM上篇:内存与垃圾回收

注:本文浓缩了宋红康老师JVM 入门到精通上篇(内存与垃圾回收)的精华

文章目录

JVM

Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。

特点:

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

JVM 的位置

JVM是运行在操作系统之上的,它与硬件没有直接的交互。
在这里插入图片描述

JAVA代码的执行流程

在这里插入图片描述

JVM 整体结构

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

JVM 的架构模型

Java编译器输入的指令流基本上是一种基于栈的指令集架构,

另外一种指令集架构则是基于寄存器的指令集架构。工具体来说:这两种架构之间的区别:

  • 基于栈式架构的特点
    设计和实现更简单,适用于资源受限的系统
    避开了寄存器的分配难题:使用零地址指令方式分配
    指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小编译器容易实现。
    不需要硬件支持,可移植性更好,更好实现跨平台

  • 基于寄存器架构的特点
    典型的应用是x86的二进制指令集:比如传统的PC以及 Android的Davlik虚拟机。
    指令集架构则完全依赖硬件,可移植性差

    性能优秀和执行更高效

    花费更少的指令去完成一项操作。
    在大部分情況下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于根式架构的指令集却是以零地址指令为主

同样执行2+3这种逻辑操作,其指令分别如下:
基于栈的计算流程(以Java虚拟机为例)

 iconst2//常量2入栈
 istore_1
 iconst_3//常量3入栈
 istore_2
 i1oad_1
 iload_2
 iadd
 //常量2、3比,执行相加
 istore_0//结果5入栈

而基于寄存器的计算流程

mov eaX,2//将eax奇存器的值设1
add eax,3//使eax高存的值加3

由于跨平台性的设计,Java的指令都是根据栈设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

JVM的生命周期

1 虚拟机的启动

Java虚拟机的启动是通过引导类加载器( bootstrap class loader)创建个初始类( initial c1ass)米完成的,这个类是由虚拟机的具体实现指定的。

2 虚拟机的执行

  • 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。
  • 程序开始执行时他才运行,程序结束时他就停止。
  • 执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。

3 虚拟机的退出

  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止
  • 某线程调用 Runtime类或 System类的exit方法,或 Runtime类的halt方法,并且Java安全管理器也允许这次exit或ha1t操作。
  • 除此之外,JNT( Java Native Interface)规范描述了用JNT Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情況

主流JVM 的发展历程

1 Sun Classic VM

早在1996年Java1.0版本的时候,Sun公司发布了一款名为 Sun Classic VM的Java虚拟机,它同时也是世界上第一款商用Java虚拟机,JDK1.4时
完全被淘汰。
这款虚拟机内部只提供解释器。
如果使用JIT编译器,就需要进行外挂。但是一旦使用了JIT编译器,JIT就
会接管虛拟机的执行系统。解释器就不再工作。解释器和编译器不能配合工作
现在 hotspot内置了此虚拟机

2 Exact VM
为了解決上。JDK1.2时,Sun提供了此虚拟机

  • Exact Merry Management:准确式内存管理
    • 也可以叫Mon- Conservative/ Accurate Memory Management
    • 虚拟机可以知道内存中某个位置的数据具体是什么类型。
  • 具备现代高性能虛拟机的雏形
    • 热点探测
    • 编译器与解释器混合工作模式
  • 只在 Solaris平台短暂使用,其他平台上还是classic VM英雄气短,终被 Hotspot.虛拟机替换

3 Hotspot VM

  • Hotspot历史
    最初由一家名为“ Longview Technologies“的小公司设计1997年,此公司被Sun收购;2009年,Sun公司被甲甲骨文收购。JDK1.3时, Hotspot VM成为默认虚拟机
  • 目前 Hotspot占有绝对的市场地位,称霸武林
    • 不管是现在仍在广泛使用的JDK6,还是使用比例较多的JDK8中,默认的虚拟机都是hotspot
    • Sun/ Oracle JDK和 OPENJDK的默认虚拟机
  • 从服务器、桌面到移动端、嵌入式都有应用
  • 名称中的 Hotspot指的就是它的热点代码探测技术。
    • 通过计数器找到最具编译价值代码,触发即时编译或栈上替换
    • 通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡

3 BEA的 JRockit

  • 专注于服务器端应用
    它可以不太关注程序启动速度,因此 Jrockit内部不包含解析器实现,全部代码
    都靠即时编译器编译后执行
  • 大量的行业基准测试显示, JRockit JVM是世界上最快的JM。
    使用 JRockit产品,客户已经体验到了显著的性能提高(一些超过了70%)和硬件成本的减少(达50%)
  • 优势:全面的Java运行时解决方案组合
    JRockit而向延迟敏感型应用的解决方案 JRockit Real Time提供以毫秒或微秒级的JM响应时间,适合财务、军事指挥、电信网络的需要
    Missioncontro1服务套件,它是一组以极低的开销来监控、管理和分析生产环境中的应用程序的工具
  • 2008 年被Oracle收购
  • oracle表达了整合两大优秀虚拟机的工作,大致在JDK8中完成。整合的方式是在Hotspot的基础上,移植 JRockit的优秀特性。
  • 高斯林:目前就职于谷歌,研究人工智能和水下机器人

4 IBM 的 J9

  • 全称: IBM Technology for Java Virtual Machine,简称IT4J,内部代号:J9
  • 市场定位与 Hotspot接近,服务器端、桌面应用、嵌入式等多用途VM
  • 广泛用于IBM的各种Java产品。
  • 目前,有影响的三大商用虚拟机之一,也号称是世界上最快的Java虚拟机
  • 2017年左右,IBM发布了开源J9 VM,命名为OpenJ9,交给 Eclipse基金会管理 ,也称为 Eclipse Open9

5 Graal VM

  • 2018年4月,Oracle Labs公开了Graal VM,号称"Run Programs Faster Anywhere”,勃勃野心
    与1995年java的” write once, run anywhere"遥相呼应。

  • Graal VM在 Hotspot VM基础上増强而成的跨语言全栈虚拟机,可以作为“任何语言的运行平台使用。语言包括:Java、 Scala、 Groovy、Kotlin;C、C++ JavaScript、Ruby、 Python、R等

  • 支持不同语言中混用对方的接口和对象,支持这些语言使用己经编写好的本地库文件

  • 工作原理是将这些语言的源代码或源代码编译后的中间格式,通过解释器转换为能被Graal VM接受的中表示。Graal VM提供 Truffle工.具集快速构建而向一种新语言的解释器。在运行时还能进行即时編译优化,获得比原生编译器更优秀的执行效率

  • 如果有一天Hotspot VM真的被取代,Graal VM希望最大。但是Java的软件生态没有丝毫变化。

类的加载子系统

类的加载子系统的作用

在这里插入图片描述

  • 类加载器子系统负责从文件系统或者网络中加载Class文件,Class文件在文件开头有特定的文件标识。
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由 Execution Engine决定。
  • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字而量和数字常量(这部分常量信息是class文件中常量池部分的内存映射

类的加载过程

在这里插入图片描述

加载

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区(或叫元空间)的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

链接

验证( Verify)

  • 目的在于确保c1ass文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性不会危害虚拟机自身安全
  • 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证

准备( Prepare):

  • 为类变量分配内存并且设置该类变量的默认初始值,即零值
  • 这里不包含用final修饰的 static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中

解析( Resolve)

  • 将常量池内的符号引用转换为直接引用的过程事实上,解析操作往往会伴随着JM在执行完初始化之后再执行
  • 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info, CONSTANT_Fieldref_info, CONSTANT_Methodref_info等

类的初始化

  • 初始化阶段就是执行类构造器方法<clinit>()的过程
  • 此方法不需定义,是方Javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
  • 构造器方法中指令按语句在源文件中出现的顺序执行。
  • < clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())
  • 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。加载出现问题则会引起其他线程阻塞

类的加载器分类

虚拟机自带的加载器

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。

  • 它用来加载Java的核心库( JAVA HOME/jre/lib/rt.jar resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
    并不继承自java.lang.Classloader,没有父加载器

  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

  • 出于安全考虑, Bootstrap.启动类加载器只加载包名为Java、Javax、sun等开头的类

扩展类加载器( Extension ClassLoader)

  • Java语言编写,由sun.misc. Launcher$extclassloader实现
  • 派生于Classloader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安
    装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

应用程序类加载器(系统类加载器,AppClassloader)

  • Java语言编写,由sun.misc. Launcher$AppClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量Classpath或系统属性java.class.path指定路径下的类库
  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
  • 通过Classloader# getsystemclassLoader()方法可以获取到该加载器

用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式:

隔离加载类
修改类加载的方式
扩展加载
防止源码泄漏

获取ClassLoader 的实例

方式一:获取当前类的Classloader

clazz. getclassloader()

方式二:获取当前线程上下文的Classloader

Thread currentthread().getcontextclassloader()

方式三:获取系统的Classloader

Classloader. getsystemclassloader ()

方式四:获取调用者的ClassLoader

Drivermanager. getcallerclassloader()

双亲委派机制

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

工作原理

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委模式
    在这里插入图片描述

优势

  • 避免类的重复加载
  • 保护程序安全,防止核心API被随意算改,这种机制叫沙箱安全机制
    自定义类:Java.lang.string

在JVM中表示两个class对象是否为同一个类存在两个必要条件:

  • 类的完整类名必须一致,包括包名
  • 加这个类的Classloader(指Classloader实例对象)必须相同

JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用伸为类型息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

类的主动使用和被动使用

主动使用

  • 创建类的实例
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射
  • 初始化一个类的子类
  • Java虚拟机启动时被标明为启动类的类
  • JDK7开始提供的动态语言支持:
    java.lang. invoke.Methodhandle实例的解析结果
    REF_getstatic、 REF_ putstatic、 REF_invokeStatic句柄对应的类没有初始化,则初始化

除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。

运行时数据区

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着 虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些 与线程对应的数据区域会随着线程开始和结束而创建和销毁。

灰色的为单独线程私有的,红色的为多个线程共享的。即:
每个线程:独立包括程序计数器、栈、本地栈
线程间共享:堆、堆外内存(永久代或元空间、代码缓存)

在这里插入图片描述
在这里插入图片描述
线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。
在 Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。 当一个Java线程准备好执行以后,此时一个操作系统的本地线程 也冋时创建。Java线程执行终后,本地线程也会回收。 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本 地线程初始化成功,它就会调用Java线程中的run()方法。

这些主要的后台系统线程在 Hotspot:JVM里主要是以下几个

  • 虚拟机线程:这种线程的搡作是需要JM达到安全点才会出现。这些操作必须在不 同的线程中发生的原因是他们都需要J达到安全点,这样堆才不会变化。这种线 程的执行类型包括〃stop-the-wor1d〃的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销
  • 周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性 操作的调度执行。
  • GC线程:这种线程对在JM里不同种类的垃圾收集行为提供了支持。
  • 编译线程:这种线程在运行时会将字节码编译成到本地代码。
  • 信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理

程序计数器

介绍

在这里插入图片描述
JVM中的程序计数寄存器(Program counter Register)中, Register的命名源于 CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。 这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴
切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC 寄存器的一种抽象模拟

它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存 储区域。 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命 周期与线程的生命周期保持一致。 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址:或者, 如果是在执行 native方法,则是未指定值( undefined)

作用

PC寄存器用来存储指向下一条指令的地址也即将要执行的指令代码。由执行引擎读取下一条指令。

在这里插入图片描述
它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础 功能都需要依赖这个计数器来完成。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的节码指令

它是唯一一个在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

在这里插入图片描述

1 为什么使用PC寄存器记录当前线程的执行地址呢?

因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续 执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

2 PC寄存器为什么会被设定为线程私有?

所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU 会不停地做任务切换,这样必然导致经常中断或恢复,为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程 都分配一个pc寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。 由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理 器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。 这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。

虚拟机栈

栈是运行时的单位,而堆是存储的单位。 一个栈帧对应着一个方法
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。

Java虚拟机栈是什么?

Java虚拟机栈( Java virtual Machine stack),早期也叫Java栈。 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧
( stack Frame),对应着一次次的Java方法调用。 是线程私有的

生命周期

生命周期和线程一致。

作用

主管Java程序的运行,它保存方法的局部变量(基本数据类型,引用数据类型的引用地址)、部分结果,并参与方法的 调用和返回。

栈是一种快速有效的分配存储方式,访问速度仅次于程 序计数器

JVM直接对Java栈的操作只有两个:

  • 每个方法执行,伴随着进栈(入栈、压栈)
  • 执行结束后的出栈工作

对于栈来说不存在垃圾回收问题

栈中可能出现的异常

  • Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量
    可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java 虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError异常。
  • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足 够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈, 那Java虚拟机将会抛出一个 OutofMemoryError异常。

设置栈内存大小
可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用最大可达深度

栈内部的存储结构

  • 每个线程都有自己的栈,栈中的数据都是以栈帧( Stack Frame)的格式存在
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧( Stack Frame)。
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

JVM直接对Java栈操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前 在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧
( Current frame),与当前栈帧相对应的方法就是当前方法( Current Method),定义这个方法的类就是当前类( Current class)。
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的 顶端,成为新的当前帧。

注意:

  1. 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用 另外一个线程的栈帧。
  2. 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果 给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
  3. Java方法有两种返回函数的方式,一种是正常的函数返回,使用 return指令;另 外一种是抛出异常没有处理异常。不管使用哪种方式,都会导致栈帧被弹出。

栈帧的结构

每个栈帧中存储着:

  • 局部变量表(Local Variables)
  • 操作数栈( Operand stack)(或表达式栈)
  • 动态链接( Dynamic Linking)(或指向运行时常量池的方法引用)
  • 方法返回地址( Return Address)(或方法正常退出或者异常退出的定义)
  • 一些附加信息
    在这里插入图片描述

局部变量表

局部变量表也被称之为局部变量数组或本地变量表

  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量

    这些数据类型包括各类基本数据类型、对象引用( reference),以及 returnAddress类型。

  • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据 安全问题

  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code 属性的 maximum local variables数据项中。在方法运行期间不会改变局部变量表的大小的

  • 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次 数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀 它的栈帧就越大,以满足方法调所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少

  • 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过 局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后随着方法栈帧的销毁,局部变量表也会随之销毁。

  • 普通方法和构造器方法的局部变量表里会有this 变量,其放在索引0位置

  • double 类型 及long类型会占据2个索引插槽,调用时使用的是起始插槽,其他类型均占有一个索引插槽

  • 栈帧中的局部变量表中的索引槽是可以重复利用的。如果一个局部变量过了其作用域,那么在其作用域之后申眀的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的。

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

操作数栈

  1. 每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出 (Last-In- First-out)的操作数栈,也可以称之为表达式栈
    (Expression Stack)
  2. 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
    • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。
    • 比如:执行复制、交换、求和等操作
      操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量 临时存储空间

操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候 个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maκ stack的值

栈中的任何一个元素都是可以任意的Java数据类型。
32bit的类型占用一个栈单位深度
64bit的类型占用两个栈单位深度

操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准 的入栈(push)和出栈(pop)操作来完成一次数据访问。

如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作 数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译 器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。

在这里插入图片描述
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题, Hotspot JVM的设计者们提出了栈顶 缓存(Top-of- Stack Cashing)技术,将栈顶元素全部缓存 在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

动态链接

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用 包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接 ( Dynamic Linking)。比如: invokedynamic指令在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在 class文件的常量池里 比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

方法的调用

在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。

静态链接:
当一个字节码文件被装载进JM内部时,如果被调用的目标方法在编译期可知且运行期保持不变时。这种情况下将调用方法的符号引用转为直接引用的 过程称之为静态链接。

动态链接:
**如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,**由于这种引用转换过具备动态 性,因此也就被称之为动态链接。

对应的方法的绑定机制为早期绑定(Early Binding)和晚期绑定 ( Late Binding),绑定是一个字段、方法或者类在符号引用被替换为
直接引用的过程,这仅仅发生一次。

早期绑定:
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时 即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定:
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

虚方法与非虚方法

如果方法在编译期就确定了具体的调用版木,这个版本在运行时是不可变的。 这样的方法称为非虚方法
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。

其他方法称为虚方法。

虚拟机中提供了以下几条方法调用指令

  • 普通调用指令
  1. invokestatic:调用静态方法,解析阶段确定唯一方法版本
  2. invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
  3. invokevirtual:调用所有虚方法
  4. invokeinterface:调用接口方法
  • 动态调用指令:

    invokedynamic:动态解析出需要调用的方法,然后执行

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而 invokedynamic指令则支持由用户确定方法版本**。其中 invokestatic指令和 invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。**

方法返回地址

存放调用该方法的PC存器的值
一个方法的结束,有两种方式:
正常执行完成
出现未处理的异常,非正常退出

无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的 指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

当一个方法开始执行后,只有两种方式可以退出这个方法:

  1. 执行引擎遇到任意一个方法返回的字节码指令( return),会有返回值
    传递给上层的方法调用者,简称正常完成出口;
    一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。
    在字节码指令中,返回指令包含 ireturn(当返回值是 boolean、byte、char short和int类型时使用)、lreturn、 freturn、 dreturn以及 areturn,另外还有一个 return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用
  2. 在方法执行的过程中遇到了异常( Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处器,就会导致方法退出。简称异常完成出口。
    方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。

本地方法栈

Java虛拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。本地方法栈,也是线程私有的。允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个 stackoverflowError异常。如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存, 或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个 outofMemoryError异常。

本地方法是使用C语言实现的。它的具体做法是 Native method stack中登记 native方法,在Execution Engine执行时加载本地方法库。

  • 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
    • 本地方法可以通过本地方法接口来访问虚拟机内都的运行时数据区。
    • 它甚至可以直接使用本地处理器中的寄存器
    • 直接从本地内存的堆中分配任意数量的内存。
  • 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持 native方法,也可以无需实现本地方法栈。
  • 在 Hotspot v中,直接将本地方法栈和虚拟机栈合二为一。

概述

一个进程对应一个JVM的实例,一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。堆内存的大小是可以调节的。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( Thread Local Allocation Buffer,TLAB)

  • 几乎所有的对象实例以及数组都应当在运行时分配在堆上
  • 数组和对象可能永远不会存储栈因枝硕中保存引用,这个引用指向 对象或者数组在堆中的位置。
  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
  • 堆,是GC( Garbage Collection,垃圾收集器)执行垃圾回收的重点
    在这里插入图片描述

堆划分

ava8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间

  • Young Generation Space 新生区

    Young/New 又被划分为Eden区和 Survivor区

  • Tenure generation space养老区 Old/Tenure

  • Meta Space 元空间 Meta

设置堆空间内存大小

Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,

默认情况下:初始内存大小:物理电脑内存大小/64
最大内存大小:物理电脑内存大小/4

可以通过选项”-Xmx”和”-Xms〃来进行设置 :建议将初始值和最大的设置一致的

-Xms 用于表示堆区(年轻代和老年代)的起始内存,等价于-XX: InitialHeapsize
-Xmx"则用于表示堆区(年轻代和老年代)的最大内存,等价于-XX: MaxHeapsize

一旦堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OutOfMemoryError异常。
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。

查看设置的参数

  • jps + jstart -gc 进程id
  • -XX:+PrintGCDetais

年级代与老年代

存储在JVM中的Java对象可以被划分为两类

  • 类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速

  • 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。

Java堆区进一步细分的话,可以划分为年轻代( YoungGen)和老年代( oldGen)
其中年轻代又可以划分为Eden空间、 Survivor0空间和 Survivor1空间(有时也叫做from区、to区 )

在这里插入图片描述
配置新生代与老年代在堆结构的占比。
默认-XX: NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改-XX: NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5

在 Hotspot中,Eden空间和另外两个 Survivor空间缺省所占的比例是8:1: 1
当然开发人员可以通过选项“-XX: SurvivorRatio”调整这个空间比例。比
如 -XX: SurvivorRatio=8

几乎所有的Java对象都是在Eden区被new出来的(除非对象超过其大小,会移动到老年代)。

绝大部分的Java对象的销毁都在新生代进行了。

对象分配过程

  1. new的对象先放伊甸园区。此区有大小限制。
  2. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收( Minor gc),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
  3. 然后将伊甸园中的剩余对象移动到幸存者0区。
  4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
  5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
  6. 啥时候能去养老区呢?可以设置年龄计数次数。默认是15次。 超过15次仍然存活的会移动到老年代
    可以设置参数:-XX: MaxTenuringThreshold=进行设置
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    总结:

针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to

关于伊甸园:伊甸园区满了会触发新生代GC,此时也会检查s0 或者s1 的对象存活(被动触发GC)

关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区元空间收集。

在这里插入图片描述

常用的调优工具

JDK命令行
Eclipse: Memory Analyzer Tool
Jconsole
VisualVM
Profiler
Java Flight Recorder
GCViewer
GC Easy

Minor GC、 Major GC 与Full GC

JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代
针对 Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:

一种是部分收集 ( Partial GC),一种是整堆收集(Full GC)

部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:

  • 新生代收集( Minor GC/ Young GC):只是新生代的垃圾收集

  • 老年代收集( Major GC/ old GC):只是老年代的垃圾收集。

    目前,只有 CMS GC会有单独收集老年代的行为。
    注意,很多时候 Mayor GC会和Full GC混淆使用,需要具体分辨是老年代 回收还是整堆回收。

  • 混合收集( Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
    目前,只有G1 GC会有这种行为

整堆收集:(FullGC):收集整个Java堆和方法区的垃圾收集。

触发时机

年轻代GC( Minor GC)触发机制

  • 当年轻代空间不足时,就会触发 Minor GC,这里的年轻代满指的是Eden代满, Survivor满不会引发GC。(每次 Minor GC会清理年轻代的内存)
  • 因为Java对象大多都具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
  • Minor GC会引发STW(用户线程暂停),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
    在这里插入图片描述

老年代GC( Major GC/Full GC)触发机制

  • 指发生在老年代的GC,对象从老年代消失时,我们说“ Major GC”或“Full GC” 发生了。
  • 出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行 Mayor GC的策略选择过程)。
  • 也就是在老年代空间不足时,会先尝试触发 Minor GC。如果之后空间还不足,则触发 Major GC
  • Major GC的速度一般会比 Mino GC慢10倍以上,STW的时间更长。
  • 如果 Major GC后,内存还不足,就报OOM了。 Major GC的速度一般会比 Minor GC慢10倍以上。

Full GC触发机制

触发Full GC执行的情况有如下五种:

  1. 调用 System.gc()时,系统建议执行Full GC,但是不必然执行
  2. 老年代空间不足
  3. 方法区空间不足
  4. 通过 Minor GC后进入老年代的平均大小大于老年代的可用内存
  5. 由Eden区、 survivor space0( From Space)区向 survivor space1(To Space)区复制时,对象大小大于 To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

说明:Full GC是开发或调优中尽量要避免的。这样暂时时间会短一些。 当出现OOM之前肯定执行了一次Full GC

内存分配策略

如果对象在Eden出生并经过第一次 Minor GC后仍然存活,并且能被 Survivor容纳的话,将被移动到 Survivor空间中,并将对象年龄设为1。对象在 Survivor区中每熬过一次 Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中

对象晋升老年代的年龄阈值,可以通过选项-XX: MaxTenuringThreshold来设置

针对不同年龄段的对象分配原则如下:

  • 优先分配到新生代的Eden
  • 大对象直接分配到老年代
    尽量避免程序中出现过多的大对象
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断
    如果 Survivor区中相同年龄的所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到 MaxTenuringThreshold中要求的年龄
  • 空间分配担保
    -XX: HandlePromotionFailure

TLAB(Thread Local Allocation Buffer )

堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内
存空间是线程不安全的.为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
在这里插入图片描述
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题, 同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。

尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为 内存分配的首选。

在程序中,开发人员可以通过选项“-XX: UseTLAB”设置是否开启TLAB空间。
默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的,当然我们可以通 过选项“-XX: TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百 分比大小。
一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

在这里插入图片描述

堆空间的参数设置

-XX:+ PrintFlagsInitial:查看所有的参数的默认初始值
-XX+ PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改不再是初始值)
-Xms:初始堆空间内存(默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比

-XX: SurvivorRatio:设置新生代中Eden和S0/S1空间的比
-XX: MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的GC处理日志
打印GC简要信息:1 -XX:+PrintGc 2 - verbose:gc

-XX: HandLepromotionFailure 是否投置空间分配担保

堆是分配对象的唯一选择吗

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有种特殊情况,那就是如果经过逃逸分析( Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。

如何将堆上的对象分配到栈,需要使用逃逸分析手段 ?
这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

通过逃逸分析, Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象动态作用域:

  • 当一个对象在方法中被定义后,new的对象实体只在方法内部使用,则认为没有发生逃逸则使用栈上分配随着方法执行的结束,栈空间就移除
  • 当一个对象在方法中被定义后,new的对象实体被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中

在JDK6u23版本之后, Hotspot中默认就已经开启了逃逸分析。
如果使用的是较早的版本,开发人员则可以通过:
选项“-XX:+ DoEscapeAnalysis〃显式开启逃逸分析
通过选项“-XX:+ PrintEscapeAnalysis〃查看逃逸分析的筛选结果。

结论:开发中能使用局部变量的,就不要使用在方法外定义。

逃逸分析代码优化

使用逃逸分析,编译器可以对代码做如下优化:

  • 栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选而不是堆分配分配完成后,继续在调用栈执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无需进行垃圾回收
  • 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除,
  • 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。 标量( Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
    相对的,那些还可以分解的数据叫做聚合量( Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
    在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。 参数-XX:+ Eliminate1 Locations:开启了标量替换(默认打开),允许将对象打散分配在栈上

方法区(元空间)

栈 堆 方法区的交互关系

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

方法区的理解

《Java虚拟机规范》中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于 Hotspot JVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
所以,方法区看作是一块独立于Java堆的内存空间。

  • 方法区( Method area)与Java堆一样,是各个线程共享的内存区域

  • 在jdk7及以前,习惯上把方法区,称为永久代。jdk8开始,便用元空间取代了永久代

  • 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。

  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。

  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。

  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢岀错误:java.lang. outofMemoryError:PermGen space 或者 java. lang. OutofMemoryError: Metaspace

    • 加载大量的第三方的jar包;
    • Tomcat部署的工程过多(30-50)
    • 大量动态生成反射类
  • 关闭JVM就会释放这个区域的内存。

设置方法区的大小

方法区的大小不必是固定的,jVM可以根据应用的需要动态调整。

JDK7及以前:

通过-XX: Permsize来设置永久代初始分配空间。默认值是20.75M
-XX: MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是 82M
当JVM加载的类信息容量超过了这个值,会报异常 OutofMemoryerror: PermGen space

JDK8及以后:

  • 元数据区大小可以使用参数-xx: MetaspaceSize和-xx: MaxMetlaspacesize指定替代上述原有的两个参数。
  • 默认值依赖于平台。 windows下,-XX: MetaspaceSize是21M,
    -XX MaxMetaspaceSize的值是-1,即没有限制。
  • 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存如果元数据区发生溢出,虚拟机一样会抛出异常 OutofmemoryError: Metaspace
  • -XX: MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JM来说其默认的-XX: Metaspacesize值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活) .然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过 MaxMetaspacesize时,适当提高该值。如果释放 空间过多,则适当降低该值。
  • 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到FullGC多次调用。
  • 为了避免频繁地GC,建议将-XX: MetaspaceSize设置为一个相对较高的值。

方法区的内部结构

在这里插入图片描述
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等

类型信息

对每个加载的类型(类class、接口 interface、枚举enum、注解 annotation),JVM必须在方法区中存储以下类型信息:
①这个类型的完整有效名称(全名=包名,类名)
②这个类型直接父类的完整有效名(对于 interface或是java. lang Object,都没有父类)
③这个类型的修饰符(public, abstract,final的某个子集)
④这个类型直接接口的一个有序列表

域(Field)信息

JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称、域类型、域修饰符( public, private, protected, static, final, volatile, transient的某个子集)

方法( Method)信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

  • 方法名称
  • 方法的返回类型(或void)
  • 方法参数的数量和类型(按顺序)
  • 方法的修饰符( public, private, protected, static,fina1, synchronized, native, abstract的一个子集)
  • 方法的字节码( bytecodes)、操作数栈、局部变量表及大小( abstract和 native方法除外)
  • 异常表( abstract和 native方法除外)
    每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、 被捕获的异常类的常量池索引

non-final的类变量

静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。

类变量被类的所有实例共享,即使没有类实例时你也可以访问它。

全局常量: static final

被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。

运行时常量池

静态常量池

字节码文件,内部包含了静态的常量池。
在这里插入图片描述
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表( Constant pool Table),包括各种字面量和对类型、 域和方法的符号引用(描述)。

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

几种在常量池内存储的数据类型包括:

  • 数量值
  • 字符串值
  • 类引用
  • 字段引用
  • 方法引用

静态常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型

静态常量池表( Constant pool Table)是 Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池

方法区(元空间),内部包含了运行时常量池。
在这里插入图片描述

  • 运行时常量池( Runtime Constant pool)是方法区(元空间)的一部分。

  • 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。

  • JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样, 是通过索引访问的。

  • 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了, 使用栈帧的动态链接将符号引用转换为真实地址

    • 运行时常量池,相对于Class文件静态的常量池的相比:具备动态性 :例如:string.intern()
  • 运行时常量池类似于传统编程语言中的符号表( symbol table),但是它所包含的数据却比符号表要更加丰富一些。

  • 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛 OutOfMemoryError异常。

方法区的演变

1.首先明确:只有 Hotspot才有永久代
BEA JRockit、IBM J9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。

2 Hotspot中方法区的变化:

在这里插入图片描述

元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存

1 永久代为什么要被元空间替换?

  • 和JRockit进行融合,JRockit没有是元空间

  • 永久代设置空间大小是很难确定的。

    在某些场景下,如果动态加载类过多,容易产生Perm区的OOM

  • 对永久代进行调优是很困难的

2 String Table为什么要调整?

JDK7中将 StringTable放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会触发。而Full GC是老年代的空间不足、永久代不足时才会触发。这就导致 Stringtable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

**3 静态变量放在哪里 ? **

静态引用对应的对象实体new 的结构始终都存在堆空间

方法区的垃圾回收

一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这
部分区域的回收有时又确实是必要的。

方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型

方法区内常量池之中主要存放的两大类常量:字面量和符号引用。
字面量比较接近]ava语言层次的常量概念,如文本字符串、被声明为final的常量值等。

而符号引用则属于编译原理方面的概念,包括下面三类常量:

1、类和接口的全限定名
2、字段的名称和描述符
3、方法的名称和描述符

Hotspot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。

回收废弃常量与回收Java堆中的对象非常类似。

判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。

需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收,也就是]ava堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

对象的实例化及内存布局与访问定位

对象的实例化

在这里插入图片描述
对象创建方式

  • new关键字
  • 通过反射获取getConstructor()构造方法类的newInstance()创建
  • 通过类的加载机制Class.forName().newInstance()方法创建对象–此方式已经过时
  • 使用类的加载器:ClassLoader
  • 利用对象的拷贝clone()复制对象
  • 反序列化获取新的对象

对象实例化的过程

  1. 加载类元信息
  2. 为对象分配内存
  3. 处理并发问题
  4. 属性的默认初始化(零值初始化)
  5. 设置对象头的信息
  6. 属性的显式初始化、代码块中初始化、构道器中初始化

对象的内存布局

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

对象的访问定位

JVM是如何通过栈帧中的对象引用访问到其内部的对象实例的呢?
在这里插入图片描述
定位,通过栈上 reference访问

对象访问方式:

  • 句柄访问在这里插入图片描述优点:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很遍)时只会改变句柄中实例数据指针即可, reference本身不需要被修改
    缺点:效率较差,需要额外的开辟内存空间
  • 直接指针( Hotspot采用)
    在这里插入图片描述
    优点:效率高
    缺点:对象被移动,reference 需要被修改

直接内存

不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
直接内存是在Java堆外的、直接向系统申请的内存区间。
来源于NIO,通过存在堆中的 DirectByteBuffer操作 Native内存通常,访问直接内存的速度会优于Java堆。即读写性能高。
因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。
Java的NIO库允许Java程序使用直接内存,用于数据缓冲区

传统的内存:读写文件,需要与磁盘交互, 需要由用户态切换到内核态在内核态。

在这里插入图片描述
直接内存:

使用NIO时,操作系统划出的直接缓存区可以被java 代码直接访问,只有 一份。NIO适合对大文件的读写操作。
在这里插入图片描述
由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。 否则会报本地内存的OOM: OutOfMemoryError; Direct buffer memory

直接内存大小可以通过 MaxDirectMemorySize设置 ,如果不指定,默认与堆的最大值-Xmx参数值一致

缺点:

  • 分配回收成本较高
  • 不受JVM内存回收管理

简单理解:
java process memory = java heap+ native memory

执行引擎

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

虚拟机 是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集
和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执
行那些不被硬件直接支持的指令今集格式

JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JⅦM所识别的字节码指令、符号表,以及其他辅助信息那么,如果想要让一个Java程序运行起来,执行引擎( Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。

执行引擎工作过程

在这里插入图片描述

  1. 执行引擎在执行的过程中究竟 需要执行什么样的字节码指令完全依赖于PC寄存器。

  2. 每当执行完一项指令操作后, PC寄存器就会更新下一条需要被执行的指令地址。

  3. 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,
    以及通过对象头中的元数据指针定位到目标对象的类型信息。

  4. 从外观上来看,所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流处理过程是字节码解析执行的等效过程,输出的是执行结果

Java代码编译执行过程

Java代码编译是由Java源码编译器来完成
在这里插入图片描述
Java字节码的执行是由JVM执行引擎来完成
在这里插入图片描述
什么是解释器( Interpreter),什么是JIT编译器?

解释器:

当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行

JIT( Just In Time Compiler)编译器:

就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言

为什么说Java是半编译半解释型语言?

在执行字节码的时候既可以使用解释器也可以使用JIT编译器

Java语言执行过程

在这里插入图片描述

解释器

JVM设计者们的初衷仅仅只是单纯地为了满足Java程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。

解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行
当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。

基于解释器执行已经沦落为低效的代名词, 为了解决这个问题,JVM平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。

不过无论如何,基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献

JIT编译器

Hotspot JVM是目前市面上高性能虚拟机的代表作之一。它采用解释器与即时编译器并存的架构。在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。

既然 HotSpot VM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如JRckitM内部就不包含解释器字节码全部都依靠即时编译器编译后执行

原因:

当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行编译器JIT要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码 需要一定的执行时间。但编译为本地代码后执行效率高

解释器响应速度快,JIT响应速度较慢但翻译为本地代码后执行效率高

当Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率,所以要采用解释器与即时编译器并存的架构来换取一个平衡点

热点代码及探测

当然是否需要启动JI编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。关于那些需要被编译为本地代码的字节码,也被称之为“热点代码”,JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。

一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之 为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR( On stack Replacement)编译。

一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。

目前 Hotspot VM所采用的热点探测方式是基于计数器的热点探测

采用基于计数器的热点探测, Hotspot VM将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器( Invocation Counter)和回边计数器(Back Edge Counter)。

  • 方法调用计数器用于统计方法的调用次数(阈值:client 1500次 server 10000次 可以通过虚拟机参数-XX: CompileThreshld来人为设定 )
    在这里插入图片描述
  • 回边计数器则用于统计循环体执行的循环次数

它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”( Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译

设置编译方式

-Xint:完全采用解释器模式执行程序;
-Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。
-Xmixed:采用解释器+即时编译器的混合模式共同执行程序。

开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:

  • Client:指定Java虚拟机运行在 Client模式下,并使用C1编译器;
    C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度
  • Server:指定Java虛拟机运行在 Server模式下,并使用C2编译器
    C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。

C1和C2编译器不同的优化策略:

在不同的编译器上有不同的优化策略,C1编译器上主要有方法内联,去虚拟化、冗余消除。

方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减 少参数传递以及跳转过程

去虚拟化:对唯一的实现类进行内联

冗余消除:在运行期间把一些不会执行的代码折叠掉

C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2上有如下几种优化 :

标量替换:用标量值代替聚合对象的属性值
栈上分配:对于未逃逸的对象分配对象在栈而不是堆
同步消除:清除同步操作,通常指 Synchronized

总结:

  • 一般来讲,JIT编译出来的机器码性能比解释器高。
  • C2编译器启动时长比C1编译器慢,系统稳定执行以后,C2编译器执行速度远远快于C1编译器。

String

String再也不用char[]来存储啦,改成了byte[]加上编码标记,节约了一些空间

不可变性

String:代表不可变的字符序列。简称:不可变性

  • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
  • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值, 不能使用原有的value进行赋值
  • 当调用 string的 replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的va1ue进行赋值。
  • 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中

字符串常量池中是不会存储相同内容的字符串的

  • String的 String Pool是一个固定大小的 Hashtable,默认值大小长度是1009。如果放进 string Pool的 string非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用 string. intern()时性能会大幅下降
  • 使用-XX: stringTableSize可设置 StringTable的长度
  • 在jdk6中 StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快;StringTablesize设置没有要求
  • 在jdk7中, Stringtable的长度默认值是60013,1009是可设置的最小值
  • jdk8开始,设置 StringTable的长度的话,1009是可设置的最小值

String 的内存分配

常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的, string类型的常量池比较特殊。它的主要使用方法有两种。

  • 直接使用双分归明出来的 string对象会直接存储在常量池中。
  • 不是用双引号声明的 string对象,可以使用 string提供的 intern()方法

字符串的拼接

  1. 常量与常量的拼接结果在常量池,原理是编译期优化
  2. 常量池中不会存在相同内容的常量
  3. 只要其中有一个是变量(final 的变量除外),结果就在堆中。变量拼接的原理是 StringBuilder的append 然后调用toString() 返回的是新的String
  4. 字符串拼接操作都是字符串常量或者常量引用或者final修饰的变量,仍然使用编译器优化,非StringBuilder 方式
  5. 拼接的结果调用 intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。

String.intern()

两个字符串s 和 t 如果s.intern() == t.intern() 成立那么 s.equals(t) 必然成立

如果不是用双引号声明的 string对象,可以使用 string提供的 intern方法: intern 方法会从字符串常量池中査询当前字符串是否存在,若不存在就会将当前字符串放入常池中。

Interned string就是确保守符串在内存里只有一份拷贝这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在符串内部池 (String Intern Pool )

对于程序中大量存在存在的字符串,尤其其中存在很多重复字符时,使用 intern()可以节省内存空间

如何保证变量s指向的是字符常量池中的数据呢?
有两种方式
方式一: string s=" jdk";//字面量定义的方式
方式二: string s= new String(" jdk"). intern();

new String( “a”)+new String( b") 几个对象?
对象1: new StringBuilder()
对象2: new String(“a”)
对象3:常量池中的”a"
对象4:; new String(“b”)
对象5:常量池中的”b”

对象6:StringBuilder toString() 方法的new String;强调一下, tostring()的调用,在字符串常量池中,没有生成”ab"

在这里插入图片描述
jdk6: false;false

jdk7/8 :false;true

s 返回的是堆空间的地址,s2 是常量池的地址 false

s3 记录的地址为new String(“11”) “+”拼接形式的常量池中不存在11

s3.intern() :

jdk 6 中 会把此对象复制一份即创建一个新的对象“11” 所以是新的地址 ,并放入串池,并返回串池中的对象地址

jdk7/8 则会把对象已存在的引用地址复制一份即对空间的new String(“11”) 地址被放入常量池, 并返回常量池中11地址但s3并没有接收即s3=s3.intern(),所以仍然指向堆空间的创建的11的地址, s3 指向堆空间中 new String("11”)的地址 s4 会使用s3 方法生成的11 的地址也是指向new String("11”)
在这里插入图片描述
总结 string的 intern()的使用:

  • jdk1.6中,将这个字符串对象尝试放入串池如果串池中有,则并不会放入。返回已有的串池中的对象的地址
    如果没有,会把此对象复制一份(新的对象),放入串池,并返回串池中的对象地址
  • jdk1.7起,将这个字符串对象尝试放入串池。 如果串池中有,则并不会放入。返回已有的串池中的对象的地址.如果没有,则会把对象已存在的引用地址复制一份,放入串池,并返回串池中的引用地址

String的垃级回收

Xms15m-Xmx15m -XX: +PrintstringTablestatistics -XX: +PrintGCDetails

UseString Deduplication(bool):开启 string去重,默认是不开启的,需要手动开启。
PrintstringDeduplicationStatistics(bool):打印详细的去重统计信息
StringDeduplicationAgeThreshold( uinta):达到这个年龄的 string对象被认为是去重的候选对象

垃圾回收

概述

1 什么是垃圾( Garbage)呢?

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空 间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。

2 为什么需要GC?

对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完因为不断地分配内存空间而不进行回收,除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。

Java 内存自动管理

自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
没有垃圾回收器,java也会和c++一样,各种悬垂指针,野指针,泄露问题
自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发

垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和放法区的回收。
其中,Java堆是垃圾收集器的工作重点。
从次数上讲:
频繁收集 Young区
较少收集old区
基本不动方法区(元空间)

对象的finalization机制

Java语言提供了对象终止(finalzation)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一·个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等

永远不要主动调用某个对象的finalize()方法 应该交给垃圾回收机制调用

  • 在finalize()时可能会导致对象复活。

  • finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下, 若不发生GC,则 finalize()方法将没有执行机会。

    一个糟糕的finalize()会严重影响GC的性能。

  • 从功能上来说, finalize()方法与c++中的析构函数比较相似,但是Java采用的是基
    于垃圾回收器的自动内存管理机制,所以finalize()方法在本质上不同于c++中的函数。

  • 由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。

如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说, 此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那 么对它的回收就是不合理的,为此,

定义虚拟机中的对象可能的三种状态。如下:

  • 可触及的:从根节点开始,可以到达这个对象。
  • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
  • 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触 及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。

以上3种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。

定一个对象objA是否可回收,至少要经历两次标记过程:

  1. 如果对象objA到 GC Roots没有引用链,则进行第一次标记
  2. 进行筛选,判断此对象是否有必要执行 finalize()方法
    1. 如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,
      则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
    2. 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue
      队列中,由一个虚拟机自动创建的、低优先级的 finalizer线程触发其finalize()方
      法执行。
    3. finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F- Queue队列中的对象进行
      第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,
      那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引
      用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触
      及的状态,也就是说,一个对象的finalize方法只会被调用一次。

垃圾回收概念

System.gc()

在默认情况下,通过 System.gc()或者 Runtime. getRuntime().gc()的调用,会显式触发FullGC,同时对老年代和新生代进行回收,尝试释放
被丢弃对象占用的内存。

System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(不一定马上执行,无法确保执行时间)。

仅仅提醒JVM 需要执行一次垃圾回收,但不一定会执行

JVM实现者可以通过 System.gc()调用来决定JVM的GC行为。

一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用 System.gc()。

System. runFinalization() 调用后强制调用使用引用的对象的 finalize()方法

内存溢出及内存泄漏

内存溢出

JavaDoc中对 OutOfMemoryError的解释是,没有空闲内存,并且垃圾集器也无法提供更多内存

没有空闲内存的情况:说明ava虚拟机的堆内存不够 :

  1. Java虚拟机的堆内存设置不够。
    比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可
    观的数据量,但是没有显式指定JM堆大小或者指定数值偏小。我们可以通过参数-Xms、
    -Xmx来调整。
  2. 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)

在抛出OutOfMemoryError之前,通常垃圾收集器一定会被触发,尽其所能去清理出空间
当然,也不是在任何情况下垃圾收集器都会被触发的 比如,我们去分配一个超大对象类似一个超大数组超过堆的最大值,JVM可以判
断出垃圾收集并不能解决这个问题,所以直接抛出 OutOfMemoryError

内存泄漏Memory Leal

只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏

但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”

注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。

在这里插入图片描述
举例:

  1. 单例模式
    单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
  2. 一些提供close的资源未关闭导致内存泄漏
    数据库连接( dataSource. getconnection()),网络连接(socket)和 io连接必须手动close,否则是不能被回收的。
  3. ThreadLocal 的value内存泄漏

Stop The World

stop-the-world,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,
有点像卡死的感觉,这个停顿称为STW。

可达性分析算法中枚举根节点( GC Roots)会导致所有Java执行线程停顿 .分析工作必须在一个能确保一致性的快照中进行
一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性
无法保证

被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。

STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉
开发中不要用 System.gc() 会导致 Stop The World的发生

垃圾回收的并行与并发

并发

是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行
并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。
在这里插入图片描述

并行

当系统有一个以上CPU执行一个进程时,另一个CPU可以执行另一个进程 两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(Parallel) 其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。
在这里插入图片描述
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。

并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的。

只有在多CPU或者一个CPU多核的情况中,才会发生并行。 否则,看似同时发生的事情,其实都是并发执行的

垃圾回收的并发与并行

并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态

如 ParNew、Parallel Scavenge、Parallel old;

串行( Serial) 相较于并行的概念,单线程执行

如果内存不够,则程序暂停,启动J垃圾回收器进行垃圾回收。回收完,再启动

并发( Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。 用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上; CMS G1

安全点与安全区域

安全点

程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点( Savepoint)”

Safe point的选择很重要**,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题**。大部分指令的执行时间都非常短暂, 通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择些执行时间较长的指令作为 Safe point,如方法调用、循环跳转和异常跳转等。

如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?

  • 抢先式中断:(目前没有虚拟机采用了)
    首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
  • 主动式中断:
    设置一个中断标志,各个线程运行到 Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。

安全区域

Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC 的 Savepoint。但是,程序“不执行”的时候呢?例如线程处于Sleep状
态或Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全 区域( Safe Region)来解决。

安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把 Safe Region看做是被扩展了的 Safepoint。

实际执行时:

  1. 当线程运行到 Safe Region的代码时,首先标识已经进入了 Safe Region如果这段时间内发生GC,JVM会忽略标识为 Safe Region状态的线程;
  2. 当线程即将离开 Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开 Safe Region的信号为止

垃圾回收的算法

垃圾标记算法

对象标记:对象存活判断

在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。

那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。

判断对象存活一般有两种方式:引用计数算法和可达性分析算法

引用计数算法

引用计数算法( Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1:当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

优点: 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:

  • 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
  • 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
  • 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷导致在Java的垃圾回收器中没有使用这类算法

可达性分析算法

相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。

相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集( Tracing Garbage Collection)

基本思路:

  • 可达性分析算法是以根对象集合( GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链( Reference chain)
  • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
  • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象

所谓" GC Roots"根集合就是一组必须活跃的引用。

垃圾清除算法

当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对 象分配内存。
目前在JVM中比较常见的三种垃圾收集算法是标记一清除算法(Mark swep)、复制算法( Copying)、标记-压缩算法(Mark-Compact )

标记清除(Mark- Sweep)算法

标记-清除算是一种非常基础和常见的垃圾收集算算法

执行过程:
当堆中的有效内存空间( available memory)被耗尽的时候,就会停止整个程序(也被称为 stop the world),然后进行两项工作,

第一项则是标记,
第二项则是清除。

标记: Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的 Header中记录为可达对象。 注意:标记的是可达对象(非垃圾对象)

清除: Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header中没有标记为可达对象,则将其回收。
在这里插入图片描述
缺点

  • 效率不算高
  • 在进行GC的时候,需要停止整个应用程序,导致用户体验差
  • 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表

注意:何为清除?

这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够如果够,就存放。

复制(Copying) 算法

核心思想

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
在这里插入图片描述
优点:

  1. 没有标记和清除过程,实现简单,运行高效
  2. 复制过去以后保证空间的连续性,不会出现“碎片”问题。

缺点:

此算法的缺点也是很明显的,就是需要两倍的内存空间
对于G1这种分拆成为大量 region的GC,复制而不是移动,意味着GC需要维护 分区之间对象引用关系,不管是内存占用或者时间开销也不小。

**如果系统中的垃圾对象很多,复制算法不会很理想。因为复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。 **。

应用场景:
在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。

标记压缩(Mark- Compact)算法

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使其他的算法。

执行过程:

  1. 第一阶段和标记清除算法一样从根节点开始标记所有被引用对象
  2. 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
    在这里插入图片描述
    标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark- Sweep- Compact)算法

二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。

是否移动回收后的存活对象是一项优缺点并存的风险决策
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时
JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

优点:

  • 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,J只需要持有一个内存的起始地址即可。
  • 消除了复制算法当中,内存减半的高额代价。

缺点:

  • 从效率上来说,标记-整理算法要低于复制算法
  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
  • 移动过程中,需要全程暂停用户应用程序。即:STW

小结对比

在这里插入图片描述
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的 阶段,比标记-清除多了一个整理内存的阶段

复合优化算法

分代收集算法

分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

目前几乎所有的GC都是采用分代收集( Generational Collecting)算法执行垃圾回收的。
在 Hotspot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点

年轻代( Young Gen)

  • 年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
  • 这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过 hotspot中的两个 survivor的设计得到缓解。

老年代( Tenured Gen)

  • 老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
  • 这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记整理的混合实现。
    • Mark阶段的开销与存活对象的数量成正比。
    • sweep阶段的开销与所管理区域的大小成正相关。
    • Compact阶段的开销与存活对象的数据成正比。

增量收集算法

上述现有的算法,在垃圾回收过程中,应用软件将处于一种 Stop the World 的状态。在 stop the world狀态下,应用程序所有的线程都会挂起,暂停正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集( Incremental Collecting)算法的诞生

基本思想

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

总的来说,增量收集算法的基础仍是传统的标记-淸除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作

缺点

使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,使得垃圾回收的总体成本上升,造成系统吞吐量的下降

分区算法

一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC 的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合貍地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间region。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

Java 引用类型

在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用( strong Reference)、软引用( Soft reference)、弱引用( Weak Reference)和虚引用 (Phantom reference)4种,这4种引用强度依次逐渐减弱

  • 强引用( Strong Reference):最传统的“引用”的定义,是指在程序代码之中普遍 存在的引用赋值,即类似“ Object obj= new Object()”这种引用关系。无论任何情下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用( Soft Reference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常
  • 弱引用( Weak Reference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象
  • 虚引用( Phantom Reference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

强引用-不回收

强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象

相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下,都是可以被回收的。所以,强引用是造成Java内存泄漏的主要原因之一

软引用-内存不足即回收

软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地艳引用存放到一个引用队列( Reference Queue)。

JDK1.2版之后提供了java.lang.ref.SoftReference类来实现软引用。

弱引用-GC发现即回收

弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是 否充足,都会回收掉只被弱引用关联的对象。

但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。

弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引 对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。

软引用、弱引用都非常适合来保存那些可有可无的缓存数据

弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。

虚引用-对象回收跟踪

也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个。
一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收

它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。

虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知

因此,也可以将一些资源释放操作放置在虚引用中执行和记录

在JDK1.2版之后提供了 PhantomReference类来实现虚引用。

垃圾回收器

GC分类与性能指标

垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。

由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。

从不同角度分析垃圾收集器,可以将GC分为不同的类型。

评估GC的性能

  • 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)
  • 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
  • 收集频率:相对于应用程序的执行,收集操作发生的频率。
  • 内存占用:Java堆区所占的内存大小。
  • 快速:一个对象从诞生到被回收所经历的时间。

简单来说,主要抓住两点:
1 吞吐量

  • 吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
    • 比如:虚拟机总共运行了188分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
  • 这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。

2 暂停时间

  • “暂停时间”是指一个时间段内应用程序线程暂停,让GC线程执行的状态
    • 例如,GC期间1θθ毫秒的暂停时间意味着在这188毫秒期间内没有应用程序线程是活动的。

在设计(或使用)GC算法时我们必须确定我们的目标:一个GC算法只可能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或尝试找到一个二者的折衷。

现在标准:在最大吞吐量优先的情况下,降低停顿时间

不同的垃圾回收器概述

串行回收器: Serial、 Serialold
并行回收器: ParNew、Parallel Scavenge、Parallel0ld
并发回收器:CMS、G1

垃圾回收器与垃圾分代之间的关系

在这里插入图片描述
在这里插入图片描述
1.两个收集器间有连线,表明它们可以搭配使用:
Serial/ Serialold、 Serial/CMS、 ParNew/ Serialold、 ParNew/CMS、Parallel Scavenge/Serial old, Parallel Scavenge/Parallel old, G1

2.其中 serialold作为CMS出现" Concurrent Mode Failure"失败的后备预案。

3.(红色虚线)由于维护和兼容性测试的成本,在JDK8时将 Serial+CMS、ParNew+ Serialold这两个组合声明为废弃(EP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。

4.(绿色虚线) JDK14中:弃用Parallel Scavenge和 Serial0ldGC组合(JEP 366)

5.(青色虚线) JDK14中:删除CMS垃圾回收器(JEP363)

查看默认的垃圾回收器

  • -XX:+ PrintcommandlineFlags: 查看命令行相关参数(包含使用的垃圾收集器)
  • 使用命令行指令: jinfo -flag 相关垃圾回收器参数 进程ID

Serial回收器 串行回收

  • Serial收集器作为 HotSpot中client模式下的默认新生代垃圾收集器。

  • Serial收集器采用复制算法、串行回收和〃stop-the-World"机制的
    方式执行内存回收。

  • 除了年轻代之外, Serial收集器还提供用于执行老年代垃圾收集的
    Serialold收集器。 serialold收集器同样也采用了串行回收

    和〃Stop the World"机制,只不过内存回收算法使用的是标记-压缩算法

  • Serialold是运行在 Client模式下默认的老年代的垃圾回收器

    • Serialold在 Server模式下主要有两个用途:①与新生代的Parallel Scavenge配合使用
    • ②作为老年代CMS收集器的后备垃圾收集方案

这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU 或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束( Stop The World)。

优势:

简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说, Seria收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

  • 运行在 Client模式下的虚拟机是个不错的选择。

在 Hotspot虚拟机中,使用-XX:+ UseSerialgc参数可以指定年轻代和老年代都使用串行收集器

ParNew回收器 并行回收

ParNew收集器则是 Seral收集器的多线程版本 只能处理的是新生代

ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。

ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the- World"机制。
ParNew是很多JVM运行在 Server模式下新生代的默认垃圾收集器。

对于新生代,回收次数频繁,使用并行方式高效。
对于老年代,闻收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)

ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。

但是在单个CPU的环境下, ParNeW收集器不比 Serial收集器更高效。虽然 Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销

在程序中,开发人员可以通过选项"-XX:+ UseParNewGC"手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
-XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数

Parallel Scavenge 回收器 吞吐量优先

Hotspot的年轻代中除了拥有 ParDew收集器是基于并行回收的以外, Parallel Scavenge收集器同样也采用了复制算法、并行回收和"stop the World"机制。

那么Parallel收集器的出现是否多此一举?
和 ParNell收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量( Throughput),它也被称为吞吐量优先的垃圾收集器。

自适应调节策略也是Parallel Scavenge与 ParNew一个重要区别。

  • 高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
  • Parallel 收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel old收集器,用来代替老年代的 Serialold收集器。
  • Parallel old收集器采用了标记-压缩算法,但同样也是基于并行回收和stop-the-Word"机制。

参数配置:

-XX: +UseParallelGC手动指定年轻代使用parallel并行收集器执行内存回收任务。
-XX+ UseParalleloldGC手动指定老年代都是使用并行回收收集器。分别适用于新生代和老年代。默认jdk8是开启的。 上面两个参数,默认开启一个,另一个也会被开启。(互相激活)

-XX:ParallelGcThreads设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能

  • 在默认情况下,当CPU数量小于8个,ParallelGCThreads的值等于CPU数量。
  • 当CPU数量大于8个,ParallelGCThreads的值等于 3+[5*CPU_Count]/8]。

-XX: MaxGCPauseMillis设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。

为了尽可能地把停顿时间控制在 MaxGcpauseMllls以内,收集器在工作时会调整Java堆大小或者其他一些参数。
对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。
该参数使用需谨慎。
-XX: GCTimeRatlo垃圾收集时间占总时间的比例(=1/(N+1)) 用于衡量吞吐量的大小。

取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1号。
与前一个-XX: MaxGcPauseMillis参数有一定矛盾性。暂停时间越长, Radio参数就容易超过设定的比例

-XX:+ UseAdaptiveSizePolicy设置Parallel Scavenge收集器 具有自适应调节策略
在这种模式下,年轻代的大小、Eden和 Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿 时间之间的平衡点。

在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间 (MaxGCPauseMills ) 让虚拟机自己完成调优工作

CMS 低延迟

CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时 间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。

CMS的垃圾收集算法采用标记-清除算法,并且也会"Stop-the- world"

工作原理

在这里插入图片描述
CMS整个过程比之前的收集器要复杂,整个过程分为4个要阶段,即初始标记阶段、并发
标记阶段、重新标记阶段和并发清除阶段。

  • 初始标记( Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop- the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出 GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。

  • 并发标记( Concurrent-Mark)阶段:从 GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起

  • 重新标记( Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。

  • 并发清除( Concurrent- Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

CMS收集器的垃圾收集算法采用的是标记-清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么CMS在为新对象分 配内存空间时,将无法使用指针碰撞( Bump the Pointer)技术,而只能够选择空闲列表( Free list)执行内存分配。

既然 Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact呢?

因为当并发清除的时候,用 Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提 的它运行的资源不受影响Mark Compact更适合“ Stop the World” 这种场景下使用

CMS的优点

  • 并发收集
  • 低延迟
  • 由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。

CMS的弊端:

  • 会产生内存碎片

  • CMS收集器对CPU资源非常敏想感

  • CMS收集器无法处理浮动垃圾

    在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收

参数设置

-XX:+ Use ConcMarkSweepGC手动指定使用CMS收集器执行内存回收任务

-XX: CMSlnitiatingoccupanyFraction设置堆内存使用率的阈值一旦达到该阈值,便开始进行回收

-XX:+ UseCMSCompactAtFullCollection用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
-XX CMSFullGCsBeforeCompaction设置在执行多少次FullGC后对 内存空间进行压缩整理。
-XX:ParallelCMSThreads设置CMS的线程数量。

如果你想要最小化地使用内存和并行开销,请选 Serial GC;
如果你想要最大化应用程序的吞吐量,请选ParallelGC
如果你想要最小化GC的中断或停顿时间,请选 CMS GC。

G1-区域化分代式

既可以适用于新生代也可以适用于老年代

因为G1是一个并行回收器,它把堆内存分割为很多不相关的区域( Region)(物理上不连续的)。使用不同的 Region来表示Eden、幸存者0区,幸存者1区,老年代等。

G1GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region

G1( Garbage- First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征

特点

G1使用了全新的分区(Region)算法

  • 兼具并行与并发

    • 并行性:G1在回收期,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
    • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行, 因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
  • 分代收集

    • 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和 Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代 或者老年代都是连续的,也不再坚持固定大小和固定数量。
    • 将堆空间分为若干个区域( Region),这些区域中包含了逻辑上的年轻代和老年,所有的 Region大小相同,且在JVM生命周期内不会被改变
    • 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代
  • 空间整合

    • CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理
    • G1将内存划分为一个个的 region。内存的回收是以 region作为基本单位的。
      Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark- Compact 算法,两种算法都可以避免内存碎片。有利于程序长时间运行
  • 可预测的停顿时间模型

    G1除了追求低停顿外,还能建立可预测的停顿 时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

  1. 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范 围,因此对于全局停顿情况的发生也能得到较好的控制。
  2. G1跟踪各个 Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
  3. 相比于CMS GC, G1未必能做到CMs在最好情况下的延时停顿,但是最差情况要好很多

缺点

相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中G1无论是为了垃圾收集产生的内存占用( Footprint)还是程序运行时的额外执行负载都要比CMS要高

参数设置

  • -XX:+UseG1GC手动指定使用G1收集器执行内存回收任务。
  • -XX:G1HeapRegionsize设置每个 Region的大小。值是2的幂,范围是1MB 到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000
  • -XX: MaxGcpauseMills设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
  • -XX:ParallelGCThread设置STW工作线程数的值。最多设置为8
  • -XX: ConcGCThreads设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
  • -XX: InitiatingHeapOccupancyPercent设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。

G1的设计原则就是简化JWM性能调优,开发人员只需要简单的三步即可完成调优:
第一步:开启G1垃圾收集器
第二步:设置堆的最大内存
第三步:设置最大的停顿时间
G1中提供了三种垃圾回收模式: YoungGC、 Mixed Go和FullGC,在不同 的条件下被触发

使用场景

  • 面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)

  • 最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;

  • 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于8.5秒;(G1通过每次只清理一部分而不是全部的 Region的增量式清理来保证每次GC停顿时间不会过长)。

  • 用来替换掉JDK1.5中的CMS收集器
    在下面的情况时,使用G1可能比CMS好:
    ①超过58%的Java堆被活动数据占用;
    ②对象分配频率或年代提升频率变化很大
    ③GC停顿时间过长(长于8.5至1秒)

回收环节

G1 GC的垃圾回收过程主要包括如下三个环节:

  • 年轻代GC( Young GC)
  • 老年代并发标记过程( Concurrent Marking)
  • 混合回收( Mixed GC)
  • (如果需要,单线程、独占式、高强度的FullGC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。)
    在这里插入图片描述

回收过程

Remembered Set 记忆集

一个对象被不同区域引用的问题
一个 Region不可能是孤立的,一个 Region中的对象可能被其他任意 Region中对象引用, 判断对象存活时,是否需要扫描整个Java堆才能保证准确?
在其他的分代收集器,也存在这样的问题(而G1更突出)
回收新生代也不得不同时扫描老年代,这样的话会降低 Minor GC的效率;

解决方法:

  • 无论G1还是其他分代收集器,JVW都是使用 Remembered Set来避免全局扫描:
  • 每个 Region都有一个对应的 Remembered Set
  • 每次 Reference类型数据写操作时,都会产生一个 Write barrier暂时中断操作
  • 然后检查将要写入的引用指向的对象是否和该 Reference类型数据在不同的 Region(其他收集器:检查老年代对象是否引用了新生代对象)
  • 如果不同,通过 CardTable把相关引用信息记录到引用指向对象的所在 Region对应的Remembered set中;
  • 当进行垃圾收集时,在GC根节点的枚举范围加入 Remembered set;就可以保证不进行全局扫描,也不会有遗漏

在这里插入图片描述

年轻代GC

JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程。

年轻代垃圾回收只会回收Eden区和 Survivor区。

YGC时,首先G1停止应用程序的执行(Stop-The- World),G1创建回收集(collection set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和 Survivor区所有的内存分段。

第一阶段,扫描根

根是指 static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同Rset 记录的外部引用作为扫描存活对象的入口。

第二阶段,更新Rset(记忆集)

处理 dirty card queue中的card,更新Rset。此阶段完成后,Rset可以准确的反映老年代对所在的内存分段中对象的引用

第三阶段,处理RSet

识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象

第四阶段,复制对象

此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到 Survivor区中空的内存分段Survivor区內存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到0ld区中空的内存分段。如果 Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。

第五阶段,处理引用
处理Soft,Weak, Phantom,Final, JNI Weak等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。

并发标记过程

  1. 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。
  2. 根区域扫描( Root Region Scanning):G1 GC扫描 Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在 Young GC之前完成。
  3. 并发标记( Concurrent Marking):在整个堆中进行并发标记(和应用程序并发执行.此过程可能被 Young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾, 那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
  4. 再次标记( Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的初始快照算法: snapshot-at- the-beginning(STAB)
  5. 独占清理(cleanup,STW):计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。
  6. 并发清理阶段:识别并清理完全空闲的区域。

混合回收

当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,
虚拟机会触发一个混合的垃圾收集器, 即 Mixed GC,该算法并不是一个old GC,除了回收整个 Young Region,
还会回收一部分的old Region。这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是 Mixed GC并不是FullGC。

可选的过程四: Full GC

G1的初衷就是要避免FullGc的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。

要避免FullGC的发生,一旦发生需要进行调整。什么时候会发生FullGC
呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用, 则会回退到full gc,这种情况可以通过增大内存解决。 导致G1 Full GC的原因可能有两个

  1. Evacuation的时候没有足够的to- space来存放晋升的对象
  2. 并发处理讨程完成之前空间耗尽

G1回收器优化建议

  • 年轻代大小
    • 避免使用-Xmn或-XX: NewRatio等相关选项显式设置年轻代大小
    • 固定年轻代的大小会覆盖暂停时间目标
  • 暂停时间目标不要太过严苛
    • G1 GC的吞吐量目标是99%的应用程序时间和18%的垃圾回收时间
    • 评估G1GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量。

垃圾回收器总结

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

选择垃圾回收器

  • 优先调整堆的大小让JVM自适应完成。
  • 如果内存小于180M,使用串行收集器
  • 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
  • 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
  • 如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器
    官方推荐G1,性能高。现在互联网的项目,基本都是使用G1

GC日志分析

内存分配与垃圾回收的参数列表
-XX:+PrintGC 输出GC日志。类似:- verbose:gc
-XX:+ PrintGcDetalls 输出GC的详细日志
-XX:+ PrintGcTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+ PrintGCDateStamps 输出GC的时间戳(以日期的形式,如2013-05 04T21:53:59.234+0800)
-XX: +PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc: …/logs/gc.log 日志文件的输出路径

可以用一些工具去分析这些gc日志。
常用的日志分析工具有: GCViewer、 GCEasy、 GCHisto、 GCLogVlewer、
Hpjmeter、 garbagecat等。


版权声明:本文为weixin_49561445原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。