JVM系列之运行时数据区

JVM系列之运行时数据区,第1张

1、运行时数据区

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域虚拟机栈、堆内存、程序计数器、方法区、本地方法栈

  • 虚拟机栈、本地方法栈、程序计数器为线程私有
  • 方法区、堆内存为线程共享
1.1、程序计数器(Program Counter Register)

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

程序计数器:用于存储下一条即将要执行的字节码指令地址,由执行引擎读取下一条指令

  • JVM中运行速度最快的一块内存区域
  • JVM的PC寄存器是对物理PC寄存器的一种抽象模拟
  • 运行时数据区中唯一不会出现OOM的区域没有垃圾回收
  • 程序计数器为线程私有,每个线程都有自己的程序计数器。每个线程有一个独立的程序计数器,线程之间互不影响
  • 记录对应线程所执行的字节码的行号指示器,为了保证线程切换后能恢复到正确的执行位置
  • 如果线程执行的Java方法,则计数器记录正在执行的虚拟机字节码的指令的地址,如果正在执行的本地方法(native修饰),这个计数器值则应为空(undefined)
1.1.1、使用PC寄存器存储字节码指令地址有什么用呢?

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

1.1.2、PC寄存器为什么会设置线程私有的?
  • 我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?
    为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
1.2、虚拟机栈(重点)

内存中的栈与堆区别?
栈是运行时的单位,而堆是存储的单位,栈解决程序如何执行,如何处理数据。堆解决的是数据存储问题,即数据怎么放,放在哪里。

1.2.1、栈基本介绍
  • Java虚拟机栈,早期也叫Java栈,每个线程创建时都会创建一个虚拟机栈,内部保存一个个栈帧,对应着一次次的Java方法调用
  • 生命周期和线程的一致,线程被销毁,虚拟机栈也就随之销毁
  • 主管Java程序的运行(主要是方法的执行),保存方法的局部变量(8种基本数据类型、对象的引用地址((类,数组,接口)))、部分中间结果,并参与方法的调用和返回
1.2.2、栈的特点
  • 快速有效的存储方式,访问速度仅次于程序计数器
  • JVM直接对Java栈的 *** 作只有两个,方法被调用,伴随着进栈(入栈、压栈),执行结束的出栈(d栈)
  • 栈不存在垃圾回收,但是存在OOM和StackOverflowError,因为Java栈大小是动态或者固定不变的。如果是动态扩展,无法申请到足够内存OOM,如果是固定,线程请求的栈容量超过固定值,则StackOverflowError
  • 使用-Xss的JVM参数,设置线程的最大栈空间。
1.2.3、栈的存储单位(栈帧)
  • 每个线程都有自己的栈,栈中的数据以栈帧格式存储

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

  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各个数据信息

  • 一条活动的线程中,一个时间点上,只会有一个活动的栈帧。只有当前正在执行的方法的栈顶栈帧是有效的,这个称为当前栈帧,对应方法是当前方法,对应类是当前类

  • 执行引擎运行的所有字节码指令只针对当前栈帧进行 *** 作

  • 先进后出,后进先出,如果方法中调用了其他方法,对应的新的栈帧会被创建出来,放在顶端,成为新的当前帧

1.2.4、栈运行原理
  • 不同线程中包含的栈帧不允许存在相互引用
  • 当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为新的栈帧。
  • Java方法有两种返回方式,一种是正常的函数返回,使用return指令,另外一种是抛出异常,不管哪种方式,都会导致栈帧被d出
1.2.5、栈的内部结构

栈的内部结构(局部变量表、 *** 作数栈、方法返回地址、动态链接、附加信息)

