二、运行时数据区
-
每个JVM只有一个Runtime实例,只有一个运行时数据区。
-
虚拟机栈、堆、方法区最重要
-
方法区和堆与虚拟机的生命周期相同(随虚拟机启动而创建,虚拟机退出而销毁),程序计数器、虚拟机栈、本地方法栈生命周期与线程相同。
-
多线程共享方法区(堆外内存或元空间)和堆,程序计数器、虚拟机栈、本地方法栈每个线程各一份。
-
线程分为守护线程和普通线程。在Hotspot JVM中,每个线程都与操作系统的本地线程直接映射:当一个java线程准备好(程序计数器、虚拟机栈、本地方法栈)执行后,此时一个操作系统的本地线程初始化成功。当java线程执行终止后,本地线程也会回收。操作系统负责所有线程的安排调度到任何一个可用的CPU上,本地线程初始化成功,它就会调用java线程中的run()方法。如果run方法异常,java线程终止,本地线程会决定JVM是否终止(当前线程是不是最后一个非守护线程,是则JVM退出)。
再放一遍这个图,关注中间部分
JVM内存布局
差异主要在于方法区。
1. 程序计数器(PC寄存器、程序钩子)
1.1 作用:
用来存储指向下一条指令的地址(即将执行的指令代码),由执行引擎读取下一条指令。
1.2 注意
-
是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域
-
每个线程有一个独立的程序计数器,是线程私有的,线程之间互不影响,生命周期与线程保持一致
-
任何时间一个线程都只有一个方法在执行(当前方法),程序计数器存储当前线程正在执行的java方法的下一条JVM指令。(如果在执行native本地方法,计数器值则是未指定值undefined)
-
没有GC和OOM:运行时数据区中唯一不会出现OOM(OutofMemoryError内存溢出异常)的区域,没有垃圾回收(方法区和堆有垃圾回收,程序计数器和栈没有)
-
当前线程所执行的字节码的行号指示器:为了线程切换后能恢复到正确的位置
1.3 面试题
1. 使用程序计数器存储字节码指令地址的作用/为什么使用程序计数器记录当前线程的执行地址?
因为CPU需要不停地切换各个线程,而切换回来以后,必须知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么字节码指令。
2. 程序计数器为什么设计成线程私有?
所谓的多线程并发,在一个特定时间段只会执行其中一个线程的方法(CPU时间片),CPU会不停地做任务切换,必然导致经常中断和恢复。为了能够准确地记录各个线程下一条要执行的字节码指令的地址,最好的的办法就是为每个线程都分配一个独立的程序计数器,各个线程进行独立计算,不会互相干扰。
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 类型。
-
局部变量表中的变量只在当前方法调用中有效,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。
-
方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
-
在栈帧中,与性能调优关系最密切的部分,就是局部变量表,方法执行时,虚拟机使用局部变量表完成方法的传递。
-
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
最基本的存储单元是slot变量槽
32位(byte、short、char、boolean存储前被转换为int)占用一个slot,64位类型(long和double)占用两个slot。如果访问64bit的局部变量,只需使用第一个索引。
JVM为局部变量表中的每个Slot分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
当一个实例方法(非静态)被调用,局部变量按顺序被复制到局部变量表中的每一个slot上。
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this,会存放在索引为0的slot处,其余的参数表顺序继续排列。静态方法不能使用this,因为this变量不存在于当前方法的局部变量表(this是属于对象的,而静态方法不用对象就可以调用,而实例方法必须用对象调用)。
槽位是可以重复的,如果一个局部变量过了其作用域,那么其作用域之后申明的新的局部变量就有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
局部变量VS静态变量(类变量)
变量按照数据类型分为基本数据类型和引用数据类型;
按照声明的位置分为成员变量和局部变量;
成员变量分为静态变量和实例变量;
成员变量:在使用前经历过初始化过程(类加载中的初始化)
静态变量:链接的准备阶段给类变量默认赋值,初始化阶段显示赋值,即静态代码块赋值
实例变量:随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
局部变量:在使用前,必须显式赋值,否则编译不通过
2.3.2 操作数栈
-
在方法执行的过程中,根据字节码指令,往栈中写入/提取数据,即入栈push/出栈pop。
-
如果被调用方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新程序计数器中下一条需要执行的字节码指令。主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
-
操作数栈并非采用访问索引方式进行数据访问,而是只能通过标准的入栈、出栈操作完成一次数据访问。
-
当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,此时这个方法的操作数栈是空的。
-
每一个操作数栈会拥有一个明确的栈深度,用于存储数值,最大深度在编译期就定义好。32bit类型占用一个栈单位深度,64bit类型占用两个栈单位深度。
-
Java虚拟机的解释引擎是基于栈的执行引擎,其中栈就是操作数栈,是执行引擎的一个工作区。
-
bipush入栈,istore出栈存入局部变量表,iload入栈,iadd运算出栈两个,结果入栈(字节码指令经执行引擎变机器指令给CPU)
栈顶缓存技术ToS(Top of Stack Cashing):
基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
由于操作数是存储在内存中,频繁的进行内存读写操作影响执行速度,将栈顶元素全部缓存到物理CPU的寄存器中,依此降低对内存的读写次数,提升执行引擎的执行效率。
2.3.3 动态链接(指向运行时常量池的方法引用)
常量池VS运行时常量池
常量池在字节码文件中,运行时常量池在运行时的方法区中。
为什么需要常量池?
常量池提供一些符号和常量,便于指令的识别。
没有也行,但是很占内存。
-
每一个栈帧内部都包含一个指向运行时常量池中,该帧所属方法的引用
-
目的是为了支持当前方法的代码能够实现动态链接,比如invokedynamic指令
-
在java源文件被编译成字节码文件中时,所有的变量、方法引用都作为符号引用,保存在class文件的常量池中。
-
描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的。
-
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
方法的调用:
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
静态链接-早期绑定-非虚方法
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行时期间保持不变,这种情况下,将调用方的符号引用转为直接引用的过程称为静态链接。构造函数?
动态链接-晚期绑定-虚方法-体现 ”多态“特性
如果被调用的方法无法再编译期被确定下来,只能在运行期将调用的方法的符号引用转为直接引用,这种引用转换过程具备动态性,因此被称为动态链接。需要实例化对象调用?
方法的绑定:描述程序中变量或方法在什么时间确定其地址或实现的方式
绑定是一个字段、方法、或者类在符号引用被替换为直接引用的过程。仅仅发生一次。
早期绑定:
被调用的目标方法如果在编译期可知,且运行期保持不变。静态链接通常是早期绑定。
晚期绑定:
被调用的方法在编译期无法被确定,只能够在程序运行期根据实际的类型绑定相关的方法。动态链接通常是晚期绑定的。
虚方法VS非虚方法:描述方法是否在运行时根据对象的实际类型来决定调用哪个方法
Java中任何一个普通方法都具备虚函数的特征(运行期确认,具备晚期绑定的特点)。如果在java程序中,不希望某个方法拥有虚函数的特征,则可以使用关键字final来标记这个方法。
非虚方法:
如果方法在编译期就确定了具体的调用版本,在运行时不可变。这样的方法称为非虚方法。静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法。(不能被重写的)
虚方法:
其他方法称为虚方法(会被重写的方法)。虚方法的调用是在运行时确定的,根据对象的实际类型决定调用哪个派生类的重写方法。
子类对象的多态性的使用前提:①类的继承关系 ②方法的重写
方法调用指令
1. 普通调用指令
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用<init>方法,私有及父类方法,解析阶段确定唯一方法版本
invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法,会被重写
shouFinal(); // invokevirtual,虽然是invokevirtual,但被final修饰,是非虚方法。
super.showFinal(); // invokespecial,调用父类
showCommon(); // invokevirtual,子类未重写该方法,调用的是父类的方法,但没有显示的super., 所以还是invokevirtual
四条普通指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。
2. 动态调用指令:JDK1.7新增,为了实现动态类型语言而做的改进
invokedynamic:动态解析出需要调用的方法,然后执行
直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式
静态语言和动态语言
区别在于对类型的检查是编译器还是运行期,满足编译期就是静态类型语言,反之就是动态类型语言。
静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息。变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
Java是静态类型语言,编译时报错。invokedynamic动态调用指令增加了动态语言的特性。
python和JavaScript是动态类型语言。
方法重写的本质
- 找到操作数栈顶所执行的对象的实际类型,记做C
- 如果在类型C中找到与常量池中描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,如果不通过,则返回java.lang.IllegalAccessError非法访问异常
- 否则,按照继承关系从下往上依次对C的各个父类进行上一步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
IllegalAccessError:程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变
AbstractMethodError:调用了抽象类方法
虚方法表:更快的找到调用哪一个方法
面向对象的编程中,会很频繁的使用动态分配,如果每次动态分配的过程都要重新在类的方法元数据中搜索合适的目标的话,就可能影响到执行效率。
因此为了提高性能,JVM采用在类的方法区建立一个虚方法表,使用索引表来代替查找。非虚方法不会出现在表中,因为非虚方法已经可以确定。
每个类都有一个虚方法表,表中存放着各个方法的实际入口。
虚方法表会在类加载的链接阶段被创建,并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法也初始化完毕。
Dog重写的两个方法指向自身,其他指向父类。
2.3.4 方法返回地址:存放调用该方法的程序计数器的值
方法的结束:方法正常退出或出现未处理异常,非正常退出
无论哪种方式退出,方法退出后都会返回该方法被调用的位置。
方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
异常退出的,返回地址是通过异常表来确定,栈帧中一般不会保存这部分信息。
上述异常处理表表示:字节码指令4~8行出现异常,如果没有处理,goto到16行return返回结束;如果处理,按target-11行处理
执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口 。
在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
返回指令包括ireturn-boolean,byte,char,short,和int类型、lreturn-long类型、freturn-float类型、dreturn-double类型、areturn-引用类型。另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。
本质上,方法的退出就是当前栈帧出栈的过程。此时需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器值等,让调用者方法继续执行下去。
2.3.5 一些附加信息-不确定有
允许携带与Java虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息。可选。
3. 本地方法栈
本地方法:
就是一个Java调用非Java代码的接口,就是一个Java方法,该方法的实现由非Java语言实现。
在定义一个native method时,并不提供实现体(有点像抽象方法),因为其实现体是由非iava语言在外面实现的。native不能用abstract一起使用,因为abstract抽象方法没有方法体,而native是有方法体的。
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
目前使用较少,除非是与硬件相关。
为什么使用本地方法?
在有些层面任务中,Java实现并不容易,或对效率很在意。
与Java环境外交互
与操作系统交互
例如与操作系统底层或硬件交换信息时的情况
例如启动一个线程
-
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 堆空间细分
为什么要分代?
其实不分代完全可以,分代的唯一理由就是优化GC性能。
如果没有分代,那所有的对象都在一块,GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
永久区/元空间实际在方法区。
4.2.1 新生代:
用来存放新生的对象。一般占据堆的1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代又分为Eden 区、ServivorFrom、 ServivorTo 3个区。
Minor GC和Major GC区别:
Minor GC:简单理解就是发生在年轻代的GC。三步(复制--清空--互换)
Minor GC的触发条件为:
当产生一个新对象,新对象优先在Eden区分配。如果Eden区放不下这个对象,虚拟机会使用复制算法发生一次Minor GC,清除掉无用对象,同时将存活对象移动到Survivor的其中一个区(fromspace区或者tospace区)。
虚拟机会给每个对象定义一个对象年龄(Age)计数器,对象在Survivor区中每“熬过”一次GC,年龄就会+1。待到年龄到达一定岁数(默认是15岁),虚拟机就会将对象移动到年老代。
如果新生对象在Eden区无法分配空间时,此时发生Minor GC。发生MinorGC,对象会从Eden区进入Survivor区,如果Survivor区放不下从Eden区过来的对象时,此时会使用分配担保机制将对象直接移动到年老代。
1.第一次Yong GC(Minor GC)后,Eden区还存活的对象复制到Surviver区的“To”区,“From”区还存活的对象也复制到“To”区,
2.再清空Eden区和From区,这样就等于“From”区完全是空的了,而“To”区也不会有内存碎片产生,
3.等到第二次Yong GC时,“From”区和“To”区角色互换,很好的解决了内存碎片的问题
Major GC的触发条件:
Major GC又称为Full GC。当年老代空间不够用的时候,虚拟机会使用“标记—清除”或者“标记—整理”算法清理出连续的内存空间,分配对象使用。
4.2.2 老年代:
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC 前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。
当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM (Out of Memory)异常。
4.2.3 永久代
指内存的永久保存区域,主要存放Class 和Meta (元数据)的信息,Class在被加载的时候被放入永久区域,它和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class 的增多而胀满,最终抛出OOM异常。
JAVA8与元数据 :
在Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
类的元数据放入native memory,字符串池和类的静态变量放入java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
4.3 对象分配的一般过程
1、new的对象先放在Eden区,此区有大小限制
2、当创建新对象,Eden空间填满时,会触发Minor GC,将Eden不再被其他对象引用的对象进行销毁,将Eden中剩余的对象移到Survivor S0区,Eden区被清空,再加载新的对象放到Eden区
3、当Eden区又满了,再次触发垃圾回收,此时将Eden区和Survivor S0区中没有被回收的对象,就会放到Survivor S1区
4、再次经历垃圾回收,又会将幸存者重新放回Survivor S0区,依次类推
5、可以设置年龄计数器,每一次幸存年龄+1,默认是15次,超过15次,则会将幸存者区幸存下来的转去老年区:-XX:MaxTenuringThreshold=N进行设置
垃圾回收频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间搜集
Survivor区满了不会触发minor GC,但不代表没有GC。
超大对象eden放不下,就要看Old区大小是否可以放下。old区也放不下,需要FullGC(MajorGC)
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
为什么使用TLAB:Thread Local Allocation Buffer
堆区是线程共享区域,任何线程都可以访问到堆区的共享数据
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
从内存模型而不是垃圾收集的角度,对Eden区域进行划分,JVM为每个线程分配了一个私有缓存区域,包含在Eden空间中
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们将这种内存分配方式成为快速分配策略
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但是JVM确实是将TLAB作为内存分配的首选
- 开发人员通过 -XX:UseTLAB 设置是否开启TLAB空间
- 默认情况下,TLAB空间内存非常小,仅占有整个Eden空间的1%,通过
-XX:TLABWasteTargetPercent 设置TLAB空间所占用Eden空间的百分比大小
- 一旦对象在TLAB空间分配内存失败,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存
4.7 逃逸分析
堆是分配对象的唯一选择吗
随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术,将会导致一些微秒变化,所有对象分配到堆上渐渐变得不那么绝对了。
有一种特殊情况,如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样无需堆上分配,也不需要垃圾回收了,也是最常见的堆外存储技术
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的时间变多。
空间分配担保:jdk7此参数失效
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间,是否大于新生代所有对象的总空间:
- 如果大于,则此次MinorGC是安全的
- 如果小于,则查看-XX:HandlePromotionFailure设置是否允许担保失败
true:
继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
大于,则尝试进行一次MinorGC,但是这次MinorGC依然是有风险的
小于,则改为进行一次FullGC
- false
则改为进行一次FullGC
jdk7此参数失效,规则改为只要老年代的连续空间大于新生代对象总大小,或者历次晋升的平均大小,就会进行MinorGC,否则进行FullGC