jvm
什么是垃圾?
没有任何引用指向的对象。
-没有被引用的对象
-多个对象互相引用,但没有栈内存中指向这几个对象
垃圾是如何产生的?
无效对象
java C C++垃圾回收对比
java自动回收,编程上简单,系统不容易出错,手动释放内存容易出问题。
-忘记回收
-多次回收 会造成回收之后重新使用这块内存后,此块内存又被使用。
##垃圾定位算法
-引用计数算法
被引用一次+1 减少一次引用-1
-根可达算法
根元素:线程栈变量,静态变量,常量池,JNI指针
STW
Stop-The-World 在执行垃圾回收算法时,java应用程序的其他线程都被挂起(除了垃圾回收器之外),
是java的一种全局暂停现象,全局停顿,所有java代码停止,native代码可以执行,但不能与JVM交互。
分代理论建立
1.弱分代假说:大多数对象都是朝生夕死的
2.强分代假说:经历了多次垃圾回收仍然存活的对象,越不容易被回收掉(年龄)
3.跨代引用假说:跨代引用相对于同代引用只占少数
GC常见垃圾回收算法
标记清除
是一种最基础的垃圾回收算法,分为标记,清楚两部分,首先标记已经消亡的对象,
然后对有标记的对象进行清除。
特点:
①效率问题,标记和清楚时效率都不高。
②会产生大量不连续的内存碎片,如果要存入较大的对象时,没有连续内存可供存储,
只能提前触发GC垃圾回收。
使用位置:
适合老年代进行垃圾收集,比如CMS收集器就是采用该算法进行回收。
标记整理
先标记,后整理,首先标记已经消亡的对象,然后让存活的对象向另一端移动,然后直接清理掉边界以外的对象。
特点:不会产生内存碎片,但整理会花费一定时间,因为移动时,如果多个线程执行,需要线程同步。
使用地方:适合老年代进行垃圾收集,parallel Old(针对parallel scanvange gc的) gc和Serial old收集器就是采用该算法进行回收的。
标记复制
将内存分为相同大小的两块,一块使用,一块闲置,当这块内存用完后,
GC会将此块内存中还存活的对象复制到另一块内存中,然后将已使用的那块内存清空。
特点
①不会出现内存碎片。
②始终会有一般的内存处于闲置状态。
使用位置:
适合新生代区进行垃圾回收。serial new,parallel new和parallel scanvage收集器,就是采用该算法进行回收的。
复制算法改进思路
IBM研究表明,新生代中的对象98%都时朝生夕死的,所以不需要1:1的比例划分内存空间,
而是将内存分为一个Eden区域和两个survivor区域,每次使用的是Eden区和一个survovor区,
当需要清理时,会将Eden区和survivor区中还存活的对象复制到另一个survivor区域中,
然后两个survivor区域进行交换。HOTSPOT虚拟机的Eden和survivor比例为8:1。
特点:
需要老年代进行分配担保,如果第二块survivor内存不足,需要对老年代进行垃圾回收,然后储存新生代的对象。
分代算法
1.新生代
Eden:survivor:survivor = 8:1:1
2.老年代
区域大小 新生代:老年代 = 1:3
分代算法回收过程
新生代中
1.YGC回收之后大多数的对象会被回收,剩余进入s0,Eden、s1清空
2.再次YGC,Eden+s0存活的进入s1,Eden、s0清空
3.当新生代中对象年龄足够老(一般为15),进入老年代
4.如果一次存活对象很多,s区域装不下,则直接进入老年代
老年代中
1.顽固分子
2.老年代满了FGC Full GC
GC Tuning(Generation)
1.尽量减少FGC
2.MinorGC = YGC
3.MajorGC = FGC
分代名词
1.部分收集(partial GC)
① 新生代收集(Minior GC/Young GC)
② 老年代收集(Major GC/Old GC)
2.混合收集(Mixed GC)
回收整个新生代和部分老年代
3.整堆回收(FULL GC)
回收整个堆和方法区
方法区垃圾回收
1.在方法区中不再使用的常量
2.在方法区中不在使用的类型
常用的垃圾回收器
1.Serial 年轻代 单线程,串行回收 采用标记复制算法
2.Parallel Scavenge 年轻代 并行回收 采用标记复制算法 吞吐量优先回收器 自适应策略
常用参数:
-XX:MaxGcPauseMillis:最短GC造成STW停顿时间
-XX:GCTimeRatio:允许最大垃圾回收时间
-XX:UseAdaptiveSizePolicy:这个参数激活后,需要手动指定新生代大小 —Xmn,
eden和survivor比例-XX:SurvivorRatio,晋升老年代对象大小 -XX:PretenurSizeThreshold
3.ParNew 配合CMS的并行回收 年轻代 serial多线程版本 标记复制算法
4.serialOld 老年代 单线程,串行回收 采用标记整理
5.paralleOld 老年代 多线程,并行回收 采用标记整理
6.Concurrent mark sweep 并发标记清除(CMS)垃圾收集器 ,最少停顿时间为目的 采用标记清楚算法
CMS 老年代 并发的 垃圾回收和应用程序同时运行,降低STW的时间(200ms)
CMS 会比并行GC多占用10%~20%空间
7.G1 JDK9发布
应用程序暂停时,移动对象
8.ZGC JDK11发布
目标:低延迟,可伸缩性,易用性。暂停时间2ms左右
JDK12时,添加了对并发类卸载的支持,从而达到应用程序在卸载未使用的类的同时继续运行,而不是暂停应用程序后,卸载未使用的类。
9.Shenandoah JDK12发布
应用程序进行过程中,可以放置对象
java 1.8默认是parallel scavennge 和 paralleold 结合
CMS GC运行过程
1.初始标记(init mark) 需要STW
标记一下GCRoots能直接关联到的对象,速度很快
2.并发标记 concurrent mark
从GCRoots直接关联的对象 遍历整个对象树的过程,与用户线程并发执行,不需要STW
3.重新标记 remark 需要STW
修正因并发标记阶段改变状态的部分对象
4.并发清除 concurrent sweep
删除掉已经标记为死亡的对象,不需要移动存活对象,与用户线程并发执行,不需要STW
常用垃圾收集器組合
serial+serialOld 吞吐量優先
parNew+CMS 低延迟
JVM内存模型
私有区域 程序计数器 虚拟机栈(JVM栈) 本地方法栈(native栈)
程序计数器
程序计数器是一块很小的内存,当前线程执行的字节码行号指示器,记录虚拟机正在执行字节指令地址,是线程私有的,各个线程互相独立存储,互不影响
注意:线程执行的是java方法,则程序计数器记录虚拟机字节码指令的地址,如果是底层方法(native修饰),那么计数器为空。
程序计数器所在内存是虚拟机中唯一没有OutOfMemoryError的区域
虚拟机栈
栈描述的是java方法执行的内存模型,线程私有,生命周期与线程相同,编译期确定大小。
每个方法被执行的时候都会创建一个栈帧,栈帧存储局部变量表,操作栈,动态链接,方法出口等信息
每一个方法被调用的过程就对应一个栈桢在虚拟机中从入栈到出栈过程
方法运行结束(出栈),栈中存放的基本数据类型和变量和局部变量的引用,都会自动释放。
本地方法栈
作用与虚拟机栈作用类似,服务于native方法
虚拟机栈和本地方法栈区别
虚拟机栈是服务JVM执行的java方法,本地方法栈是服务JVM执行的native方法
共享区域 堆 方法区
堆
堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制
所有对象实例及数组都要在堆上分配内存(随着JIT编译器的发展和逃逸分析技术的成熟,这个说法不太准确)
无论成员变量、局部变量、类变量,他们指向的对象都存储在堆内存中。
方法区
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。
运行时常量池:存放编译期生成的各种字面量和符号引用。
JVM内存参数设定
-Xms 堆初始内存大小
-Xmx 最大堆内存大小
-Xss 单个线程栈大小
-XX:NewSize 初始新生代堆内存大小
-XX:MaxNewSize 初始新生代堆最大内存大小
—XX:MetaspaceSize 元数据区大小
-XX:MaxMetaspaceSize 元数据区最大值
JVM类加载过程
加载-验证-准备-解析-初始化-使用-卸载
加载
1.根据类的全限定名获取此类的二进制字节流
2.将该二进制字节流所代表的静态存储结构转化为方法区内运行时数据结构
3.在方法区中生成这个类的java.lang.Class对象,作为程序访问这个类常量,静态变量,编译器即时编译的代码的入口。
验证
验证是连接的第一步,且工作量在jvm类加载子系统中占了相当大的一部分
目的:为了确保二进制字节流中的代码符合JVM的要求和规范,并且不会危害虚拟机自身安全,验证阶段直接决定jvm是否会收到恶意代码的攻击
验证阶段可以自定义关闭,关闭可以缩短JVM类加载时间
参数 -Xverify:none
检验过程包含:1.文件格式验证 2.元数据验证 3.字节码验证 4.符号引用验证
- 文件格式验证
内容:验证字节流是否符合Class文件格式的规范、以及是否被当前版本的虚拟机处理
目的:保证输入的字节流能正确地解析并存储于方法区之内,且格式上符合描述一个Java类型信息的要求。只有保证二进制字节流通过了该验证后,
它才会进入内存的方法区中进行存储,所以后续3个验证阶段全部是基于方法区而不是字节流了 - 元数据验证
内容:对字节码描述的信息语义进行分析,以保证符合java语言规范的要求
目的:对类的元数据信息进行语义校验,保证不存在不符合java语言的元数据信息 - 字节码验证
内容:对类的方法体进行校验分析,保证被校验的方法在运行时不会危害虚拟机
目的:通过数据流和控制流分析,确定程序语义合法的、符合逻辑的 - 符号引用验证
内容:对类自身以外(如常量池中的各种符号引用)的信息进行匹配校验
目的:保证解析动作可以正常执行,如果无法通过虚拟机符号引用验证则会出现java.lang.IncompatibleClassChangeError异常的子类
注意:该验证发生在虚拟机将符号引用转化为直接引用的时候,即解析阶段
准备
- 为已在方法区中的类静态变量分配内存
- 为已分配内存的静态变量赋值(类型的初始值(0、false、null等))
- 注意:若是final修饰的值但若被final修饰的常量如果有初始值,那么在编译阶段就会将初始值存入constantValue属性中,在准备阶段就将constantValue的值赋给该字段(此处将value赋为123).
解析
解析就是将常量池中的符号引用转化为直接引用的过程
JVM会根据需要来判断,是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析
解析动作:有七类符号及其对应在常量池的七种常量类型
- 类或接口(CONSTANT_Class_info)
- 字段(CONSTANT_Fieldref_info)
- 类方法(CONSTANT_Methodref_info)
- 接口方法(CONSTANT_InterfaceMethodref_info)
- 方法类型(CONSTANT_MethodType_info)
- 方法句柄(CONSTANT_MethodHandle_info)
- 调用点限定符(CONSTANT_InvokeDynamic_info)
举个例子,设当前代码所处的为类D,把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,解析过程分三步:
若C不是数组类型:JVM将会把代表N的全限定名传递给D类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作。一旦这个加载过程出现了任何异常,解析过程就宣告失败。
若C是数组类型且数组元素类型为对象:JVM也会按照上述规则加载数组元素类型
若上述步骤无任何异常:此时C在JVM中已成为一个有效的类或接口,但在解析完成前还需进行符号引用验证,来确认D是否具备对C的访问权限。如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常
初始化
在准备阶段,类变量已经经过一次初始化了,在这个阶段,则是根据程序员通过程序制定的计划去初始化类的变量和其他资源。这些资源有static{}块,构造函数,父类的初始化等。
堆内存逻辑结构
对象的分配过程
JVM 名词解释
JIT 即时编译器
可以把java的字节码、需要被解释的指令,转换成可以直接发送给处理器的指令程序
全限定名
一个类的包名+类名
符号引用
以一组符号来描述所引起的目标,可以是任何字面量,只要使用时候能无歧义的定位到目标即可
符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式,与虚拟机无关
直接引用
可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄
与虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不同