1.2.5.1、局部变量表(Local Variable Table)
  • 定义为一个字节数组,主要用于存储方法参数,定义在方法体内部的局部变量,数据类型包括各类基本数据类型、对象引用以及return address类型
  • 局部变量表建立在线程的栈上,是线程私有的,因此不存在数据安全问题
  • 局部变量表容量大小是在编译期确定下来的
  • 局部变量表存放编译期可知的各种基本数据类型(8种)、引用类型(reference)、return address类型,boolean、byte、short、char、int、float、引用数据类型都是4个字节,32位。long、double是8个字节,64位。
  • 最基本的存储单元是Slot(槽位),32位占用一个Slot,64位类型(long和double)占用两个slot,为什么设计成32和64呢?因为 *** 作系统一般就是32位和64位
    1. 关于Slot的理解,JVM虚拟机会为局部变量表中的每个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定位置的局部变量值

    2. 如果当前栈帧是由构造方法或者实例方法创建的,那么该对象的引用this变量,会存放在index为0的Slot处,其余的局部变量按照顺序继续排列,如果方法有形参,会将形参放置到局部变量表对应的槽位中。

    3. 局部变量表中的槽位是可以复用的,如果一个局部变量过了其作用域,那么其作用域之后申明的新的局部变量就有可能会复用过期局部变量的槽位,从而达到节省资源的目的

  • 局部变量表中的变量只有在当前方法调用中有效,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程
1.2.5.2、静态变量与局部变量对比及小结

变量的分类:

  • 按照数据类型分基本数据类型引用数据类型
  • 按照声明的位置分为成员变量(在使用前经历过初始化过程)和局部变量(在使用前,必须进显式赋值,否则编译不通过,直接报错)
    1. 成员变量:主要包括类变量和实例变量
      • 类变量:是在链接的准备阶段给类变量进行零值初始化,如果是final修饰且值为字面量,准备阶段进行显式赋值(不需要执行代码),否则初始化阶段显示赋值,即调用类的()方法进行赋值
      • 实例变量:随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值。会在对象的()方法中进行显式赋值
    2. 局部变量:在使用前,必须进显式赋值,否则编译不通过,直接报错

在栈帧中,与性能调优关系最密切的部分,就是局部变量表。在方法执行的过程中,虚拟机使用局部变量表完成方法参数的传递,局部变量表中的引用变量也是重要的垃圾回收根节点(GC Roots),只要被局部变量表中直接或间接引用的对象都不会被回收

1.2.5.3、 *** 作数栈 1.2.5.3.1、为什么需要 *** 作数栈呢?
  • 因为Java虚拟机的解释引擎是基于栈的执行引擎,其中栈就是 *** 作数栈,在方法执行的过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈、出栈
  • 如果被调用方法带有返回值的话,其返回值将会被压入当前栈帧的 *** 作数栈中,并更新程序计数器中下一条需要执行的字节码指令
  • 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
  • 当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的 *** 作数栈是空的
  • 每一个 *** 作数栈会拥有一个明确的栈深度,用于存储数值,最大深度在编译期就定义好
  • 栈中32位类型占用一个栈单位深度,64位类型占用两个栈单位深度
  • *** 作数栈并非采用访问索引方式进行数据访问,而是只能通过标准的入栈、出栈 *** 作完成一次数据访问
1.2.5.3.2、代码举例,演示字节码执行过程中如何使用局部变量表和 *** 作数栈





1.2.5.3.3、栈顶缓存技术

由于 *** 作数是存储在内存中,频繁地进行内存读写 *** 作影响执行速度,将栈顶元素全部缓存到物理CPU的寄存器中,依此降低对内存的读写次数,提升执行引擎的执行效率

1.2.5.4、动态链接

  • 指向常量池的方法引用
    1. 每一个栈帧内部都包含一个指向运行时常量池中,该帧所属方法的引用。目的是为了支持当前方法的代码能够实现动态链接,比如invokedynamic指令
    2. 在.java源文件被编译成.class字节码文件中时,所有的变量、方法引用都作为符号引用,保存在class文件的常量池中
    3. 描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的
    4. 动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
  • 常量池、运行时常量池
    1. 常量池在字节码文件中
    2. 运行时常量池,在运行时的方法区中
1.2.5.5、方法返回地址

方法返回地址是指存放调用该方法的pc寄存器的值
例如:A方法调用B方法,B方法结束后,需要返回A方法继续执行A方法的代码,那么如何知道呢下一步该执行A方法的哪个字节码指令呢?用B方法的方法返回地址来记录A方法的PC寄存器的值,即A方法的指令的下一条指令的地址。

