java虚拟机
1 意义
- 屏蔽各个硬件平台和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果
2 运行时数据区组成
2-1 线程私有
程序计数器
- 当前线程所执行的字节码的行号指示器:
- 如果正在执行的是java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;
- 如果正在执行natvie方法,计数器值为空(undefined)
作用
- java虚拟机字节码解释器通过改变这个计数器的值来选取下一条要执行的字节码指令
- 没有规定任何outofmemoryerror的情况
虚拟机栈
- 用途:为jvm执行java方法服务
- 编译期间完成分配
结构:栈帧:局部变量表、操作数栈、动态链接、方法出口
- 基本类型变量,(boolean,byte,char,short,int,float,long,double)
- 对象句柄
- 方法参数
- 方法的局部变量
- 两种异常:stackoverflowerror、outofmemoryerror; -xss设置栈大小
本地方法栈
- 用途:为虚拟机使用到的native方法服务
- 两种异常:stackoverflowerror、outofmemoryerror
2-2 线程共有
方法区(自用)
- 用途:用于存储已被jvm加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
运行时常量池
内容:存放编译产生的字面量(常量final)和符号引用
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
特点:运行时常量池相对于class文件常量池的一个重要特征是具备动态性
- java语言并不要求常量一定只有编译期才能产生,运行期间也能将新的常量放入池中
- 异常:(无法扩展)outofmemoryerror
方法区的gc
- 参考垃圾对象的判定
gc堆(可及)
- 目的:存放对象实例和数组
分代处理
- 目的:更好的回收内存和更快的分配内存
新生代
- eden空间
- from survivor空间
- to survivor空间等
- 老年代
- 空间结构:逻辑连续的空间,物理可不连续
- 优先分配tlab(thread local allocation buffer),减少加锁,提高效率
- gc管理的主要区域
异常:如果在堆中没有完成内存分配,并且堆也无法扩展时,会抛出outofmemoryerror
- -xms初始堆大小 -xmx最大堆大小
3 对象的创建
3-1 检查参数是否在常量池中定位到一个类的符号引用;该类是否被加载、解析、初始化过
- 若没有做进行类加载
3-2 若有则分配内存
内存绝对规整
- 用“指针碰撞”来分配内存
内存不规整
- 用“空闲列表”来分配内存
线程安全
对分配内存空间的工作进行同步处理
- 采用cas+失败重试的方式保证更新操作的原子性
每个线程分配一块本地线程分配缓冲区
tlab
- -xx:+/-usetlab
- 3、始化已分配内存为零值(保证类型不赋初值可以使用)
- 4、上面工作完成后,执行init方法按照程序员意愿初始化对象
4 对象创建流程图
5 对象的内存布局
5-1 对象头
- 存储运行时数据
- 存储类型指针
5-2 实例数据
- 是对象真正存储的有效信息
5-3 对齐填充
- 起占位符的作用
6 对象的访问定位
6-1 使用句柄
- 堆中有句柄池,存储到实例数据和类型数据的指针;栈中的引用指向对象的句柄地址
优点
- reference中地址相对稳定;
- 对象被移动(gc时)时只会改变句柄中的实例数据指针
6-2 直接指针
-
栈中的引用直接存储对象地址,到方法区中类型数据的指针包含在对象实例数据中
优点
- 访问速度快,节省了一次指针定位的开销
7 oom异常
7-1 虚拟机栈和本地方法栈溢出
线程请求栈深度大于最大深度stackoverflowerror
- 设置-xss128k,在单线程下,通过不断调用递归方法。
新线程拓展栈时无法扩展出现outofmemoryerror错误
- 不断创建新线程,并让创建的线程不断运行
- -xss
7-2 方法区和运行时常量池溢出
java.lang.outofmemoryerror后会跟permgen space
- 不断创建新的字符串常量,并添加到list中
- -xx:permsize和-xx:maxpermsize
7-3 堆溢出
java.lang.outofmemoryerror:java heap space
内存泄漏
- 通过不断创建新对象,并放入list中,保证gcroots到对象之间路径可达
- 内存溢出
- -xms -xmx
7-4 本机直接内存溢出
- 在heap dump文件中没有明显异常
- -xx
8 垃圾对象的判定
8-1 对象的引用
强引用
- 存在就不回收
软引用
将要发生内存溢出之前
- 实现缓存
弱引用
下一次垃圾回收
- 回调函数中防止内存泄露
虚引用
对对象的生存时间没有影响
- 能在这个对象被收集器回收时收到一个系统通知
8-2 引用计数法
- 难以解决对象之间相互循环引用的问题
8-3 根搜索算法
- 从gc roots向下搜索建立引用链;一个对象如果到gc roots没有任何引用链相连时,证明对象不可用
gc roots
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
8-4 堆中垃圾回收过程
- 1、如果对象在进行可达性分析后发现没有与gc roots相连接的引用链,那它将会被第一次标记
- 2、断对象是否有必要执行finalize()方法,(没有覆盖,被调用过,都没有必要执行),放入f-queue队列
- 3、放入f-queue中,进行第二次标记
- 4、被拯救的移除队列,被两次标记的被回收
8-5 方法区中垃圾回收
废弃常量
- 没有任何一个对象引用常量池中的“abc”常量
无用的类(满足条件可以被回收,非必然)
- 1、该类所有的实例都已经被回收
- 2、加载该类的加载器被回收
- 3、该类对应的javalang.class对象没有在任何地方被引用,无法通过反射访问该类的方法
9 垃圾回收算法
9-1 标记-清除算法
- 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
- 效率问题,标记和清除两个过程的效率都不高
- 空间问题,产生大量不连续的内存碎片,连续内存不足会再次触发gc
9-2 复制算法
- 将内存等分,每次用一块,当这块内存用完了,就将活着的对象复制到另一块,然后把前者清空
- 对象存活率较高时就要进行较多的复制操作,效率将会降低
- 空间利用率低
9-3 标记-整理算法
- 所有存活的对象移向一端,然后直接清理掉端边界以外的内存
9-4 分代收集算法
新生代
- 复制算法
老年代
- 标记-整理或标记-清除
9-5 hotspot算法
枚举根结点
- 当执行系统停顿下来后,并不需要一个不漏的检查完所在执行上下文和全局的引用位置,在hotspot的实现中,使用一组称为oopmap的数据结构来存放对象引用
安全点
- 在这些特定的位置,线程的状态可以被确定
位置
- 方法调用指令
- 循环跳转指令
- 异常跳转指令
中断方式
抢占式
- gc发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑在安全点上
主动式
- 设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起
安全区域
- 背景:线程sleep状态或者blocked状态的时候,无法响应jvm中断,走到安全的地方,jvm也不能等他们,这样就无法进行gc
- 安全区域是指在一段代码中,引用关系不会发生变化,这个区域中的任何地方开始gc都是安全的。
10 垃圾收集器
10-1 serial收集器
特点
- 新生代收集器
- 采用复制算法
- 单线程收集
- 进行垃圾收集时,必须暂停所有工作线程,直到完成
应用场景
- 是hotspot在client模式下默认的新生代收集器
- 简单高效(与其他收集器的单线程相比)
- 对于限定单个cpu的环境来说,serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
参数设置
- "-xx:+useserialgc":添加该参数来显式的使用串行垃圾收集器;
10-2 parnew收集器
特点
- 新生代收集器
- 采用复制算法
- 除了多线程外,其余的行为、特点和serial收集器一样
应用场景
- server模式下,parnew收集器是一个非常重要的收集器
- 单个cpu环境中,不会比serail收集器有更好的效果,因为存在线程交互开销
参数设置
- "-xx:+useconcmarksweepgc":指定使用cms后,会默认使用parnew作为新生代收集器;
- "-xx:+useparnewgc":强制指定使用parnew;
- "-xx:parallelgcthreads":指定垃圾收集的线程数量,parnew默认开启的收集线程与cpu的数量相同;
10-3 parallel scavenge收集器
特点
- 新生代收集器
- 采用复制算法
- 多线程收集
- cms等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间; 而parallel scavenge收集器的目标则是达一个可控制的吞吐量(throughput)
应用场景
- 高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间
- 当应用程序运行在具有多个cpu上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互
参数设置
- "-xx:maxgcpausemillis"控制最大垃圾收集停顿时间
- "-xx:gctimeratio" 设置垃圾收集时间占总时间的比率
- "-xx:+useadptivesizepolicy"
10-4 serial olc收集器
特点
- 针对老年代
- 采用"标记-整理"算法(还有压缩,mark-sweep-compact)
- 单线程收集
应用场景
- 主要用于client模式
在server模式中
- 在jdk1.5及之前,与parallel scavenge收集器搭配使用(jdk1.6有parallel old收集器可搭配)
- 作为cms收集器的后备预案,在并发收集发生concurrent mode failure时使用
10-5 parallel old收集器
特点
- 针对老年代
- 采用"标记-整理"算法
- 多线程收集
应用场景
- jdk1.6及之后用来代替老年代的serial old收集器
- 特别是在server模式,多cpu的情况下
- 在注重吞吐量以及cpu资源敏感的场景,就有了parallel scavenge加parallel old收集器的"给力"应用组合
参数设置
- "-xx:+useparalleloldgc":指定使用parallel old收集器
10-6 cms收集器
特点
- 针对老年代
- 基于"标记-清除"算法(不进行压缩操作,产生内存碎片)
- 以获取最短回收停顿时间为目标
- 并发收集、低停顿
- 需要更多的内存(看后面的缺点)
应用场景
- 与用户交互较多的场景
- 希望系统停顿时间最短,注重服务的响应速度
- 以给用户带来较好的体验
- 如常见web、b/s系统的服务器上的应用
参数设置
- "-xx:+useconcmarksweepgc":指定使用cms收集器
运行过程
- 初始标记
- 并发标记
- 重新标记
- 并发清除
缺点
- 对cpu资源非常敏感
- 无法处理浮动垃圾,可能出现"concurrent mode failure"失败
- 产生大量内存碎片
10-7 g1收集器
特点
并行与并发
- gc收集线程并行
- 用户线程与gc线程并发
- 分代收集,收集范围包括新生代和老年代
- 空间整合:结合多种垃圾收集算法,空间整合,不产生碎片
- 可预测的停顿:低停顿的同时实现高吞吐量
应用场景
- 面向服务端应用,针对具有大内存、多处理器的机器
- 最主要的应用是为需要低gc延迟,并具有大堆的应用程序提供解决方案
运行过程(不计remembered set操作)
初始标记
标记gc root能直接关联到的对象
- 需要停顿用户线程
并发标记
对堆中对象可达性分析
- 并发执行
最终标记
修正并发标记中因用户线程运行发生改变的标记记录
- 需要停顿线程
筛选回收
对region的回收价值和成本排序,根据参数指定回收计划
- 可以并发
参数设置
- "-xx:+useg1gc":指定使用g1收集器
- "-xx:initiatingheapoccupancypercent":当整个java堆的占用率达到参数值时,开始并发标记阶段;默认为45
- "-xx:maxgcpausemillis":为g1设置暂停时间目标,默认值为200毫秒
- "-xx:g1heapregionsize":设置每个region大小,范围1mb到32mb;目标是在最小java堆时可以拥有约2048个region
10-8 内存分配和回收策略
对象优先在eden分配
- 大多数情况下,对象在新生代eden区中分配,当eden区没有足够空间进行分配时,虚拟机将发起一次minor gc
大对象直接进入老年代
- 所谓大对象,就是需要大量连续内存空间的对象,最典型的大对象就是那种很长的字符串以及数组
- 长期存活的对象将进入老年代
动态对象年龄判定
- 如果在survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以进入老年代,无须等到maxtenuringthreshold中要求的年龄
空间分配担保
- minor gc之前,jvm检查老年代最大连续空间是否大于新生代所有对象的空间,成立则确保minor gc安全
- 不成立,参看参数handlepromotionfailure是否允许担保失败,允许则检查老年代最大连续空间是否大于历次晋升的对象的平均大小,大于则尝试minor gc
- 否则,进行full gc
- 内存管理机制
- 垃圾收集器
- 内存分配策略
11 虚拟机类加载机制
11-1 类加载的时机
声明周期
- 加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中验证、准备、解析3个部分统称为连接
以下情况对类进行初始化
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
- 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.methodhandle实例最后的解析结果bef_getstatic、bef_putstatic、bef_invokestatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
11-2 类加载的过程(5)
加载
完成3件事
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口
类的来源
- 从zip包中读取,这很常见,最终成为日后jar、ear、war格式的基础
- 从网络中获取,这种场景最典型的应用就是applet
- 运行时计算生成,这种场景使用得最多的就是动态代理基础
- 由其它文件生成,典型场景就是jsp应用,即由jsp文件生成赌赢的class类
- 从数据库中读取,这种场景相对的少些,例如有些中间件服务器可以选择把程序安装到数据库中来完成程序代码在集群间的分发
验证
- 验证是连接阶段的第一步,这一阶段的目的就是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
验证项
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
- 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都讲在方法区汇总进行分配
解析
- 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
解析动作分类
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化
- 类初始化是类加载过程中的最后一步,这时才真正开始执行类中定义的java程序代码
11-3 类加载器
- 虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为类加载器
- 比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个class文件,被同一个虚拟机加载,只要它们的类加载器不同,那这两个类就必定不相等
11-4 双亲委派模型
分类
启动类加载器
- 这个类加载器使用c++语言实现,是虚拟机自身的一部分
其他类加载器
- 这些类加载器由java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.classloader
- java内存模型与线程
- 线程安全