JVM-运行时数据区

运行时数据区包括:方法区,堆,程序计数器,本地方法栈,虚拟机栈

JVM调优工具

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

基础

结构概览1

结构概览2

  • 每个线程:有独立包括程序计数器、栈、本地栈
  • 线程间共享:堆,堆外内存(永久代或元空间、代码缓存)

程序计数器(PC寄存器)

  • 名称Program Counter RegisterJVM中的PC寄存器是对物理PC寄存器的一种抽象模拟
  • 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
  • 任何时间一个线程只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefined)
  • 它是唯一一个在Java虚拟机规范中没有规定OOM(内存溢出)的区域,没有GC(垃圾回收)

PC寄存器字节码操作

问题

使用PC寄存器存储字节码指令地址有什么用?为什么使用PC寄存器记录当前线程的执行地址?

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

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

CPU在并发执行线程的时候会来回切换,为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的方法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况

虚拟机栈

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

介绍

  • Java虚拟机栈,早起也叫Java栈,每个线程创建时都会创建一个虚拟机栈,内部保存一个个栈帧,对应着一次次的Java方法调用
  • 生命周期和线程的一致
  • 主管Java程序的运行,保存方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回

优点

  • 跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令
  • 快速有效的存储方式,访问速度仅次于程序计数器
  • JVM直接对JAVA栈的操作只有两个
    1. 每个方法执行,伴随着进栈(入栈,压栈)
    2. 执行结束的出栈
  • 栈不存在GC,但是存在OOM,Java栈大小是动态或者固定不变的。如果是动态扩展,无法申请到足够内存OOM,如果是固定,线程请求的栈容量超过固定值,则StackOverflowError
  • 使用-Xss (记忆:站着做一个小手术,栈Xss),设置线程的最大栈空间

-Xss设置

栈的存储单位

  • 每个线程都有自己的栈,栈中的数据以栈帧格式存储
  • 线程上正在执行的每个方法都各自对应一个栈帧
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各个数据信息
  • 先进后出,后进先出
  • 一条活动的线程中,一个时间点上,只会有一个活动的栈帧。只有当前正在执行的方法的栈顶栈帧是有效的,这个称为当前栈帧,对应方法是当前方法,对应类是当前类

活动线程

  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
  • 如果方法中调用了其他方法,对应的新的栈帧会被创建出来,放在顶端,成为新的当前帧

栈运行原理

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

栈帧的内部结构

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

内部结构

局部变量表

  • 定义为一个数字数组,主要用于存储方法参数,定义在方法体内部的局部变量,数据类型包括各类基本数据类型,对象引用,以及return Address类型
  • 建立在线程的栈上,是线程私有的,因此不存在数据安全问题
  • 容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中
  • 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型
  • 最基本的存储单元是slot,32位占用一个slot,64位类型(long和double)占用两个slot
  • 局部变量表中的变量只有在当前方法调用中有效,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。
  • 方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁

Slot

  • JVM虚拟机会为局部变量表中的每个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
  • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this,会存放在index为0的slot处,其余的参数表顺序继续排列

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

补充

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

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

操作数栈

  • 可以使用数组和链表来实现,操作数栈就是使用数组来实现的
  • 在方法执行的过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈

  • 如果被调用方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新程序计数器中下一条需要执行的字节码指令
  • Java虚拟机的解释引擎是基于栈的执行引擎,其中栈就是操作数栈
  • 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
  • 当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
  • 每一个操作数栈会拥有一个明确的栈深度,用于存储数值,最大深度在编译期就定义好
  • 栈中,32bit类型占用一个栈单位深度,64bit类型占用两个栈单位深度
  • 操作数栈并非采用访问索引方式进行数据访问,而是只能通过标准的入栈、出栈操作完成 一次数据访问
  • 栈顶缓存技术:由于操作数是存储在内存中,频繁的进行内存读写操作影响执行速度,将栈顶元素全部缓存到物理CPU的寄存器中,依此降低对内存的读写次数,提升执行引擎的执行效率

动态链接

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

