Java虚拟机
Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
是什么?
模仿计算机硬件结构(PC,内存管理,处理器)的软件
功能?
翻译官,执行java字节码(将Java字节码转换为系统可处理的机器码)
作用?
具备跨平台的特性,可以在不同的设备上运行
JRE 和 JDK
JVM(Java Virtual Machine ):解释class文件
JRE(Java Runtime Env):运行时环境,运行Java程序需要的环境;JVM + lib。
JDK(Java Development Kit):包含 JRE,还有javac,java等工具,用于Java程序的开发。
整体工作原理
java源文件 --> javac --> class文件(字节码文件)
class文件 --> 双亲委派机制(类加载)–> 数据放置在方法区中(类信息,常量池信息物理上放置在堆中)
执行引擎
- 类加载过程
- 双亲委派机制
- 运行时内存
类加载
class文件结构
- 魔数:是否是java可处理的class文件
- 版本号:class文件版本
- 常量池计数器:常量的数目
- 常量池:存放常量(字面量和符号引用)
- 访问标记:public protected private deflaut
- 类索引:本类的全限定名
- 父类索引:父类的全限定名
- 接口计数器:接口数目(多接口)
- 接口数据区:多个接口的信息
- 字段计数器:字段的数目
- 字段数据区:字段的信息(访问权限、类型、名称)
- 方法计数器:方法数目
- 方法数据区:方法的信息(访问)
- 属性计数器:属性数目
- 属性数据区:对类、字段、方法的描述(比如方法体中的字节码、方法抛出的异常列表,final定义的常量值等等信息)
类加载过程
**整体过程:**加载 – 连接(验证-准备-解析)-- 初始化
-
加载
双亲委派机制进行类的加载
-
连接
-
验证
语法错误查验
-
准备
内存分配,初始化 0
-
解析
字符引用 -> 直接引用
-
-
初始化
clinit方法(静态方法块之类)
双亲委派机制
- 启动类加载器
- 扩展类加载器
- 应用程序类加载器
- 自定义类加载器
简单的说:先让父加载器进行处理,当父加载器不能完成该任务时候,自己才去加载。
再双亲委派机制中,父加载器会先进行加载
运行时内存区域
整体划分
线程私有:PC、本地方法栈、栈
线程共用(也是GC的区域):堆、方法区
PC (Program Counter)
程序计数器:记录当前线程下一步需要执行的指令地址,从而实现代码的流程控制(switch,for,break, exception)
- 线程私有,生命周期与线程的生命周期保持一致。
- 字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
JVM栈
java方法在执行的时候会创建一个栈帧放入到栈中,栈帧中存放了:局部变量表、操作数栈、动态链接、方法返回地址
方法调用次数由什么决定?
栈的大小决定:栈越大,方法嵌套调用次数越多。
局部变量数目:对于一个方法 / 函数,其参数和局部变量越大,其栈帧越大,则会占用更多的栈空间
局部变量表(Local Variables)
特点 | 解释 |
---|---|
局部变量 | 只是在当前方法调用中有效 GC root节点(垃圾回收的根节点) |
基本存储单元 | 变量槽 slot,一个 slot 为 32 位 |
类型 | 基本数据类型 、 引用数据类型 |
基本存储单元(slot):在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型,64位的类型(long和double)占用两个slot。
- byte 、 short 、char在存储前被转换为 int,
- boolean也被转换为 int,0 表示false,非0 表示true。
- long和double 则占据两个slot。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sBjUyoNT-1638360510270)(Java虚拟机.assets/image-20210528091637754-16287497439661.png)]
this对象位置:如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的Slot处,其余的参数按照参数表顺序继续排列。
操作数栈(Operate Stack)
特点 | 解释 |
---|---|
实现 | 数组 |
目的 | 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间![]() |
放置的数据类型 | 32bit的类型占用一个栈单位深度 64bit的类型占用两个栈单位深度 |
访问 | 栈(后入先出) |
初始 | 方法调用,生成栈帧,这时候产生的操作数栈是空的 |
最大深度 | 每一个操作数栈都有一个栈深度用于存储数值 最大深度在编译期决定,保存在code属性中,为max_stack的值 |
返回值 | 方法的返回值会被压入当前栈帧的操作数中,并更新PC中下一条需要执行的字节码指令 |
Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
动态链接(Dynamic Linking)
指向运行时常量池的方法引用
特点 | 解释 |
---|---|
功能 | 指向运行时常量池的方法的引用 该引用指向运行时常量池,表明该栈帧对应哪一个方法 (多态中指向子类的方法,实现多态的特性) |
存在原因 | 1、 .java文件编译为.class文件时,所有变量和方法引用都可作为符号引用放置在.class文件的常量池。 2、符号引用是全限定名(本质上就是一个字符串) 3、动态链接指向的是:直接引用(方法的符号引用的转换) |
非虚方法:如果方法在编译期就确定了具体的调用版本
-
这个版本在运行时是不可变的。这样的方法称为非虚方法。
-
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
子类对象的多态性的使用前提:1. 类的继承关系,2. 方法的重写
虚拟机中提供了以下几条方法调用指令:
- 普通调用指令:
- invokestatic: 调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
- invokevirtual: 调用所有虚方法
- invokeinterface: 调用接口方法
- 动态调用指令:
- invokedynamic:动态解析出需要调用的方法,然后执行,lambda表达式会产生invokedynamic指令
重写的本质:
-
到操作数栈顶的第一个元素所执行的对象的实际类型,记作 C。
这个对象 是调用对应实例方法的 对象
-
如果在 类型C 中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验(public。。。)
- 如果通过则返回这个方法的直接引用,查找过程结束
- 如果不通过,则返回 java.lang.IllegalAccessError 异常
-
否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
-
如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常。
方法返回地址(Return Address)
方法正常退出或者异常退出的定义
特点 | 解释 |
---|---|
方法结束 | 1. 正常执行完成 2. 未处理异常,非正常退出 |
在方法退出后,都返回到该方法被调用的位置。
-
方法正常退出时,调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
-
异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
异常处理表:实现方法时候,写的可以处理的异常,会形成异常处理表
在栈中可能的异常
由于栈可以设定 固定大小 还是 动态扩展大小:
- 固定栈大小,线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverFlowError异常
- 可动态扩展,无法申请到足够内存时,抛出一个OOM异常(OutOfMemoryError)
// 设置栈大小的指令:
-Xss256k
本地方法栈
服务native本地方法(非java代码的接口),功能与栈类似调用本地方法本地接口,叫做JNI(Java Native Interface)
JVM堆
主要存放 new 的对象,字符串常量池也是放在堆中
JDK1.7 以上版本,Class对象也是放在堆中
内存分配 & 对象在不同分区的转移
-
对象优先在Eden区域分配
Eden剩余内存不足?触发 minorGC
对象大于 Eden 区域总大小? 直接转到老年区
-
大对象直接进入到老年代
-XX : PretenureSizeThreshold 参数(Serial与),超过该参数的对象直接放入到老年代
-
长期存活的对象
年龄大于 -XX: MaxTenuringThreshold之后进行老年代
-
动态对象年龄判定
S 区中 相同年龄 对象大于 S区的1/2 ?
无需等到 -XX:MaxTenuringThreshold,可以直接进入到老年代
年龄从小到大进行累加,到达一定年龄段之后,累加和大于 S * TargetSurvivorRatio 之后,就将这个年龄段以上的对象进行晋升到老年代。
-
空间分配担保
发生时间:Minor GC之前
目的:检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
- 在发生Minor GC之前,JVM会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
- 老年代最大连续可用空间 ? 新生代所有对象总空间
- -XX:HandlePromotionFailure是否允许担保 (JDK7 之后不再使用)
- 老年代最大连续可用空间 ?历代S区到Old区对象大小平均值
- 在发生Minor GC之前,JVM会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
GC类型
**部分收集 **和 整堆收集
- 部分收集:比如年轻代的收集,老年代的收集。
- 新生代收集 Young GC / Minor GC: Eden、S区的垃圾收集器
- 老年代收集 Old GC / Major GC :老年代收集
- MCS GC会有单独收集老年代的行为,其他很少只是使用老年代收集,
- 一般都是直接使用 Full GC
- 混合收集:同时收集年轻代和老年代,如:G1收集器
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
GC触发
- 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC;
- 对老年代GC称为Major GC;
- 而Full GC是对整个堆和方法区来说的;
Minor GC触发条件
- eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
Full GC触发条件
- System.gc()方法的调用
- 老年代空间不足(年龄增长后到老年代的对象,大对象,)
- 方法区空间不足
- 通过Minor GC后进入老年代的对象平均大小 大于 老年代的可用内存
- 由Eden区、From Space区向To Space区复制时,对象大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
对象分配过程:TLAB
Thread local Allocation Buffer (TLAB)
从内存模型的角度来说:对Eden进行了划分,JVM给线程在Eden区分配了一个私有缓存区域
这样做的优势:
- 避免内存分配过程中的线程安全问题!(分配同一块内存)
- 选项
-XX:UseTLAB
设置是否开启TLAB空间,默认Eden的 1%- 一旦对象在TLAB空间分配内存失败时,JVM就会通过CAS保证内存分配的原子性,从而直接在Eden空间中分配内存。
当然,尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
#官网说明:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial #查看所有的参数的默认初始值
-XX:+PrintFlagsFinal #查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms<N> #初始堆空间内存(默认为物理内存的1/64)
-Xmx<N> #最大堆空间内存(默认为物理内存的1/4)
-xmn<N> #设置新生代的大小。(初始值及最大值)
-XX:NewRatio #配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio #设置新生代中Eden和s0/s1空间的比例
-XX:MaxTenuringThreshold #设置新生代垃圾的最大年龄
-XX:+PrintGcDetails #输出详细的GC处理日志
#打印gc简要信息:
-XX:+PrintGc
-XX:+PrintGcDetail
-verbose:gc
-XX:HandlePromotionFailure #是否设置空间分配担保
堆并不是对象分配 唯一选择
随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术被提出。
逃逸分析
一个对象在栈中产生之后,方法结束之后在其他地方不存在引用,那么则认为该对象没有逃逸。反之。方法结束之后其他地方存在引用,则认为是发生了逃逸。
-XX:+DoEscapeAnalysis #开启逃逸分析
-XX:-DoEscapeAnalysis #关闭逃逸分析
-XX:+PrintEscapeAnalysis #显示分析结果
逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
//没有逃逸,可以分配到栈上,随着方法执行的结束,栈空间就被移除。
public void my_method(){
V v=new V();
// use v
//............
v=null;
}
//StringBuffer对象经过返回在外部存在引用。发生了逃逸
public static StringBuffer createStringBuffer (String s1,String s2){
StringBuffer sb = new StringBuffer();
sb.append (s1);
sb.append (s2);
return sb;
}
//修改为栈上分配
public static String createstringBuffer (String s1,String s2){
StringBuffer sb = new StringBuffer () ;
sb.append (s1) ;
sb.append (s2);
return sb.tostring();
}
逃逸分析优化
-
栈上分配
对象没有发生逃逸时,对象会被分解为标量,分配到栈中,相当于栈的局部变量,栈帧出栈时候被销毁。
减少GC的压力,提高性能
-
锁清除
线程同步锁是非常牺牲性能的,当JIT编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。
例如:StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作。
-
标量替换
标量:不能被进一步分解(基本数据类型 , 对象引用)
聚合量:能被进一步分解的量就是聚合量(对象)
将对象转化为标量
JDK1.8 默认开启标量替换,建立在逃逸分析上
方法区
类的元信息(描述信息)放置在方法区。
类型信息,成员变量信息,方法描述
静态变量、常量、类信息(构造方法、接口定义)运行时的常量池都存在方法区中、但是实例变量存在堆内存中、和方法区无关
永久代为什么被元空间所替代:
JVM对永久代设置了一个固定的大小上限,并且无法对其进行调整,可能会导致java.lang.OutOfMemoryError*;因此提出使用直接内存的元空间,其不会报***java.lang.OutOfMemoryError***。
-XX:MaxMetaspaceSize标志可以设置最大元空间的大小,其默认为unlimited(只受系统内存限制)。元数据区
永久代被替代后,方法区移至 Metaspace,字符串常量移至堆。
直接内存:
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。
本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。
java的NIO也会用到直接内存,
其中:分配直接内存、内存映射等
其他划分
常量
- 整型:
二进制:0b01101100、0B10110101 八进制:0342 十进制:198 十六进制:0x25AF
- 浮点数:
2e3f 3.6d 0f 3.84d 5.022e+23f
- 字符常量
‘a’ ‘1’ ‘&’ ‘\r’ ‘\u0000’
- 字符串常量
“HelloWorld" “123" “We come \n XXX” "”
- 布尔常量
true和false
- null 常量
null
常量池
- 静态常量池(Class文件中的常量池)
- 运行时常量池
- 字符串常量池
Class常量池(静态常量池)
.class
文件中,存在常量计数器和常量数据区,这两者构建了静态常量池。
运行时常量池
放置运行时常量,从静态常量池转换而来,并且静态常量池中的符号引用会转换为直接地址。
字符串常量池
功能:位于堆中,放置字符串常量。
new String(“Alvin”) 产生了几个对象
2个,一个在字符串常量池中,一个在堆中
intern方法
str.intern()
让字符串放置在字符串常量池中。
若str
已经在字符串常量池中,直接返回其引用;不在常量池中,先放入常量池中,然后再返回其引用
对象
对象结构
对象头
对象创建
- 类加载
- 内存分配
- 两种方式(指针碰撞,空闲列表)
- 并发分配问题(TLAB、CAS重试)
- 初始化 0
- 设置对象头
- 类指针
- 对象hash信息
- GC分代年龄
- …
- 执行init方法
- 父类变量初始化
- 父类语句块
- 父类构造函数
- 子类变量初始化
- 子类语句块
- 子类构造函数
内存分配
三种策略:
- 优先在Eden区
- 大对象直接进入老年代
- 长期存活的对象进入老年代
分配方式:
-
指针碰撞
条件:内存整齐(没有内存碎片)的情况下(复制,标记整理算法)
原理:被占用的内存在一边,未被占用的在另一边,从两者之间的分界位置开始从未使用的内存区域方向移动获取需要的内存大小即可
GC收集器:Serial,ParNew
-
空闲列表
条件:内存凌乱(存在内存碎片)的情况下;(标记-清除算法)
原理:JVM维护一个列表,该列表会记录可用的内存,分配的时候找一块足够大的内存划分给对象实例
GC收集器:CMS
并发问题:
问题描述:多个线程进行创建对象时候,内存如何分配?
- JVM在Eden区会给每个线程分配一小块区域 TLAB,线程会优先使用TLAB的内存
- 当线程的 TLAB 区域用尽之后,使用 CAS 保证内存分配的原子性
引用问题 (强、软、弱,软,引用队列)
引用层级关系
四大引用与引用队列
强引用
**GC角度:**就算出现OOM也不会对该对象进行回收
**场景:**是我们最常见的引用,将对象赋给一个引用变量,这个引用变量就是强引用。当一个对象是强引用的时候,它处于可达状态,不会被GC回收掉。
强引用也是造成内存泄漏的原因之一
软引用
**GC角度:**内存够用则不进行回收,不够用则进行回收
**场景:**内存敏感程序中,比如高速缓存中就会用到
弱引用
**GC角度:**引用的对象存在强引用则不会被回收,反之,引用的对象不存在强引用则会被回收
场景: ThreadLocal的键就是使用的弱引用
虚引用
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
**GC角度:**如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
**场景:**虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。
PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。换句话说,设置成引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
引用队列
Java中引用在GC的时候,在这期间(GC之前)都会被放在引用队列ReferenceQueue中保存一下。
**作用:**我们希望当一个对象被gc掉的时候通知用户线程,进行额外的处理时,就需要使用引用队列了。ReferenceQueue即这样的一个对象,当一个obj被gc掉之后,其相应的包装类,即ref对象会被放入queue中。我们可以从queue中获取到相应的对象信息,同时进行 额外的处理 。比如反向操作,数据清理等。
所以虚引用和引用队列常常一起使用
相关场景介绍
软引用场景
假如有一个应用需要读取大量的本地图片:
如果每次读取图片都从硬盘读取则会严重影响性能,
如果一次性全部加载到内存中又可能造成内存溢出。
此时使用 软引用 可以解决这个问题:
设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。Map<String, SoftReference<Bitmap>>imageCache = new HashMap<String, SoftReference<Bitmap>>();
知道WeakHashMap吗?
WeakHashMap的键是弱引用
引用总结
GC 垃圾回收
对象已死
引用计数
记录对象的引用信息,当对象只有 0 引用的时候,对象被认为是可回收对象。
难以解决循环引用的情况
可达性分析
将对象分为可达对象与不可达对象,可达对象是仍然存在引用的对象,不可达对象是垃圾(应当被回收的对象)
GC Roots节点
- 虚拟机栈中的对象
- 本地方法栈引用的对象
- 方法区中静态对象,常量池中对象
- JVM内部引用对象(Class对象,,常驻异常,ClassLoader)
- synchronized所持有的对象
GC算法
GC收集器
收集器
名称 | N/O | 特点 | 垃圾回收算法 |
---|---|---|---|
Serial | New | STW期间只有 一 个GC线程 | 复制 |
ParNew | New | STW期间存在 多 个GC线程 | 复制 |
Parallel Scavenge | New | STW期间存在 多 个GC线程 目标是提高吞吐量(用户线程时间)/ (STW + 用户线程时间) 可设置**-XX:+UseAdptiveSizePolicy**参数:自适应调节策略 | 复制 |
Serial | Old | - | 标记-整理 |
ParNew | Old | - | 标记-整理 |
CMS | Old | 1. 算法:标记清除算法 2. 目标:最少STW时间 3. 步骤:初始标记 - 并发标记 - 重新标记 - 并发清除 4. 缺点:CPU敏感;会出现浮动垃圾,从而导致另一次 Full GC;内存碎片问题 5. 优势:第一个真正意义上的并发收集器 6. 范围:老年代 | 标记-清除 |
G1 | All | 1. 算法:分代处理,标记整理 2. 目标:面向服务端 3. 步骤:初始标记 - 并发标记 - 重新标记 - 筛选回收 4. 缺点:Minor GC效率变低,需要扫描整个堆空间;内存占用更高 5. 优点:用户可预测GC停顿时间;极少的垃圾碎片;划分离散的区域Region 6. 范围:全堆 | 标记-整理 |
N/O : 新生代/老年代
Parallel Scavenge(自适应调节策略。):-XX:+UseAdptiveSizePolicy :虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
**STW:**Stop The World
**吞吐量:**用户线程运行时间 / (用户线程运行时间+GC运行时间)
G1:
筛选回收:
- 根据回收价值和成本进行排序
- 根据用户期望的GC时间指定回收计划
Region的优势:
- 不同Region具有优先级,保证G1收集器在有限时间内获取尽可能高的收集效率
- 是实现可预测停顿时间的基础
CMS
CMS的每一个步骤的功能
-
初始标记:
整个过程是 STW
- 标记 GC root可达的老年代对象;
- 遍历新生代对象,标记可达的老年代对象
-
并发标记:
并发的标记 GC root 可达对象
-
重新标记:
并发执行过程中可能发生:
-
新生代对象晋升到老年代,
-
大对象直接分配在老年代,
-
更新老年代对象引用关系,等情况
这些对象都需要进行重新标记,否则有些对象就会被遗漏、发生漏标的情况
-
-
并发清理:
并发的清理不可达的对象
CMS的参数调优
-
-XX:UseConcMarkSweepGC
-
-XX:CMSInitiatingOccupancyFraction =n
CMS时并发执行的,那么在CMS并发标记过程中,应用线程仍然会进行空间分配(创建新的对象),为了保证在CMS阶段可以创建对象,那么就必须预留一部分空间。
换句话说:CMS不会再老年代满的时候才开始收集。相反他会尝试更早的开始收集,以避免回收完成之前,堆没有足够空间进行分配!
默认情况下:68%占用率的时候久会开始进行回收
此参数可以设置该 阈值!!!
-
-XX:+UseCMSInitiatingOccupancyOnly
我们用该标志来命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期
只有当我们充足的理由(比如测试)并且对应用程序产生的对象的生命周期有深刻的认知时,才应该使用该标志。
G1图示
CMS与G1的区别:
特征 | CMS | G1 |
---|---|---|
范围 | 老年代 | 全堆 |
算法 | 标记-清除 | 标记-整理 |
STW | 最小STW为目标 | 可预测垃圾回收时间 |
碎片 | 可能导致大量垃圾碎片 | 很少产生垃圾碎片 |
过程 | 初始标记 - 并发标记 - 重新标记 - 并发清理 | 初始标记 - 并发标记 - 重新标记 - 筛选回收 |
分区 | 将堆分为新生代和老年代(连续划分) | 使用Region划分,Region之间是离散的, 不同Region可能属于不同的代 |
合作 | 可与新生代Serial和ParNew合作使用 | 不需要 |
一些GC的参数
-XX:+UseAdptiveSizePolicy:Parallel Scavenge的自适应调节策略。
-XX:ParallelGCThreads=N :数字N表示启动多少个GC线程
cpu>8 N=5/8
cpu<8 N=实际个数
GC触发
- 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC;
- 对老年代GC称为Major GC;
- 而Full GC是对整个堆和方法区来说的;
Minor GC 触发条件
- eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
Full GC 触发条件
- System.gc()方法的调用
- 老年代空间不足(年龄增长后到老年代的对象,大对象,)
- 方法区空间不足
- 通过Minor GC后进入老年代的对象平均大小 大于 老年代的可用内存
- 由Eden区、From Space区向To Space区复制时,对象大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
JVM调优
JVM 命令
- 通用指令
- -x指令:编译、解释、混合-X:mixed
- -XX指令:+PrintGCDetails, 堆内存,栈内存大小之类
OOM(OutOfMemoryError)
- 堆空间溢出
- 元空间受到直接内存的约束,元空间溢出
- 过度使用直接内存
- 创建太多线程
- GC频率过高,但效率太低
java.lang.StackOverflowError:栈空间溢出
OOM1. 堆空间溢出
java.lang.OutOfMemoryError :Java Heap space
一直创建对象,堆内存不够用了
OOM2. 元空间溢出(元空间大小受到本地内存限制)
java.lang.OutOfMemoryError :Metaspace
模拟Metaspace空间溢出,我们不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的
不停的加载新的类到JVM,就会报这个错误
OOM3. 过度使用直接内存
java.lang.OutOfMemoryError :Direct buffer memory
写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/0方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
Native堆 :直接内存
***ByteBuffer.allocate(capability)***第一种方式是分配 JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较馒
***ByteBuffer.allocateDirect(capability)***第2种方式是分配 OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快
ByteBuffer.allocateDirect(capability) 分配直接内存:可能将本地内存全部占用,再次尝试分配本地内存就会出现OutOfMemoryError:Direct buffer memory。那程序直接崩溃了。
OOM4. 创建太多线程出现的异常
OutOfMemoryError:unable to create new native thread
导致原因:
- 你的应用创建了太多线程了,一个应用进创建多个线程,超过系统承载极限
- 你的服务器并不允许你的应用程序创建这么多线程, Linux系统默认允许单个进程可以创建的线程数是1024个,你的应用创建超过这个数量,就会报java.lang.OutOfMemoryError: unable to create new native thread
OOM5. GC频率过高的异常
java.lang.OutOfMemoryError :GC overhead limit exceed
GC回收时间过长,连续多次GC 都只回收了不到2%的极端情况下才会抛出。
怎么算作时间过长?
超过 98% 的时间在进行GC操作但是只是回收不到 2% 的内存
不抛出异常会发生什么?
一直进行无效的GC,CPU使用率居高不下