本质上方法的退出就是当前栈帧出栈的过程。此时需要恢复上层方法的局部变量表、 *** 作数栈,将返回值压入调用者栈帧的 *** 作数栈,设置PC寄存器值等,让调用者方法继续执行下去

  • 方法的结束
    1. 正常执行完成(有返回值和无返回值),执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口,
      • 返回指令:
      • ireturn:返回值是boolean、byte、char、short和int类型时使用
      • lreturn:返回值是long
      • dreturn:返回值是double
      • areturn:返回值是引用数据类型
      • 还有一个return指供声明为void的方法、实例初始化方法、类和接口的初始化方法使用
    2. 出现未处理异常,非正常退出,异常退出的,返回地址是通过异常表来确定,栈帧中一般不会保存这部分信息

正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值

1.2.5.6、一些附加信息

允许携带与Java虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息。不确定有,可选情况

1.2.5.7、方法的调用 1.2.5.7.1、静态链接

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

1.2.5.7.2、动态链接

如果被调用的方法无法再编译期被确定下来,只能在运行期将调用的方法的符号引用转为直接引用,这种引用转换过程具备动态性,因此被称为动态链接,例如子类重写父类方法

1.2.5.7.3、方法的绑定

绑定是一个字段、方法、或者类,在符号引用被替换为直接引用的过程。仅仅发生一次

  • 早期绑定:被调用的目标方法如果再编译期可知,且运行期保持不变
  • 晚期绑定:被调用的方法在编译期无法被确定,只能够在程序运行期根据实际的类型绑定相关的方法。
1.2.5.7.4、虚方法和非虚方法
  • 非虚方法:如果方法在编译期就确定了具体的调用版本,则这个版本在运行时是不可变的。这样的方法称为非虚方法,静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
  • 其他方法称为虚方法

Java中任何一个普通方法都具备虚函数的特征(运行期确认,具备晚期绑定的特点),C++中则使用关键字virtual来显式定义,如果在Java程序中,不希望某个方法拥有虚函数的特征,则可以使用关键字final来标记这个方法,禁止子类重写该方法。

1.2.5.7.5、方法调用指令
  • 普通调用指令
    1. invokestatic:调用静态方法,解析阶段确定唯一方法版本
    2. invokespecial:调用()方法,私有及父类方法,解析阶段确定唯一方法版本
    3. invokevirtual:调用所有虚方法
    4. invokeinterface:调用接口方法
    5. 其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法
  • 动态调用指令JDK1.7新增
    1. invokedynamic:动态解析出需要调用的方法,然后执行
    2. 直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式
  • 静态语言和动态语言:区别在于对类型的检查是编译器还是运行期,满足编译期就是静态类型语言,反之就是动态类型语言,Java是静态类型语言,动态调用指令增加了动态语言的特性
1.2.5.7.6、方法重写的本质
  • 找到 *** 作数栈顶的第一个元素所执行的对象的实际类型,记做C
  • 如果在类型C中找到与常量池中描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,如果不通过,则返回java.lang.IllegalAccessError异常
  • 否则,按照继承关系从下往上依次对C的各个父类进行上一步的搜索和验证过程
  • 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
1.2.5.7.7、虚方法表
  • 面向对象的编程中,会很频繁的使用动态分配,如果每次动态分配的过程都要重新在类的方法元数据中搜索合适的目标的话,就可能影响到执行效率,因此为了提高性能,JVM采用在类的方法区建立一个虚方法表,使用索引表来代替查找
  • 每个类都有一个虚方法表,表中存放着各个方法的实际入口
  • 虚方法表会在类加载的链接阶段被创建,并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法也初始化完毕
1.3、本地方法接口

1.3.1、什么是本地方法?

简单讲,就是一个Java调用非Java代码的接口

1.3.2、为什么使用native method?

与Java环境外交互,例如与 *** 作系统底层或硬件交换信息时的情况,启动一个线程

1.4、本地方法栈

  • Java虚拟机栈管理Java方法的调用,而本地方法栈用于管理本地方法的调用
  • 本地方法栈,也是线程私有的
  • 允许被实现成固定或者是可动态扩展的内存大小,内存溢出情况和Java虚拟机栈相同
  • 使用C语言实现
1.4.1、本地方法栈的执行流程