方法的调用

  • 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行时期间保持不变,这种情况下降调用方的符号引用转为直接引用的过程称为静态链接
  • 动态链接:如果被调用的方法无法再编译期被确定下来,只能在运行期将调用的方法的符号引用转为直接引用,这种引用转换过程具备动态性,因此被称为动态链接
  • 方法的绑定:
    • 绑定是一个字段、方法、或者类在符号引用被替换为直接引用的过程。仅仅发生一次
    • 早期绑定:被调用的目标方法如果再编译期可知,且运行期保持不变
    • 晚期绑定:被调用的方法在编译期无法被确定,只能够在程序运行期根据实际的类型绑定相关的方法
  • Java中任何一个普通方法都具备虚函数的特征(运行期确认,具备晚期绑定的特点),C++中则使用关键字virtual来显式定义
  • 如果在java程序中,不希望某个方法拥有虚函数的特征,则可以使用关键字final来标记这个方法
  • 虚方法和非虚方法
    • 非虚方法:如果方法在编译期就确定了具体的调用版本,则这个版本在运行时是不可变的。这样的方法称为非虚方法;静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法
    • 其他方法称为虚方法

方法调用指令

普通调用指令

  • invokestatic:调用静态方法,解析阶段确定唯一方法版本
  • invokevirtual:调用所有虚方法
  • invokeinterface:调用接口方法,虚方法
  • invokespecial:调用方法,私有及父类方法,解析阶段确定唯一方法版本
  • 其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法
  • 动态调用指令JDK1.7新增:invokedynamic 动态解析出需要调用的方法,然后执行
  • 直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式

静态语言和动态语言区别在于对类型的检查是编译器还是运行期,满足编译期就是静态类型语言,反之就是动态类型语言,Java是静态类型语言,动态调用指令增加了动态语言的特性

静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息

方法重写的本质

  • 找到操作数栈顶的第一个元素所执行的对象的实际类型,记做C
  • 如果在类型C中找到与常量池中描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,如果不通过,则返回java.lang.IllegalAccessError异常
  • 否则,按照继承关系从下往上依次对C的各个父类进行上一步的搜索和验证过程。* 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常

虚方法表

  • 面向对象的编程中,会很频繁的使用动态分配,如果每次动态分配的过程都要重新在类的方法元数据中搜索合适的目标的话,就可能影响到执行效率,因此为了提高性能,JVM采用在类的方法区建立一个虚方法表,使用索引表来代替查找
  • 每个类都有一个虚方法表,表中存放着各个方法的实际入口
  • 虚方法表会在类加载的链接阶段被创建,并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法也初始化完毕

方法返回地址

  • 存放调用该方法的pc寄存器的值
  • 无论哪种方式退出,方法退出后,都会返回该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址
  • 异常退出的,返回地址是通过异常表来确定,栈帧中一般不会保存这部分信息
  • 执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口
  • 返回指令包括:ireturn返回值是boolean,byte,char,short,和int类型时使用,lreturn,dreturn,areturn
  • 本质上,方法的退出就是当前栈帧出栈的过程。此时需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器值等,让调用者方法继续执行下去。
  • 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值

一些附加信息

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

虚拟机栈面试题

本地方法接口

  • 简单来说就是,一个Native Method就是一个Java调用非Java代码的接口
  • 使用native关键词修饰的方法就是本地方法

本地方法栈

  • 存在Error,不存在GC
  • Java虚拟机栈管理Java方法的调用,而本地方法栈用于管理本地方法的调用
  • 本地方法栈,也是线程私有的
  • 当一个线程调用一个本地方法时,就进入了一个全新的并且不再受虚拟机限制的位置,它和虚拟机拥有同样的权限
  • 允许被实现成固定或者是可动态扩展的内存大小:内存溢出情况和Java虚拟机栈相同
  • 使用C语言实现
  • 具体做法是Native Method Stack 中登记native方法,在Execution Engine执行时加载到本地方法库
  • 当某个线程调用一个本地方法时,就会进入一个全新,不受虚拟机限制的世界,它和虚拟机拥有同样的权限。
  • 并不是所有的JVM都支持本地方法,因为Java虚拟机规范并没有明确要求本地方法栈的使用语言,具体实现方式,数据结构等
  • Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一

核心概述

  • 存在Error和GC
  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域
  • Java堆区在JVM启动的时候即被创建,其空间大小也就确认了。堆内存的大小是可调节的
  • Java虚拟机规范规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(TLAB)
  • 几乎所有的对象实例都在这里分配内存
  • 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,引用指向对象或者数组在堆中的位置
  • 方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
  • 堆是GC执行垃圾回收的重点区域

