二、运行时数据区
-
每个JVM只有一个Runtime实例,只有一个运行时数据区。
-
虚拟机栈、堆、方法区最重要
-
方法区和堆与虚拟机的生命周期相同(随虚拟机启动而创建,虚拟机退出而销毁),程序计数器、虚拟机栈、本地方法栈生命周期与线程相同。
-
多线程共享方法区(堆外内存或元空间)和堆,程序计数器、虚拟机栈、本地方法栈每个线程各一份。
-
线程分为守护线程和普通线程。在Hotspot JVM中,每个线程都与操作系统的本地线程直接映射:当一个java线程准备好(程序计数器、虚拟机栈、本地方法栈)执行后,此时一个操作系统的本地线程初始化成功。当java线程执行终止后,本地线程也会回收。操作系统负责所有线程的安排调度到任何一个可用的CPU上,本地线程初始化成功,它就会调用java线程中的run()方法。如果run方法异常,java线程终止,本地线程会决定JVM是否终止(当前线程是不是最后一个非守护线程,是则JVM退出)。
再放一遍这个图,关注中间部分
1. 程序计数器(PC寄存器、程序钩子)
1.1 作用:
用来存储指向下一条指令的地址(即将执行的指令代码),由执行引擎读取下一条指令。
1.2 注意
-
是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域
-
每个线程有一个独立的程序计数器,是线程私有的,线程之间互不影响,生命周期与线程保持一致
-
任何时间一个线程都只有一个方法在执行(当前方法),程序计数器存储当前线程正在执行的java方法的下一条JVM指令。(如果在执行native本地方法,计数器值则是未指定值undefined)
-
没有GC和OOM:运行时数据区中唯一不会出现OOM(OutofMemoryError内存溢出异常)的区域,没有垃圾回收(方法区和堆有垃圾回收,程序计数器和栈没有)
-
当前线程所执行的字节码的行号指示器:为了线程切换后能恢复到正确的位置
1.3 面试题
2. 虚拟机栈
2.1 注意
-
每个线程创建时都会创建一个虚拟机栈,生命周期和线程的一致,为线程私有。
-
内部保存一个个栈帧,对应着一次次的Java方法调用。主管Java程序的运行,保存方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。
-
快速有效的存储方式,访问速度仅次于程序计数器
-
JVM直接对JAVA栈的操作只有两个:
每个方法执行,伴随着进栈(入栈,压栈)
执行结束的出栈
-
没有垃圾回收,可能会出现两种异常:
虚拟机栈的大小可以是固定的活动态扩展。第一种固定大小,如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverFlow异常;第二种动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
使用-Xss 设置线程的最大栈空间。
2.2 栈的存储单位
-
每个线程都有自己的栈,栈中的数据以栈帧格式存储
-
线程上正在执行的每个方法都各自对应一个栈帧
-
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各个数据信息
-
先进后出
-
一条活动的线程中,一个时间点上,只会有一个活动的栈帧。只有当前正在执行的方法的栈顶栈帧是有效的,这个称为当前栈帧,对应方法是当前方法,对应类是当前类
-
执行引擎运行的所有字节码指令只针对当前栈帧进行操作
-
如果方法中调用了其他方法,对应的新的栈帧会被创建出来,放在顶端,成为新的当前帧,在方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为新的栈帧
-
栈帧被弹出:正常return返回;抛出异常
-
不同线程中包含的栈帧不允许存在相互引用
2.3 栈帧的内部结构
栈帧的大小取决于内部结构。一个栈帧即一个方法。局部变量表与操作数栈较为重要,其他三个部分可以统称为帧数据区。
2.3.1 局部变量表:数组(从0索引,-1索引结束)
-
主要用于存储方法形参,定义在方法体内部的局部变量,数据类型包括各类基本数据类型,对象引用,以及return address类型。
-
由于局部变量表建立在线程的栈上,是线程私有的,因此不存在数据安全问题。
-
局部变量表容量大小是在编译期确定下来的,存放编译期可知的各种基本数据类型(8种),引用类型(reference),return address 类型。
-
局部变量表中的变量只在当前方法调用中有效,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。
-
方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
-
在栈帧中,与性能调优关系最密切的部分,就是局部变量表,方法执行时,虚拟机使用局部变量表完成方法的传递。
-
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
2.3.2 操作数栈
-
在方法执行的过程中,根据字节码指令,往栈中写入/提取数据,即入栈push/出栈pop。
-
如果被调用方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新程序计数器中下一条需要执行的字节码指令。主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
-
操作数栈并非采用访问索引方式进行数据访问,而是只能通过标准的入栈、出栈操作完成一次数据访问。
-
当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,此时这个方法的操作数栈是空的。
-
每一个操作数栈会拥有一个明确的栈深度,用于存储数值,最大深度在编译期就定义好。32bit类型占用一个栈单位深度,64bit类型占用两个栈单位深度。
-
Java虚拟机的解释引擎是基于栈的执行引擎,其中栈就是操作数栈,是执行引擎的一个工作区。
-
bipush入栈,istore出栈存入局部变量表,iload入栈,iadd运算出栈两个,结果入栈(字节码指令经执行引擎变机器指令给CPU)
2.3.3 动态链接(指向运行时常量池的方法引用)
-
每一个栈帧内部都包含一个指向运行时常量池中,该帧所属方法的引用
-
目的是为了支持当前方法的代码能够实现动态链接,比如invokedynamic指令
-
在java源文件被编译成字节码文件中时,所有的变量、方法引用都作为符号引用,保存在class文件的常量池中。
-
描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的。
-
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
2.3.4 方法返回地址:存放调用该方法的程序计数器的值
方法的结束:方法正常退出或出现未处理异常,非正常退出
返回指令包括ireturn-boolean,byte,char,short,和int类型、lreturn-long类型、freturn-float类型、dreturn-double类型、areturn-引用类型。另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。
本质上,方法的退出就是当前栈帧出栈的过程。此时需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器值等,让调用者方法继续执行下去。
2.3.5 一些附加信息-不确定有
允许携带与Java虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息。可选。
3. 本地方法栈
-
Java虚拟机栈管理Java方法的调用,而本地方法栈用于管理本地方法的调用,功能相似,也会抛出StackOverFlow和OutOfMemoryError异常。
-
本地方法栈,也是线程私有的。
-
允许被实现成固定或者是可动态扩展的内存大小,内存溢出情况和Java虚拟机栈相同
-
使用C语言实现
-
具体做法是在本地方法栈中登记native方法,在执行引擎执行时加载到本地方法库
-
当某个线程调用一个本地方法时,就会进入一个全新,不受虚拟机限制的世界,它和虚拟机拥有同样的权限。
-
并不是所有的JVM都支持本地方法,因为Java虚拟机规范并没有明确要求本地方法栈的使用语言,具体实现方式,数据结构等
-
Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一
4. 堆
4.1 堆的概述
-
一个JVM实例(进程)只存在一个堆内存,被所有线程共享(线程安全问题),但可以在堆内划分一小块区域,作为线程私有的缓冲区(TLAB),并发性更好。
-
在虚拟机启动的时候创建,其空间大小也就确认了,堆内存大小可调节
-Xms表示堆空间的起始内存
-Xmx表示堆空间的最大内存,超过最大内存将抛出OOM
通常将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾会后清理完堆区后,不需要重新分隔计算堆区的大小,从而提高性能
-
堆也是Java内存管理的核心区域,是JVM中所管理的内存最大的一块,也是垃圾回收器管理的主要区域。方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
-
堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
-
“几乎”所有的对象实例都在这里分配内存:逃逸分析,判断是否发生逃逸,如果没有,可以进行栈上分配或标量替换。数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,引用指向对象或者数组在堆中的位置
4.2 堆空间细分
永久区/元空间实际在方法区。
4.2.1 新生代:
用来存放新生的对象。一般占据堆的1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代又分为Eden 区、ServivorFrom、 ServivorTo 3个区。
4.2.2 老年代:
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC 前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。
当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM (Out of Memory)异常。
4.2.3 永久代
指内存的永久保存区域,主要存放Class 和Meta (元数据)的信息,Class在被加载的时候被放入永久区域,它和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class 的增多而胀满,最终抛出OOM异常。
4.3 对象分配的一般过程
4.4 Minor GC/Major GC/Full GC
4.4.1 Minor GC:年轻代的垃圾收集
4.4.2 Major GC:老年代的垃圾收集
4.4.3 Full GC:整堆手机,整个堆和方法区
4.5 内存分配策略
4.6 TLAB
从内存模型而不是垃圾收集的角度,对Eden区域进行划分,JVM为每个线程分配了一个私有缓存区域,包含在Eden空间中
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们将这种内存分配方式成为快速分配策略
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但是JVM确实是将TLAB作为内存分配的首选
- 开发人员通过 -XX:UseTLAB 设置是否开启TLAB空间
- 默认情况下,TLAB空间内存非常小,仅占有整个Eden空间的1%,通过
-XX:TLABWasteTargetPercent 设置TLAB空间所占用Eden空间的百分比大小
- 一旦对象在TLAB空间分配内存失败,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存
4.7 逃逸分析
4.7.1 逃逸分析
逃逸分析的基本行为就是分析对象动态作用域。
- 当一个对象在方法中定义后,对象只在方法内部使用,则认为没有发生逃逸
- 当一个对象在方法中被定义后,它被外部方法引用,则认为发生逃逸,例如作为调用参数传递到其他地方
4.7.2 栈上分配
将堆分配转为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
4.7.3 同步策略
如果一个对象被发现只能从一个线程被访问到,对于这个对象的操作可以不考虑同步。
JIT编译器可以借助逃逸分析来判断同步块所使用的的锁对象,是否只能够被一个线程访问,而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候,就会取消对这部分代码的同步。这样就大大提高并发性和性能,这个取消同步的过程就叫同步省略,也叫锁消除
4.7.4 分离对象或标量替换
分离对象:有的对象可能不需要作为一个连续的内存结构存在,也可以被访问到,那么对象的部分(或全部)可以不存储在内存。而是存储在CPU寄存器中
标量是指一个无法再分解的更小的数据的数据。Java中原始数据类型就是标量
可以分解的数据叫聚合量,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量
4.8 堆空间的常用参数 
当xms、xmx、xmn、newratio确定,survivorratio设置的过大(即Eden区很大,survivor区很小),会导致,minor GC时,survivor区放不下而将对象存到老年代,失去了minor GC和分代的意义。survivorratio设置的过小(即Eden区很小,survivor区很大),会导致eden区很快存满,频繁的进行minor GC,影响用户进程,STW的时间变多。