具体做法是Native Method Stack中登记native方法,在执行引擎执行时加载到本地方法库,当某个线程调用一个本地方法时,就会进入一个全新,不受虚拟机限制的世界,它和虚拟机拥有同样的权限,并不是所有的JVM都支持本地方法,因为Java虚拟机规范并没有明确要求本地方法栈的使用语言,具体实现方式,数据结构等,HotSpot JVM中,直接将本地方法栈和虚拟机栈合二为一

1.5、堆(重点) 1.5.1、堆的核心概述

  • 一个JVM实例只存在一个堆内存,堆内存也是Java内存管理的核心区域,Java堆区在JVM启动的时候即被创建。堆内存的大小是可调节的,可以通过启动参数,或者自适应调节
  • Java虚拟机规范规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
  • 所有的线程共享Java堆内存,在这里还可以划分线程私有的缓冲区(TLAB)
  • "几乎"所有的对象实例都在这里分配内存(HostSpot并没有实现栈上分配)
  • 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,引用指向对象或者数组在堆中的内存地址
  • 方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
  • 堆内存是GC执行垃圾回收的重点区域
1.5.1.1、堆空间细分
  • Java7及之前,内存逻辑上分为新生代、老年代、永久代,新生代分为Eden区和Survivor区,Survivor区又分为from区和to区,谁空谁是to
  • Java8及之后,内存逻辑上分为新生代、老年代、元空间,新生代分为Eden区和Survivor区,Survivor区又分为from区和to区,谁空谁是to
1.5.1.2、jvisualvm工具,jdk自带,在bin目录下,也可以安装插件

1.5.1.3、打印垃圾回收的日志

-XX:+PrintGCDetails,打印垃圾回收的日志

1.5.2、虚拟机栈、堆内存、方法区关系


根据方法区中的类创建对象,对象的引用地址保存在栈中,创建的对象实例保存在堆中

1.5.3、设置堆内存的大小与OOM
  • -Xms :堆内存的起始内存(memory start),等价于-XX:InitialHeapSzie

  • -Xmx:堆内存最大占用量(memory max),等价于-XX:MaxHeapSize,超过最大内存将抛出OOM

  • 通常将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾会后清理完堆区后,不需要重新分隔计算堆区的大小,从而提高性能(防止内存抖动,降低GC频率,提高程序的吞吐量)

  • 默认大小

    1. 初始内存大小,物理电脑内存大小 / 64
    2. 最大内存大小,物理电脑内存 / 4
  • jps命令:查看当前 *** 作系统中运行的Java进程,主要是获得进程id

  • jstat命令:查看JVM在gc时的统计信息,jstat -gc 进程号

1.5.4、年轻代与老年代

Java对象划分为两类:生命周期短的和长的

  • 堆内存 = 新生代(YoungGen)(Eden + Survior 0)+ 老年代(OldGen)

  • 新生代与老年代空间默认比例1 : 2

  • -XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1 / 3(ratio:比率比例的意思)

  • jinfo -flag NewRatio ,查看参数设定值

  • 在HotSpot中,Eden区大小和另外两个Survivor大小缺省所占的比例是:8 : 1 : 1

    1. -XX:SurvivorRatio调整这个空间比例,Eden与Survivor区的比例
    2. 实际是6 : 1 : 1,因为有自适应机制,-XX:-UseAdaptiveSizePolicy:-表示关闭自适应,实际没有用。直接用Ratio分配即可
  • 几乎所有的Java对象都是在Eden区被new出来的。Eden区放不了的大对象,直接进入老年代了

  • IBM研究表明,新生代99%的对象都是朝生夕死

  • -Xmn:设置新生代最大内存大小,如果同时设置了新生代比例与此参数冲突,则以此参数为准

1.5.5、对象分配 1.5.5.1、图解对象分配

  1. new出来的对象先放在Eden区,此区有大小限制
  2. 当创建新对象,Eden空间填满时,会触发Minor GC,将Eden区不再被其他对象引用的对象进行销毁。再加载新的对象放到Eden区
  3. 将Eden中剩余的对象移到幸存者0区
  4. 再次触发垃圾回收,此时上次幸存者下来的,放在幸存者0区的,如果没有回收,就会放到幸存者1区
  5. 再次经历垃圾回收,又会将幸存者重新放回幸存者0区,依次类推
  6. GC分代年龄默认是15次,超过15次,则会将幸存者区幸存下来的转去老年区
  7. -XX:MaxTenuringThreshold=N进行设置晋升年龄

