为什么要学 JVM?
⚫ 面试的需要:入职 BATJ、TMD、PKQ 等一线大厂不光关注技术的广度,更关注
技术的深度,JVM 技术是大厂面试的必备技能,掌握越深越好
⚫ 中高级程序员、架构师必备技能:架构师每天都在思考如何让我的系统更快,
如何避免系统出现性能瓶颈。单纯的依靠物理机不足以解决问题,分析系统
性能、调优系统瓶颈离不了对 JVM 中内存、垃圾回收、字节码指令、性能监
控工具、调优参数的熟练掌握。
⚫ 精进技术、极客追求:JVM 是 Java 生态的核心价值的体现,垃圾回收算法、
JIT、底层原理值得每个程序员去探索。同时,JVM 作为跨语言的平台,对于
深入理解 Scala、Kotlin、JavaScript、Jython、Groovy 也很有帮助
JVM的架构模型
Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构。
具体来说:这两种架构之间的区别:
- 基于栈式架构的特点
1.设计和实现更简单,适用于资源受限的系统;
2.避开了寄存器的分配难题;使用零地址指令方式分配
3.指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现
4.不需要硬件的支持,可移植性更好,更好实现跨平台
- 基于寄存器架构的特点
1.典型的应用是x86的二进制指令集:比如传统的PC以及Android的Davlik虚拟机.
2.指令集架构则完全依赖硬件,可移植性差
3.性能优秀和执行更高效
4.花费更少的指令去完成一项操作
5.在大部分的情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主。
总结:
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
时至今日,尽管嵌入式平台已经不是Java程序的主流运行平台了(准确来说应该是HotSportVm的宿主环境已经不局限于嵌入式平台了),那么为什么架构更换为基于寄存器的架构呢?
栈:
跨平台性、指令集小、指令多;执行能力比寄存器差
虚拟机的生命周期
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
-
虚拟机的执行
- 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。
- 程序开始执行时他才运行,程序结束时他就停止。
- 执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。
-
虚拟机的退出
有如下的几种情况:
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
- 某线程调用Runtime类或System类的exit方法,或Runtime类的halt
方法,并且Java安全管理器也允许这次exit或halt操作。
- 除此之外,JNI(JavaNative Interface)规范描述了用JNI
Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况。
JVM内存模型
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在这行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。
-
简图
-
详细图
类加载器子系统作用
- 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件开头有特定的文件标识。
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExcutionEngine决定。
- 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串和数字常量(这部分信息是Class文件中常量池部分的内存映射)
类加载器ClassLoader角色
- class file 存在于本地磁盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出N个一模一样的的实例。
- class file 加载到JVM中,被称为DNA元数据模板,放在方法区。
- 在.class文件—》 JVM —》 最终成为元数据模块,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
类的加载过程
public class HelloLoader{
public static void main(String[] args){
System.out.println("谢谢ClassLoader加载我...");
System.out.println("你的大恩大德,我下辈子再报!");
}
}
加载:
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
补充:加载.class文件的方式
- 从本地系统中直接加载
- 通过网络获取,典型场景:Web Applet
- 从zip压缩包中获取,打包好的jar、war格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 由其他文件生成,典型场景:JSP应用
- 从专有数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的防Class文件被反编译后的保护措施
链接:
验证(Verify):
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包括四种验证:
- 文件格式验证:主要按照Class文件16进制背后的秘密文章中的阐述的格式,严格的进行校验。
- 元数据验证:主要是语义校验,保证不存在不符合Java语言规范的元数据信息,如:没有父类,继承了final类,接口的非抽象类实现没有完整实现方法等。
- 字节码验证:主要对数据流和控制流进行分析,确定成行语义是否合法,符合逻辑。
不合法的例子:
操作数栈放置了int类型数据,却当成long类型使用;
把父类对象赋值给了子类数据类型;
. . . . . .
- 符号引用验证:解析阶段发生的验证,当把符号引用转化为直接引用的时候进行验证。这主要是对类自身以外的信息进行匹配性校验。主要包括:
全限定名是否可以找到对应的类;
指定类是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;
校验类,字段和方法的可见性;
. . . . . .
准备(Prepare):
- 为类变量分配内存并设置该类变量的默认初始值,即零值。
- 这里不包含用final修饰的static,因为final在编译的时候就分配了,准备阶段会显示初始化;
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
解析(Resolve):
- 将常量池内的符号引用转为直接引用的过程。
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 符号引用就是一组符号来描述所引用的目标。符号引用字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等;对应常量池中的CONDTANT_Class_info、CONSTANT_Fielder_info、CONSTANT_Methodref_info等。
初始化:
- 初始化阶段就是执行类构造器方法和< clinit >()的过程。
- 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序来执行。
- < clinit >()不同于类的构造器(< init >)是类加载的时候执行的比如静态代码块。(关联:构造器是虚拟机视角下的< init >())
- 若该类具有父类,JVM会保证子类的< clinit >()执行前,父类的< clinit >()已经执行完毕。
- 虚拟机必须保证一个类的< clinit >()方法在多线程下被同步加锁。
局部变量表(Local Variables)
局部变量表也称为本地变量表,是存在栈中的,是线程私有的所以不存在安全问题
关于solt的理解
public class MianTest {
public void test(){
int a=1;
long b=2L;
double c=0;
}
}
1.非static方法默认0是this
2.double和long类型占两个槽,其他都只占1个槽
3.局部变量表所需的容量大小是在编译期确定下来的
4.局部变量表中的变量只能在当前方法调用中有效。当方法调用结束时,随着方法栈帧的销毁,局部变量表也会随之销毁。
双亲委派机制
- java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
- JVM支持两种类型的类加载器,分别是引导类加载器(Bootstrap Classloader),自定义类加载器(Userdefined Classloader)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有继承自Classloader的加载器定义为自定义加载器。
引导类加载器。
启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载器是使用C/C++语言实现的,嵌套在JVM内部。
- 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自ava.lang.ClassLoader,没有父加载器。
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
应用类加载器(系统类加载器,AppClassLoader)
- java语言编写,由sun.misc.LaunchersAppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该加载器是用户编写的程序默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过ClassLoader#getSystemclassLoader() 方法可以获取到该类加载器
为什么要用双亲委派机制?
优势
● 避免类的重复加载
● 保护程序安全,防止核心API被随意篡改
○ 自定义类:java.lang.String
○ 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
双亲委派机制工作原理
1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是先把这个请求委托给父类的加载器去执行;
2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
用户自定义类加载器实现步骤
1.开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现 自己的类加载器,以满足一些特殊的需求
2. 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中
3. 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
沙箱安全机制
防止恶意代码污染java源代码
比如我定义了一个类名为String所在包为java.lang,因为这个类本来是属于jdk的,如果没有沙箱安全机制的话,这个类将会污染到我所有的String,但是由于沙箱安全机制,所以就委托顶层的bootstrap加载器查找这个类,如果没有的话就委托extsion,extsion没有就到aapclassloader,但是由于String就是jdk的源代码,所以在bootstrap那里就加载到了,先找到先使用,所以就使用bootstrap里面的String,后面的一概不能使用,这就保证了不被恶意代码污染
程序计数器(PC Register)
概念:
JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。
作用:
-
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
-
使用PC寄存器存储字节码指令地址有什么用呢?为什么使用PC寄存器记录当前线程的执行地址呢?
● CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
-
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
局部变量表(local variables)
也被称作局部变量数组,或者本地变量表。
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
● 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
● 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的LocalVariableTable数据项中。在方法运行期间是不会改变局部变量表的大小的。
● 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
● 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
说明:
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
每一个栈帧都对应着一个局部变量表,每个方法都是一个栈帧
public class DynamicLinkingTest {
int num;
String info;
public void test1(){
String a="";
info="JVM";
this.test2();
}
public void test2(){
num=2;
}
}
关于Slot的理解
- JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访
问到局部变量表中指定的局部变量值
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量壳中的每一个Slot上
- 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。
- 如果当能顿是由构造方法或者实例方法创建的, 那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排
- 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
- 局部变量表,最基本的存储单元是Slot(变量槽)局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型 (reference),returnAddress类型的变量。
- 在局部变量表里,32位以内的类型只占用一个slot(包括 returnAddress类型),64位的类型)占用两个slot
byte,short,char在存储前被转换位int,boolean也被转换位int(0表示false,非0表示true)。
long和double占据两个Slot
操作数栈(Operand Stack)
- 每一个独立的栈帧 中除了包含局部变量表以外,还包含一个后进先出
(Last-In-First out)的操作数栈,也可以称之为表达式栈 (Expression Stack)。
- 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出
栈。使用它们后再把结果压入栈
- 比如:执行复制、交换、求和等操作
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
- 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
- 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
- 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
- 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。
- 栈中的任何一个元素都是可以任意的Java数据类型。
- 32bit的类型占用一个栈单位深度
- 64bit的类型占用两个栈单位深度
- 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。
动态链接(Dynamic Linking或指向运行时常量池的方法引用)
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接 (Dynamic Linking)。比如:invokedynamic指令
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Svmbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。
方法的调用
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。
这样的方法称为非虚方法。
- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
- 其他方法称为虚方法
虚拟机中提供了以下几条方法调用指令:
- 普通调用指令:
1.invokestatic:调用静态方法,解析阶段确定唯一方法版本
2.invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
3.invokevirtual:调用所有虚方法
4.invokeinterface:调用接口方法
- 动态调用指令:
5.invokedynamic:动态解析出需要调用的方法,然后执行前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedvnamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
关于invokedynamic指令
- JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个
invokedvnamic指令,这是Java为了实现[动态类型语言]支持而做的一种改进。
- 但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助
ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda
表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
- Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不
是对Java语算规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方
法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。
动态类型语言和静态类型语言:
- 动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
- 说的再直白一点就是,静态类型语言是判断变量自身的类型信息:动态类型
语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,
这是动态语言的一个重要特征。
堆(heap)
堆是存储Java创建对象的地方,堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们是共享同一堆空间的。
堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
堆结构组成
jdk1.8的时候jvm有很大的改进:使用元空间(mate space)取代了永久代。虽然元空间逻辑上仍然可以视为方法区的一种实现,但是在jdk1.8的jvm里面却没有给予方法去单独的一块内存区域了(使用直接内存,不在由JVM分配空间)。
在jdk1.7的版本开始,已经开始了一部分去永久代的工作。比如字符串常量池迁移到堆内存中。而1.8则更进一步,把方法区的运行时常量池等信息全部迁移到了本地内存(native memory)之中
Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
● Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
● Tenure generation space 养老区 Old/Tenure
● Meta Space 元空间 Meta
IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。
几乎所有的new 出来的对象都放在Eden区(除了大对象当Eden放不下的时候会直接放到Old区)
- 一个对象在堆内存中的流向:
针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不再永久代和元空间进行收集。
GC(Garbage Collection垃圾回收)
HotSpot垃圾收集器
jdk1.7和1.8默认采用ParallelGC,也就是Parallel Scavenge(新生代)+Parallel Old(老年代)GC。 jdk9采用G1垃圾收集器。
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
(这里说的都是针对HotSport虚拟机)
按照回收区域又分为两大种类型:
- 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC / Old GC):只是老年代的圾收集。
(目前,只有CMSGC会有单独收集老年代的行为)
一种是整堆收集(FullGC),收集整个java堆和方法区的垃圾收集。
Minor GC
Minor GC 是俗称,新生代(新生代分为一个 Eden区和两个Survivor区)的垃圾收集叫做 Minor GC
触发条件:
当 Eden 区的空间耗尽了怎么办?这个时候 Java虚拟机便会触发一次 Minor GC来收集新生代的垃圾,存活下来的对象,则会被送到 Survivor区。
简单说就是当新生代的Eden区满的时候触发 Minor GC (Minor GC会回收Survivor区 s0区、s1区、Eden区,Survivor区 满了的时候不会触发GC,Survivor区是被动的回收)
Minor GC会引发STW(Stop The World),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
Major GC
Major GC 是清理老年代。(只是老年代的垃圾收集)
目前只有CMS GC会有单独收集老年代的垃圾收集
注意:很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
触发条件:
● 老年代空间不足时,会先尝试触发Minor Gc。如果之后空间还不足,则触发Major GC。
● Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长 。
● 如果Major GC后,内存还不足,就报OOM了。
出现了Major GC 经常会伴随至少一次的Minor GC (但非绝对),较少发生,执行速度较慢(MajorGC 的速度一般会比 Minor GC 慢 10倍以上。)
Full GC
收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式
触发条件:
按HotSpot VM的serial GC的实现来看,触发条件是:
- 调用System.gc()时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些。
堆分区的原因
不同对象的生命周期不同。70%-99%的对象是临时对象。
● 新生代:有Eden、两块大小相同的survivor(又称为from/to,s0/s1)构成,to总为空。
● 老年代:存放新生代中经历多次GC仍然存活的对象。
分代的唯一理由就是优化GC性能,如果没有分代,那所有的对象都在一块,GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
对象分配过程
- new的对象先放伊甸园区。此区有大小限制。
- 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃
圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
- 然后将伊甸园中的剩余对象移动到幸存者0区。
- 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会
放到幸存者1区。
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
- 啥时候能去养老区呢?可以设置次数。默认是15次。
可以设置参数:-xx:MaxTenuringThreshold=进行设置。
- 在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理
- 若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常
java.lang.OutOfMemoryError:Java heap sapce
总结:
- 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
- 关于垃圾回收:频繁在新生代收集,很少在老年代收集,几乎不在永久区/元空间收集
内存分配策略(或对象提升(Promotion)规则)
如果对象在Eden出生并经过第 一次MinorGC后仍然存活,并且能被Survivor 容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在 Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中,对象晋升老年代的年龄阈值,可以通过选项-XX:MaxTenuringThreshold来设置
针对不同年龄段的对象分配原则如下所示:
- 优先分配到Eden
- 大对象直接分配到老年代
⚫ 尽量避免程序中出现过多的大对象(指new 出来的对象伊甸园放不下,内存担保原则会直接放到老年代)
- 长期存活的对象分配到老年代
- 动态对象年龄判断
⚫ 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。
- 空间分配担保
⚫ -XX:Handlepr tomotionFailure
为什么有TLAB(Thread Local Allocation Buffer)
● 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。
●由于对象实例的创建过程在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
● 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
什么是TLAB?
-
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
-
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题 同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称 之为快速分配策略。
-
据宋红康所说所有OpenJDK衍生出来的JVM都提供了TLAB的设计。
TLAB的再说明
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为 内存分配的首选。
- 在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间。
- 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
- 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。