垃圾收集器和内存分配策略
在程序计数器、虚拟机栈和本地方法栈中,由于这些和线程的创建直接相关,随线程创建而生,随线程退出而灭,所以这部分的内存回收是确定的。但是Java堆和方法区,一个接口中多个实现类需要的内存可能不一样,一个方法的多个分支需要的内存也可能不一样,只有程序运行期间才知道会创建那些对象,这部分内存的分配和回收是动态的。
判断对象是否存活
引用计数法
给对象添加引用计数,当对象的引用计数为0时,说明该对象没有被使用,可以被回收。实现简单,执行效率高。python使用的就是这种方法。但是,对于相互引用,可能会导致无法回收,所以Java虚拟机并不使用这种方法。
可达性分析算法
当一个对象到GC Roots没有任何引用链的时候,则判定该对象是可回收对象。
在java中,可作为GC Roots的对象包括
1 | |
引用
强引用
软引用
SoftReference
描述一些还有用但是并非必须的对象,在内存溢出异常之前,将会回收这些对象
弱引用
WeakReference
只能生存到下一次垃圾回收之前
虚引用
PhantomReference
对象被回收的时候收到一个系统通知
回收方法区
主要是回收废弃常量和无用的类,
运行时数据区
- 方法区
- 堆
- 虚拟机栈
- 本地方法栈
- 程序计数器:当前线程执行的字节码行号指示器,如果当前线程正在执行native方法,这个计数器的值为Undefined
虚拟机栈: 保存Java方法执行的内存模型,每个方法在执行时都会创建一个栈帧,存储局部变量表、操作数栈、动态连接、方法出口等信息,其中,局部变量表存储着编译期可知的基本数据类型、对象引用、returnAddress类型。 如果线程请求的栈深度大于虚拟机所允许的深度,将StackOverflowError;虚拟机栈可以动态扩展,如果扩展时无法请求到足够的内存,将OutOfMemory
本地方法栈:与虚拟机栈相似,执行的是Native方法
Java堆: 一般来说,所有的对象实例和数组都在此分配,但也有栈上分配、标量替换等技术,这个区也是垃圾收集器管理的主要区域,如果堆中没有内存完成实例分配,将OutOfMemory
方法区:存储虚拟机加载的类信息、常量、静态变量、jit编译后的代码等,如果没有内存完成实例分配,将OutOfMemory
运行常量池:是方法区的一部分,存放编译期生成的字面量和符号引用
直接内存:nio相关
对象的创建
new指令创建对象
- 检查参数能否在常量池中定位到一个类的符号引用,并检查这个类是否已被加载、解析、初始化,如果没有,先执行相关过程
- 从java堆中划分出区域,为对象分配内存: 指针碰撞、空闲列表,线程安全:同步处理,CAS和失败重试;本地分配缓冲TLAB,-XX:+/-UseTLAB
- 将内存空间初始化为0值,不包括对象头
- 相关设置,设置Object Header信息
对象头 Object Header 实例数据 Instance Data 对齐填充 Padding
对象头包含:
- 自身运行时数据,哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳
- 类型指针,指向类元数据的指针
- 数组长度,如果对象是数组
实例数据 代码中定义的各种类型字段 虚拟机分配策略:longs/doubles、ints、shorts/chars、bytes/booleans、oops 相同宽度的总是分配到一起,父类的定义的变量出现在子类之前,CompactFields=true,那么也会出现子类交窄的变量插入父类的空隙之中
对象访问定位
- 句柄访问: 堆中划分出句柄池,存放着对象实例数据的指针和对象类型数据的指针
- 直接指针访问:
堆参数: -Xms20m -Xmx20m 栈参数: -Xss128k 方法区参数: -XX:PermSize=10m -XX:MaxPermSize=10M
对象优先在Eden区中分配,当没有足够的空间,那么就Minor GC(新生代GC) 大对象直接进入老年代 长期存活的对象进入老年代,默认15岁 动态判定年龄
垃圾回收
引用计数
Java中很少采用,无法解决循环引用问题
可达性分析法
当一个对象不可达GCRoots节点的时候,该对象可被回收
可以作为GCRoots的对象包括以下几种
- 虚拟机栈中引用的对象
- 方法区中静态类型属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
要回收对象时候,还需要俩次标记:
- 当对象可达性分析后,对象没有与GC Roots相引用的链,那么对象会被第一次筛选,当对象没有覆盖finalize方法、或者finalize方法已经被虚拟机栈调用过,虚拟机将这俩种情况都是为没有必要执行
- 如果这个对象有必要执行finalize,那么这个对象会被放到F-Queue队列中,之后,由Finalizer线程去执行
方法区回收:
回收废弃常量:判断有没有引用 回收类:
- 类的所有实例被回收
- 加载类的ClassLoader被回收
- 类对应的java.lang.Class对象没有在任何地方被引用
- 参数 -Xnoclassgc可以控制是否回收
回收算法
标记清除:效率不高,产生碎片
复制算法:划分成俩快,当一边用完,将活对象复制到另一边,回收对象,新生代的回收一般采用这个算法,hotspot默认划分80%的eden区和俩个10%的Survivor,当一次回收时,一个Survivor不够存放活的对象,就挪至老年代
标记整理:老年代回收算法
hotspot相关实现
枚举根节点
OopMap存放着哪些地方存放着引用 安全点safepoint,程序只有在安全点才停顿下来gc,是否具有让程序长时间执行的特征,
如何让gc发生时,所有线程都跑到安全点上停顿:主动式中断,让各个线程轮询中断标志,如果标志为真,则中断挂起自己
安全区域:当线程sleep或者blocked,无法响应jvm的中断请求,走到safepoint挂起,需要用safe region: 一段代码中引用关系不会变化,当程序走出safe region时,需要确保gc已完成
收集器
- Serial
-
CMS
- 最短回收停顿为目标
- 基于标记清除
- 步骤
- 初始标记,stop the world
- 并发标记
- 重新标记 stop the world
- 并发清除
- 缺点
- 对cpu资源敏感
- 无法处理浮动垃圾
- 标记清除,产生内存碎片
- G1
- 面向服务端
- 并行与并发
- 分代收集
- 标记整理
- 可预测的停顿
- 将Java堆划分成大小相等region,跟踪每个region的垃圾堆积的价值大小,每次根据允许的收集时间,回收价值最大的region
- 步骤
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
监控工具
- jps
- jstat
- jinfo
- 实施查看和调整虚拟机各项参数
- jmap
- 内存映像工具
- jhat
- 虚拟机堆转存储快照分析工具
- jstack
- HSDIS
- jit生成代码反汇编
- jConsole
- VisualVM
Class文件结构
一组以8位字节为基础单位的二进制流,没有分隔符
- 头4个字节 魔数 CAFEBABE
- 2个字节 次版本号
- 2个字节 主版本号
- 2个字节 常量池容量计数值
- 某些字节 常量池存放字面量和符号引用,使用javap -verbose test.class可以查看常量
- 2 个字节 访问标志,class是类还是接口、是否是public、是否是abstract、…
- 2个字节类索引、2个字节父类索引
- 某些2个字节集合,接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
字节码指令集
操作数只有一个字节,多数指令只有操作码
iload用于从局部变量表中加载int型数据到操作数栈中
加载和存储指令
将数据在栈帧中的局部变量表和操作数栈之间来回传输
iload istore
运算指令
iadd ladd isub imul idiv ineg ishl
类型转换指令
i2b i2c f2i
对象创建和访问指令
new newarray anewarray getfield putfield
操作数栈管理指令
pop pop2 dup dup2 swap
控制转移指令
ifeq ifne ifnull if_icmpne
tableswitch lookupswitch
方法调用和返回指令
异常处理指令
同步指令
虚拟机类加载机制
加载的时机
加载 –> 验证 –> 准备 –> 解析 –> 初始化 –> 使用 –> 卸载
验证、准备、解析统称为连接
初始化:
- new、getstatic、putstatic、invokestatic时,如果类没有初始化,那么就初始化
- java.lang.reflect
- 初始化一个类,会先初始化父类
- 虚拟机启动时,用户需要执行一个主类
- 动态语言支持时,如果一个java.lang.invoke.MethodHandle最后解析的结果REF_getstatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有初始化,那么就初始化
类加载过程
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的金泰存储结果转化方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
准备阶段:
为类变量分配内存并设置类变量的初始值,在方法区内
解析阶段:
将常量池内的符号引用替换为直接引用的过程,
初始化: 执行java代码
<clinit>方法由编译器自动收集类中的所有的类变量的赋值动作和静态语句块中的语句合并产生的,静态语句块只能访问到定义在之前的变量,定义在后的变量,可以赋值,但是不能访问
类加载器:
如果相同的类,是不同的类加载器加载的,那么这俩个类,是不相等的
从虚拟机来讲,有俩种类加载器,一种是启动类加载器,使用C++实现,是虚拟机的一部分,另一种就是所有其他的类加载器,java语言实现,独立于虚拟机外部,全部继承自java.lang.ClassLoader
双亲委派模型:
当一个类加载器接收到一个类加载请求,先把这个委派给父加载器完成,只有当父加载器无法处理,那么就自己处理
例外:
- 当前的自定义类加载器,重写findClass而不是loadClass,
- 线程上下文类加载器,JNDI
- 满足热更新等动态需求,
虚拟机字节码执行引擎
栈帧
是虚拟机运行时数据区中的虚拟机栈的栈元素,存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息
- 局部变量表
- 是一组变量值存储空间,存放方法参数和方法内部定义的局部变量
- 容量以变量槽为最小单位
- 局部变量表的槽会重用,所以”把不适用的对象手动赋值为null“
- 操作数栈
- 动态连接
- 方法返回值
方法调用
方法调用中的目标方法在class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将部分符合引用转化为直接引用:方法在程序真正运行之前就有一个可确定的调用版本,运行期不可改变,编译期可知,运行期不变,主要包括静态方法和私有方法
- invokestatic √
- invokespecial √
- invokevirtual 特例:final修饰的方法 √
- invokeinterface
- invokedynamic 运行时动态解析出调用点限定符所引用的方法
分派
1 | |
其中,Human是变量的静态类型,编译期可知,Man是变量的实际类型,运行期可知,虚拟机重载的时候,是通过参数的静态类型为判定依据,叫静态分派,
问题: 字面量不需要定义 结果: 重载的结果可能不唯一
动态分派:根据运行期根据实际类型确定方法执行版本的分派
动态单分派、静态多分派
优化: 虚方法表
支持动态语言:
java.lang.invoke.MethodHandle
invokedynamic
tomcat
- CommonClassLoader common
- CatalinaClassLoader server
- SharedClassLoader shared
- WebappClassLoader WebApp/WEB-INF
编译期优化
运行期优化
Java内存模型
每个线程拥有自己的工作内存,所有的”变量”存放在主内存,工作内存中保存了该线程使用的变量的副本,线程对变量的操作必须在工作内存
内存交互操作
- lock
- unlock
- read
- load
- use
- assign
- store
- write
volatile
- 保证变量对所有线程可见性,不保证线程安全,读到了之后,可能被修改
- 禁止指令重排优化
原子性
synchronized,对应的字节码指令是monitorenter和monitorexit,对应的虚拟机操作就是lock和unlock
可见性
volatile
synchronized 对一个变量unlock之前,必须先把此变量同步回主内存中,
final
有序性
本线程内观察,所有的操作都是有序的;另一个线程观察一个线程,所有操作都是无序,指令重排序、工作内存与主内存同步延迟
先行发生原则
线程
使用内核线程
程序一般不会直接使用内核线程,而是使用轻量级进程,与内核线程一对一
用户线程
不需要系统内核支持,所有的线程操作都是用户处理
用户线程加轻量级进程
N:M
java线程
sun jdk使用的是一对一轻量级进程
调度
- 协同式
- 线程执行完后,通知系统切换到另一个线程
- 抢占式
- 系统分配执行时间,java采用的方式
状态转换
- 新建
- 运行
- 无限等待
- 限期等待
- 阻塞
- 结束
线程安全
- 不可变:只要不可变对象被正确的创建,没有this引用逃逸,
- 绝对线程安全:
- 相对线程安全: 大部分java的线程安全类都是这种,对对象的单独的操作都是线程安全的,但是对特定顺序的连续调用,就需要额外同步手段
- 线程兼容:通过调用端使用正确的同步手段保证对象在并发环境中可以安全的使用
- 线程对立:
实现方法
- 互斥同步:
- 多线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用
- 临界区、互斥量、信号量都是实现互斥的主要方式
- synchronized编译后分别形成monitorenter和monitorexit这俩个字节码指令,monitorenter执行,首先获取对象的锁,如果没被锁,或者当前线程已经拥有锁,那就把锁的计数器加1,monitorexit就会减1,如果失败,那就阻塞等待
- java.util.concurrent中的重入锁RentrantLock,
- 等待可中断
- 支持公平锁,先入先出
- 绑定多个条件
- 非阻塞同步:
- 乐观锁,操作和冲突检测,需要硬件指令集的发展,`cas v a b,当内存位置v的值是a的时候,那么将v处的值替换成b,否则就不更新,总是反汇v的旧值a,存在ABA问题,但是不怎么影响
- 无同步
锁优化
- 自旋锁和自适应的自旋锁
- 请求锁的线程,如果请求不到锁,那么先忙等待一会儿,而不是挂起,适合锁持续时间不是很长的情况
- 锁消除
- 利用逃逸分析技术,判定一段代码的堆上的所有数据不会逃逸出去被其他线程访问,那么就把它们当作栈数据对待
- 轻量级锁
- 偏向锁
- 当锁对象第一次被线程获取的时候,虚拟机把对象头中的标志位设置为01,偏向模式,同时,使用cas操作,把获取到这个锁的线程id记录在对象的Mark World中,如果cas操作成功,那么持有这个偏向锁的线程以后每次进入这个锁相关的同步块的时候,虚拟机可以不再进行任何同步操作