总结:针对幸存者s0、s1区,复制之后有交换,谁空谁是to,频繁在新生代收集,很少在老年代收集,几乎不在永久代|元空间收集

1.5.5.2、图解对象分配的流程图

  • 触发YGC,幸存者区就会附带进行回收,不会主动进行回收
  • 超大对象Eden放不下,就要看Old区大小是否可以放下,Old区也放不下,需要FullGC(MajorGC),这两GC概念还是有区别的
1.5.6、常用调优工具
  • JDK命令行
  • VisualVM
  • Jconsole
  • Jprofiler
  • Eclipse:Memory Analyzer Tool
  • Java Flight Recorder
1.5.7、MinorGC、MajorGC、FullGC 1.5.7.1、GC类型

针对HotSpotVM的实现,GC按照内存回收区域分为部分收集(partial)、整堆收集(FullGC)

  • 部分收集(partial): 新生代收集、老年代收集、混合收集
    1. 新生代收集:MinorGC (YoungGC)
    2. 老年代收集:MajorGC(OldGC),目前只有CMS GC会单独收集老年代的行为,很多时候MajorGC与FullGC混淆使用,具体分辨是老年代回收还是整堆回收
    3. 混合收集(MxiGC):收集整个新生代以及部分老年代的垃圾收集,目前只有G1 GC会有这种行为(G1使用region划分内存,同时回收新生代和部分老年代)
  • 整堆收集(FullGC):收集整个Java堆和方法区的垃圾
1.5.7.2、MinorGC的触发条件
  • 当年轻代空间不足时,就会触发MinorGC,这里的年轻代指的是Eden区满,Survivor满不会触发GC。每次MinorGC会清理年轻代的内存,顺带着清理幸存者区
  • 因为Java对象大多朝生夕灭,所以MinorGC非常频繁
  • MinorGC会引发STW(Stop The World)
1.5.7.3、老年代GC(MajorGC/FullGC)触发条件
  • 出现了MajorGC,经常会伴随至少一次MinorGC,但是并非绝对,在Parallel Scavenge收集器的收集策略里就直接进行MajorGC的策略选择过程,也就是老年代空间不足,会先尝试触发MinorGC,如果之后空间还不足,则触发MajorGC
  • MajorGC的速度比MinorGC慢10倍以上,STW的时间更长
  • 如果MajorGC后,内存还不足,就报OOM了。程序发生OOM之前,一定会执行一次FullGC
1.5.7.4、FullGC 触发条件
  • 调用System.gc()时,系统建议执行FullGC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过MinorGC后进入老年代的平均大小,大于老年代的可用内存
  • 由Eden区,Survivor 0区向Survivor 1区复制时,对象的大小大于ToSpace可用内存,则把改对象转存到老年代,且老年代的可用内存小于该对象的大小

FullGC是开发或调优中尽量要避免的,这样暂停时间会短一些

1.5.8、堆空间分代思想
  • 其实不分代也可以,分代的理由是优化GC性能
  • 如果将所有生命周期对象全部放在一起,那判断对象是否存活就需要遍历整个堆内存,效率就大大降低
  • 分代之后,大多数情况下只需要遍历新生代,也就是对象生命周期短的内存区域,就可以回收大量内存
1.5.9、内存分配策略 1.5.9.1、长期存活的对象分配到老年代

如果对象在Eden分配并经过第一次MinorGC后仍然存活,并且能被Survivor区容纳,则被移动到Survivor空间中,并将对象年龄设置为1,对象在Survivor区每熬过一次MinorGC,年龄就+1。当年龄增加到一定程度(默认为15,不同JVM,GC都所有不同)时,就会被晋升到老年代中,-XX:MaxTenuringThreshold参数设置晋升年龄的阈值

1.5.9.2、大对象直接分配到老年代

尽量避免程序中出现过多的大对象,只有Serial和ParNew垃圾收集器支持该行为