堆控件细分

  • 设置堆内存的大小与OOM:-Xms :小秘书表示堆空间的起始内存,-Xmx:小明星表示堆空间的最大内存
  • 初始内存大小是:物理电脑内存大小 / 64,最大内存大小是:物理内存大小 / 4
  • jps查看当前程序运行的进程,jstat查看JVM在gc时的统计信息,jstat -gc 进程号
  • 通常将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾会后清理完堆区后,不需要重新分隔计算堆区的大小,从而提高性能

年轻代和老年代

  • 新生代与老年代空间默认比例1:2-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
  • 在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是:8:1:1,但实际是6:1:1
  • 手动设置新生代中的Eden区与Survivor区的比例**-XX:SurvivorRatio=8**
  • 关闭自适应的内存分配策略**-XX:-UseAdaptiveSizePolicy**,暂时用不到
  • 几乎所有的Java对象都是在Eden区被new出来的,如果对象太大,Eden放不下就直接到老年代
  • 设置新生代的空间大小**-Xmn**(一般不设置)

对象分配过程

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

过程

总结:

  • 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
  • 垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间搜集
  • 触发YGC,幸存者区就会进行回收,不会主动进行回收
  • 超大对象eden放不下,就要看Old区大小是否可以放下,old区也放不下,需要FullGC(MajorGC),这两GC概念还是有区别的

MinorGC,MajorGC,FullGC

针对HotSpot VM实现,它里面的GC按照回收区域分为两大种类型:一种是部分收集(MinorGC,MajorGC),一种是整堆收集(FullGC)

  • 部分收集:不是完整收集整个Java堆的垃圾收集
    • 新生代收集(MinorGC/YoungGC):只是新生代(Eden、S0、S1)的垃圾收集
    • 老年代收集(MajorGC/OldGC):只是老年代的垃圾收集
      • 目前,只有CMS GC会有单独收集老年代的行为
      • 很多时候MajorGC会和FullGC混淆使用,需要具体分辨是老年代回收还是整堆回收
    • 混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集,目前只有G1 GC会有这种行为
  • 整堆收集:收集整个Java堆和方法区的垃圾收集
  • 测试GC-Xms9m -Xmx9m -XX:+PrintGCDetails
  • 如果对象再Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor区容纳,则被移动到Survivor空间中,并将对象年龄设置为1,对象再Survivor区每熬过一次MinorGC,年龄就+1,当年龄增加到一定程度(默认为15,不同Jvm,GC都所有不同)时,就会被晋升到老年代中

新生代GC(MinorGC)的触发条件

  • 年轻代空间不足时,就会触发MinorGC,这里的年轻代指的是Eden代满,Survivor满不会触发GC。每次MinorGC会清理年轻代的内存
  • 因为Java对象大多朝生夕灭,所以MinorGC非常频繁
  • MinorGC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行

老年代GC(MajorGC/FullGC)触发条件

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

FullGC的触发机制

  • 调用System.gc()时,系统建议执行FullGC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过MinorGC后进入老年代的平均大小,大于老年代的可用内存
  • 由Eden区,Survivor 0区向Survivor 1区复制时,对象的大小大于ToSpace可用内存,则把改对象转存到老年代,且老年代的可用内存小于该对象的大小
  • FullGC是开发或调优中尽量要避免的,这样暂停时间会短一些

为什么需要把Java堆分代,不分代就不能正常工作了吗

其实不分代也可以,分代的理由是优化GC性能
如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室中。GC的时候要找到哪些对象没用,这样就会在堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储朝生夕死的对象的区域进行回收,这样就可以腾出很大的空间出来

内存分配策略

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

TLAB

Thread Local Allocation Buffer

  • 堆区是线程共享区域,任何线程都可以访问到堆区的共享数据

  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。

  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度

  • 从内存模型而不是垃圾收集的角度,对Eden区域进行划分,JVM为每个线程分配了一个私有缓存区域,包含在Eden空间中

  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们将这种内存分配方式成为快速分配策略
  • openjdk衍生出来的JVM都提供了TLAB的设计
  • 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但是JVM确实是将TLAB作为内存分配的首选
  • 开发人员通过-XX:UseTLAB设置是否开启TLAB空间
  • 默认情况下,**TLAB空间内存非常小,仅占有整个Eden空间的1%**,通过-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小
  • 一旦对象在TLAB空间分配内存失败,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存

堆空间参数设置

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

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

方法区

  • 存在Error和GC

相关文章

JVM

JVM-类加载子系统

JVM-虚拟机栈面试题