JVM整理
2021-03-23 15:14:29 7 举报
AI智能生成
有问题,欢迎指正
作者其他创作
大纲/内容
垃圾回收机制
判断对象是否存活
1.引用计数法
如果对象计数变为0,表明可以被回收
无法解决问题: 如果多个对象之间相互引用,那么就无法处理
2.可达性分析
通过GC Roots来进行判断,如果一个对象可通过链路分析到达GC Roots,那么就是存活状态
可作为GC Root的对象
虚拟机栈中所引用的对象
方法区中类静态属性引用的对象
方法区中常量池对象引用
本地方法JNI
虚拟机内部的引用,包括基本数据类型的class对象,一些异常
同步锁(synchornized)持有的对象
如何标记对象死亡
至少需要经历两次标记
1.第一次标记过程
如果被标记它无引用关系,那么会进行判断,是否执行finalize(类似C语言的析构函数)
不执行
1.未覆盖finalize方法
2.已经执行过一次
2.第二次标记过程
会将第一次标记的对象放入一个由虚拟机创建的F-GC队列(低调度)中
如何逃脱
1.在finalize方法中重新挂上一个对象引用(方法只能被执行一次,第二次之后操作都是无效)
逃脱不了的话,基本上就是被回收操作了
备注:finalize不必过多关注,他不保证方法执行完毕,只是会执行该方法
四种引用类型
1.强引用
传统的引用赋值,类似于new操作,只要关系还在,就不会被垃圾回收
2.软引用
在内存不足的情况下,垃圾回收会清理的对象
可以创建于缓存操作
3.弱引用
每次垃圾回收会清理的对象
4.虚引用
无法通过虚引用来取得一个对象实例,仅在被回收时,发送一个通知
垃圾收集
提出理论
弱分代假说---新生代
绝大部分对象都是朝生夕灭
强分代假说---老年代
越熬过多次垃圾回收机制,就越难以消灭
名词整理
PartialGC (局部GC操作)
Minor GC(新生代GC)
MajorGC(老年代GC)
MIXED GC(混合GC)--只有G1收集器会有这种操作
FULL GC(对整个java堆和方法区的GC操作)
算法整理
1.标记--清除算法(最基础)
对内存中回收对象进行标记,之后进行回收
缺点
1.如果大量对象需要回收,那么标记清除次数随着对象数量级大而大
2.容易产生大量碎片,当虚拟机创建连续的大对象时,找不到空间分配,又会触发垃圾回收机制
2.标记--复制算法(新生代使用)
对可使用内存进行一次划分,只是用其中的部分,当要进行垃圾回收时,将那些未被标记的部分复制到另一部分中
使用起来高效率,但是会有部分内存空间会被浪费
3.标记--整理算法(老年代使用)
与标记--清除算法类似,只不过整理操作是通过指针操作向同一端方向操作
缺点:因为更新引用地址,所以所有线程操作都会短暂暂停,就是所谓的STW(stop-the-world)现象
垃圾收集器
Serial(新生代使用)
单线程,导致其他工作线程会出现STW现象
SerialOld(老年代使用)
单线程,使用标记--整理算法
ParNew(新生代使用)
多线程垃圾回收方式
- CMS
标记清除算法,分为四个步骤
1.初始标记,会出现STW,标记GCROOT关联的对象
2.并发标记,开始遍历整个GCROOT,标记没引用对象
3.重新标记,会出现STW,对步骤2中修改的对象引用一次处理
4.并发清除
Parallel Scavenge(新生代使用)
多线程并行,采用标记复制算法,吞吐量优先为目的,用户线程出现STW
Parallel Old(老年代使用)
多线程并发,标记整理算法
G1(通用)
jdk9以上开始执行,将内存划分为不同的区域,以回收的速率能够跟的上对象分配的速率为目的,并不是完全清理整个内存为目的而设计
垃圾收集器并发和并行概念
并行
多条垃圾线程同时工作,默认此时的用户线程是等待状态
并发
垃圾线程和用户线程都在工作,用户线程也能相应请求,不过垃圾处理的吞吐量会有一定影响
额外知识:CMS和ParNew收集器新老年代的晋升及回收策略
1.新生代分为Eden区,Survivor区的from和to区,按照8:1:1
2.对象创建优先分配到eden区,大对象直接放入老年代里
3.GC开始时,eden区的存活对象放入到to区,from区存活的对象根据年限,分配到老年代以及to区
4.如果复制到to区的内存不够,则让老年代进行分配担保
后端编译与优化
即时编译器
判定热点代码
被多次调用的方法
被多次执行的循环体
热点探测方式
基于采样的热点探测
周期性探测各个线程的栈顶,判断哪些方法经常出现在栈顶
基于计数器的热点探测
为每个执行方法创建一个计数器
虚拟机类加载机制
class文件结构
“Class文件”并非特指某个存在于
具体磁盘中的文件,而应当是一串二进制字节流,无论其以何种形式存在,包括但不限于磁盘文件、
网络、数据库、内存或者动态产生等
具体磁盘中的文件,而应当是一串二进制字节流,无论其以何种形式存在,包括但不限于磁盘文件、
网络、数据库、内存或者动态产生等
魔数
版本号
常量池
字面量
符号引用
访问标志
类索引,父类索引,接口索引等集合
字段表集合
方法表集合
访问标志
名称索引
描述符索引
属性表集合
属性表集合
类加载生命周期
加载
1.根据全类名获取二进制流数据
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中创建代表他的class对象,作为方法区中该类的访问入口
验证
1.文件格式验证
2.元数据验证
3.字节码验证
4.符号引用验证
准备
为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初
始值的阶段
始值的阶段
解析(不一定按照这个顺序执行,为了符合动态绑定特性)
是Java虚拟机将常量池内的符号引用替换为直接引用的过程
初始化
初始化类变量值和其他资源(同一个类加载器下,一个类只会被加载一次)
初始化的必备6种情况
1.遇到new、getstatic、putstatic或invokestatic这四条字节码指令
new关键字
静态类型字段
调用类型的静态方法
2.反射调用
3.初始化类时,父类没有被初始化时,触发父类的初始化
4.虚拟机启动,需要指定一个main类,如SpringBoot的Application类
5.使用动态代理机制
6.jdk8以上的default关键字修饰,如果实现类初始化,那么该接口在之前就要进行初始化
使用
卸载
类加载器(两个类进行判断相等是由是否同一个加载器加载某,才能判断这个类是不是一样)
启动类加载器
扩展类加载器
jdk9之后变为平台类加载器
应用程序类加载器
自定义类加载器
双亲委托模型
当一个类收到加载请求的时候,他自己本身不会执行加载操作,而是抛给他的父类加载器,如果父类完成不了这个操作的时候,自身才会进行家在请求
java内存区域
运行时数据区域
1.程序计数器(线程私有)
存储指令,告知线程当前应该做什么操作
2.java虚拟机栈(线程私有)--数据结构为栈帧
生命周期与当前线程相同
描述java方法执行的线程内存模型,每个方法执行,都会创建一个虚拟机栈,用来存储局部变量表,操作数,动态连接,方法出口等。执行流程就相当于一个入栈到出栈的过程
局部变量表
存放了各种已知的基本数据类型,对象引用指针或者是对象访问句柄
线程所请求的栈大于虚拟机设置的栈的深处,会抛出stackoverflowException
如果栈允许动态扩容,当栈扩展到无法申请所需的内存时,则会抛出outofMemoryException错误
3.本地方法栈
与java虚拟栈功能类似,只不过调用的是本地的方法,不收虚拟机限制
同2一样也会出现同样异常
4.java堆(所有线程共用数据)
几乎所有的对象创建都是在这里进行分配内存操作
jdk1.7以上将 字符串常量池和class和静态变量放置在了堆中
5.方法区(线程共用)
1.7之前
永久代
1.8之后
元空间
只存储类型信息等描述信息,并且放在了直接内存中
运行时常量池
存放各种已知定义类版本,字段,方法,接口等描述信息
常量池表,存放编译器生成的各种字面量和符号引用
直接内存
对象访问定位
直接指针访问
句柄访问
对象内存布局
对象头
markword
哈希码
25bit
分代年龄
4bit
锁标志位
2bit
GC标志
1bit
类型指针,即通过该指针确定对象是哪个类的实例
实例数据
对齐填充
填充为8的倍数
内存溢出和泄露以及虚拟机监控工具
内存溢出
程序申请内存时,内存不足
产生原因
堆溢出
栈溢出
方法区和运行常量池溢出
本地内存溢出
内存泄漏
程序申请内存时,无法回收已申请的内存空间
原因
长生命的对象持有短生命对象的引用
连接未关闭
变量作用域不合理
内部类持有外部类
hash值改变
jdk工具
jps:虚拟机进程工具
jstat:虚拟机统计信息监视工具
jinfo:Java配置信息工具
jmap:Java内存映像工具
jhat:虚拟机堆转储快照分析工具
jstack:Java堆栈跟踪工具
java线程内存模型
主内存与工作内存
主内存
所有变量都存储在这
工作内存
从主内存读取的变量,为其创建副本,所有操作都是在工作内存中进行,最后在回显至主内存中
工作线程无法读取到别的线程的变量副本操作
内存交互操作
lock
unlock
read
作用主内存,从主内存将数据传输到工作内存中
load
作用工作内存,从read操作中读取到的数据,在工作内存中创建副本
use
assign(赋值)
store
作用工作内存,将变量值传入到主内存中
write
将store操作的变量数据存储到主内存中
原子性,可见性与有序性
原子性
基本类型数据的访问,几乎是原子性的(long和double除外)
可见性
当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改
volatile
有序性
线程内表现为串行的语义
指令重排序
为了解决各个操作之间执行速度不一致
先行发生原则
程序次序规则
一个线程内,按照控制流顺序
传递性
如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出
操作A先行发生于操作C的结论
操作A先行发生于操作C的结论
管程锁定规则
一个unlock操作先行发生于后面对同一个锁的lock操作
volatile变量规则
对一个volatile变量的写操作先行发生于后面对这个变量
的读操作
的读操作
线程启动规则
线程中断规则
线程终止规则
线程的实现方式(不指java线程)
内核线程实现(1:1)
由操作系统决定,实现线程上下文切换
用户线程实现
不需要内核去关心,但是用户程序需要自己进行线程执行,销毁等操作
混合实现
Java线程实现
“主流”平台上的“主流”商用Java虚拟机的线程模型普遍都被替换为基于操作
系统原生线程模型来实现,即采用1:1的线程模型
系统原生线程模型来实现,即采用1:1的线程模型
java线程调度
协同式线程调度
在此多线程系统中,线程的执行时间由线程本身控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。
优势
实现简单,由于线程要把自己事情干完才会进行线程切换,切换操作对线程自己是可知的,所以没什么线程同步问题
缺点
线程执行时间不可控,如果一个线程编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里
抢占式线程调度
在此多线程系统中,每个线程将由系统来分配时间,线程的切换不由线程本身决定
优势
线程执行时间是系统可控的,也不会出现一个线程导致整个进程阻塞的问题。Java使用的线程调度方法就是这个
java线程安全与锁优化
java线程安全
不可变
只要一个不可变的对象被正确地构建出来(即没有发生this引用逃逸的情况),那其外部的可见状态永远都
不会改变,永远都不会看到它在多个线程之中处于不一致的状态。“不可变”带来的安全性是最直接、
最纯粹的
不会改变,永远都不会看到它在多个线程之中处于不一致的状态。“不可变”带来的安全性是最直接、
最纯粹的
绝对线程安全(java中的并发类库基本上不是绝对线程安全)
定义其实是很严格的,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”可能需要付出非常高昂的,
甚至不切实际的代价
甚至不切实际的代价
相对线程安全
保证对这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性
线程兼容
对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对
象在并发环境中可以安全地使用
象在并发环境中可以安全地使用
线程对立(避免使用)
线程安全的实现方法
互斥同步(属于一种悲观的并发策略,其总
是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题)
是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题)
synchronized关键字
非公平锁
重入锁(ReentrantLock),ReentrantLock与synchronized相比增加了一些高级功能,主要有以下三项:等待可中断、可实现公平锁及锁可以绑定多个条件
等待可中断
指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改
为处理其他事情
为处理其他事情
公平锁
多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平
锁则不保证这一点
锁则不保证这一点
通过设置布尔参数可实现公平锁,但会急剧影响吞吐量
锁绑定多个条件
非阻塞同步(乐观策略)
使用CAS策略(简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值)
会出现ABA问题,在事务里也可以被叫做幻读
无同步方案
锁优化
是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行
效率
效率
自旋锁
指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,jdk高版本之后将其优化为自适应自旋,如果锁经常被快速释放,那么循环时间会自适应设置设长
锁消除
一些代码要求同步,但是对被检测到不可能存在共享
数据竞争的锁进行消除
数据竞争的锁进行消除
锁粗化
虚拟机探测到有这样一串零碎的操作
都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
轻量级锁(CAS实现锁升级)
当只有一个线程进行同步时,对象头会将锁标志变为轻量级锁
如果出现两个线程以上同时抢占,那么会将轻量级锁膨胀为重量级锁
偏向锁
这个锁会偏向于第一个获得它的线
程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需
要再进行同步
程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需
要再进行同步
当一个对象已经计算过一
致性哈希码(Object:hashCode)后,再也无法进入偏向锁状态
致性哈希码(Object:hashCode)后,再也无法进入偏向锁状态
当一个对象当前正处于偏向锁状态,又收到需要
计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁
计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁
0 条评论
下一页