1.5.9.3、动态对象年龄分配

如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄

1.5.9.4、空间分配担保
  • -XX:+HandlePromotionFailure
  • JDK7及以后这个参数就失效了
  • 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间,是否大于新生代所有对象的总空间
    1. 如果大于,则此次MinorGC是安全的
    2. 如果小于,则查看-XX:HandlePromotionFailure设置是否允许担保失败
      1. false,则改为进行一次FullGC
      2. true,会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,大于,则尝试进行一次MinorGC,但是这次MinorGC依然是有风险的,小于,则改为进行一次FullGC
    3. jdk6update24之后,这个参数不会再影响到虚拟机的空间分配担保策略。
  • 规则改为只要老年代的连续空间大于新生代对象总大小,或者历次晋升的平均大小,就会进行MinorGC
1.5.10、为对象分配内存TLAB(Thread Local Allocation Buffer)

堆区是线程共享区域,任何线程都可以访问到堆区的共享数据,由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的,因此JVM采用两种策略(TLAB、CAS加失败重试)为对象分配内存。

1.5.10.1、TLAB

从内存模型而不是垃圾收集的角度,对Eden区域进行划分,JVM为每个线程分配了一个私有缓存区域,包含在Eden空间中,多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们将这种内存分配方式成为快速分配策略

  • openjdk衍生出来的JVM都提供了TLAB的设计
  • 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但是JVM确实是将TLAB作为内存分配的首选
  • 开发人员通过-XX:+UseTLAB设置是否开启TLAB空间
  • 默认情况下,TLAB空间内存非常小,仅占有整个Eden空间的1%,通过-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小
  • 一旦对象在TLAB空间分配内存失败,JVM就会尝试通过使用加锁机制确保数据 *** 作的原子性,从而直接在Eden空间中分配内存

    使用TLAB的方式为避免多个线程 *** 作同一地址,需要使用加锁等机制,进而影响分配速度
1.5.10、堆空间的参数设置

1.5.11、堆内存是分配对象的唯一选择吗?
  • 随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术,将会导致一些微秒变化,所有对象分配到堆上渐渐变得不那么绝对了
  • 有一种特殊情况,如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样无需堆上分配,也不需要垃圾回收了,也是最常见的堆外存储技术
  • TaoBaoVM,其中创新的GCIH(GC invisible heap)技术实现了off-heap,实现了将生命周期较长的Java对象从heap中移动heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的
1.5.11.1、逃逸分析

逃逸分析的基本行为就是分析对象动态作用域,当一个对象在方法中定义后,对象只在方法内部使用,则认为没有发生逃逸,当一个对象在方法中被定义后,它被外部方法引用,则认为发生逃逸,例如作为调用参数传递到其他地方中。

1.5.11.1.1、栈上分配

将堆分配对象转为栈分配对象,如果一个对象在栈中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配

1.5.11.1.2、同步策略

如果一个对象被发现只能从一个线程被访问到,对于这个对象的 *** 作可以不考虑同步,JIT编译器可以借助逃逸分析来判断同步块所使用的的锁对象,是否只能够被一个线程访问,而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候,就会取消对这部分代码的同步。这样就大大提高并发性和性能,这个取消同步的过程就叫同步省略,也叫锁消除

1.5.11.1.3、标量替换(分离对象)
  • 有的对象可能不需要作为一个连续的内存结构存在,也可以被访问到,那么对象的部分(或全部)可以不存储在内存。而是存储在CPU寄存器中
  • 标量是指一个无法再分解的更小的数据的数据。Java中基本数据类型就是标量
  • 可以分解的数据叫聚合量,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量

  • 标量替换参数:-XX:+|-EliminateAllocations,默认打开
1.6、方法区(重点) 1.6.1、虚拟机栈、堆内存、方法区交互关系
  • 从线程是否共享该区域来看,方法区线程共享
  • 从变量的存放区域来看,方法区存放类的元数据信息
1.6.2、方法区的理解
  • Java虚拟机规范中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但是一些简单的实现,可能不会选择去进行垃圾收集或者进行压缩。对于HotSpot而言,方法区还有一个别名叫Non-Heap(非堆),目的就是要和堆分开,所以方法区看作是一块独立于Java堆的内存空间
  • 方法区和Java堆一样,是各个线程共享的内存区域
  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样,都是可以不连续的
  • 方法区的大小和堆空间一样,可以选择固定大小或者可扩展
  • 方法区的大小决定了系统可以保存多少个类,如果定义太多类,加载大量的第三方的Jar包,Tomcat部署过多工程,导致方法区溢出,虚拟机同样会抛出内存溢出OOM:PermGenspace或者Metaspace
  • 关闭JVM就会释放这个区域的内存
1.6.3、HotSpot中方法区的演进(JDK7、JDK8)

  • 在jdk7及以前,习惯上把方法区,称为永久代,jdk8开始,使用元空间取代了永久代,
  • 元空间的本质和永久带类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存
  • 根据Jvm规范,如果方法区无法满足新的内存分配需求,将抛出OOM异常
1.6.4、设置方法区大小与OOM

方法区大小不是固定的,JVM可以根据应用动态调整

1.6.4.1、JDK7及以前
  • 通过-XX:PermSize 来设置永久代初始分配空间,默认值是20.75M
  • -XX:MaxPermSize来设定永久代最大可分配空间,32位机器默认是64M,64位机器默认是82M
  • 如果JVM加载的类信息容量超过了这个值,会报OOM:PermGenspace
1.6.4.1、JDK8及以后
  • -XX:MetaspaceSize,初始分配空间
  • -XX:MaxMetaspaceSize,最大分配空间
  • 默认值依赖平台
    1. windows下初始为21M,最大是-1即没有限制,如果不指定大小,虚拟机耗用所有可用系统内存,元数据区发生溢出,一样OOM:Metaspace
    2. 对于一个64位服务端JVM来说,默认的初始元数据区空间为21M,这就是初始的高水位线。一旦触及这个水位线,FulllGC会触发并卸载没有用的类,然后高水位线会被重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放空间不足,在不超过最大设定值时,适当提高该值。如果释放空间过多,则适当降低该值
    3. 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次,FullGC多次调用。为了避免频繁FullGC,建议将-XX:MetaspaceSize设置为一个相对较高的值
1.6.5、如何解决OOM
  • 要解决OOM或heap space异常,一般的手段是通过内存映像分析工具,对dump出来的堆转存储快照进行分析,重点确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄露,还是内存溢出
  • 如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链,于是就能找到内存泄露对象时通过怎样的路径与GC Roots相关联,导致垃圾收集器无法自动回收他们。根据引用链信息,可以较准确的定位出泄露代码的位置
  • 如果不存在内存泄露,或者说内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与物理机器内存对比是否还可以调大,从代码检查是否某些对象生命周期过长,持有状态时间过长,尝试减少程序运行时的内存耗用
1.6.6、方法区的内部结构 1.6.6.1、方法区存储什么

它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存

1.6.6.2、类型信息

对于每个加载的类型(类Class、接口Interface、枚举Enum、注解annotation),JVM必须在方法区中存储以下类型信息

  • 这个类的完整有效名称(全名=包名.类名)
  • 这个类型直接父类的完整有效名,对于interface或Object没有父类
  • 这个类型的修饰符:public、abstract、final等
  • 这个类型直接接口的一个有序列表
1.6.6.3、域信息(Field字段)
  • JVM必须在方法区中保存类型的所有域的相关信息
  • 域的声明顺序
  • 域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient的某个子集)
1.6.6.4、方法信息(Method)

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

  • 声明顺序
  • 方法名称
  • 方法的返回类型,或void
  • 方法参数的数量和类型,按顺序
  • 方法的修饰符,public、private、protected、static、final、synchronized、native、abstract的一个子集
  • 方法的字节码bytecodes、 *** 作数栈、局部变量表及大小
  • 异常表,abstract和native方法除外,每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引
1.6.6.5、non-final的类变量
  • 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
  • 类变量被类的所有实例共享,即使没有类实例时,你也可以访问他
1.6.6.6、全局常量

static final 修饰,被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。直接引用字面量

1.6.6.7、常量池

方法区,内部包含了运行时常量池,字节码文件内部包含了常量池,运行时将常量池加载到方法区,就是运行时常量池。

  • 要弄清楚方法区,需要理解清楚ClassFile,因为加载类的信息都在方法区
  • 要弄清楚方法区的运行时常量池,需要理解清楚ClassFile中的常量池

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

1.6.6.7.1、为什么要用常量池?

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

1.6.6.7.2、常量池有什么?
  • 数量值
  • 字符串值
  • 类引用
  • 字段引用
  • 方法引用

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

1.6.6.8、运行时常量池
  • 运行时常量池是方法区的一部分,常量池表是class文件的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
  • JVM为每个已加载的类型都维护一个常量池,池中的数据像数组项一样,通过索引访问,运行时常量池包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后,才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里转换为真实地址
  • 运行时常量池,相对于class文件常量池的另一个重要特征是:具备动态性,例如:String.intern()方法可以将字符串也放入运行时常量池
  • 当创建类或接口的运行时常量池,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值。则JVM会抛出OOM异常
  • 这里注意,常量池数量为N,则索引为1到N-1
1.6.7、方法区使用举例
















1.6.8、方法区的演进细节(重点)

首先明确,只有HotSpot才有永久代,

1.6.8.1、HotSpot中方法区的变化
  • JDK1.6及之前,有永久代,静态变量和字符串常量池存放在永久代上

  • JDK1.7,有永久代,但已经开始逐步去永久代,字符串常量池、静态变量从永久代中移除,转而保存在堆内存中

  • JDK1.8及之后,无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池,静态变量仍在堆

1.6.8.2、永久代为什么被元空间替换

随着 Java8的到来,HotSpotVM中再也见不到永久代了,但是并不意味着类的元数据信息也消失了,这些数据被转移到了一个与堆不相连的本地内存区域,这个区域叫做元空间MetaSpace。

  • 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统的可用内存空间
  • 为永久代设置空间大小很难确定,在某些场景下,如果动态加载类过多,就容易产生OOM
  • 而元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受本地内存限制
  • 对永久代进行调优是很困难的
1.6.9、方法区的垃圾回收

有些人认为方法区是没有垃圾收集行为的,其实不然。Java虚拟机规范对方法区的约束非常宽松,提到过可以不要求虚拟机在方法区实现垃圾收集。事实上,也确实有未实现或未能完整实现方法区类型卸载的收集器,如JDK11 ZGC

1.6.9.1、方法区的垃圾收集主要回收两部分内容
  • 常量池中废弃的常量,HotSpot对常量池的回收策略很明确,只要常量池中的常量没有被任何地方引用,就可以被回收,回收废弃常量与回收Java堆中对象非常类似

  • 不再使用的类型(二进制数据),需要同时满足三个条件

    1. 该类所有的实例已经被回收,java堆中不存在该类及其任何派生子类的实例
    2. 加载该类的类加载器已经被回收
    3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问改类的方法

满足以上三个条件后,并不是和对象一样立即被回收,仅仅是允许,HotSpot虚拟机提供了-Xnoclassgc参数进行控制

在大量使用反射、动态代理、CGLib等字节码框架、动态生成JSP以及OSGI这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

1.6.9.2、方法区内常量池中主要存放的两大类常量
  • 字面量,比较接近Java语言层次的常量概念,如文本字符串,被声明为final的常量值等
  • 符号引用(其实也是字符串),属于编译原理方面的概念
    1. 类和接口的全限定名
    2. 字段的名称和描述符
    3. 方法的名称和描述符
1.7、直接内存
  • 不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,直接内存是在Java堆外的,直接向系统申请的内存区间,来源于NIO,通过存在堆中的DirectByteBuffer *** 作Native内存。
  • 通常,访问直接内存的速度会优于Java堆,即读写性能高,因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存,Java的NIO库允许Java程序使用直接内存,用于数据缓冲区。
  • 也可能导致OOM异常,直接内存在堆外,所以大小不受限于-Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于 *** 作系统能给出的最大内存。
  • 缺点
    1. 分配回收成本较高
    2. 不受JVM内存回收管理
    3. 如果出现内存泄漏难以排查
  • 直接内存大小可以通过MaxDirectMemorySize设置,如果不指定,默认与堆的最大值-Xmx参数值一致

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/922308.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存