深入理解JVM
2021-10-11 16:07:05 0 举报
AI智能生成
深入理解JAVA虚拟机
作者其他创作
大纲/内容
运行时数据区域
线程私有
程序计数器
字节码行号指示器,本地方法则为空
虚拟机栈
存储局部变量表,操作栈,动态链接,方法出口等信息
本地方法栈
只为本地方法服务
线程共享的
Java堆
新生代(Young)
Eden区 8
From Survivor 1
To Survivor 1
老年代(Old )
方法区
运行时常量池是方法区的一部分
不需要连续内存,可以动态扩展
存放已经被虚拟机加载的类信息,如常量,静态变量
直接内存
垃圾回收
垃圾收集器
Serial
使用一个CPU或一条收集线程去完成垃圾收集工作
暂停其它所有的工作线程(Stop The Word))
JDK1.3之前是新生代收集的唯一选择
ParNew
使用多条线程收集。其余的和Serial一样
Server模式下的虚拟机首选新生代收集器。
目前除了Serial收集器,只有它可以与CMS收集配合工作
Parallel Scavenge
新生代收集器。使用复制算法收集
特点是达到一个可控制的吞吐量,也被称为“吞吐量优先”收集器
Serial Old
使用标记-整理算法收集
主要Client模式下虚拟使用
Server模式,则有两种用途,一是在JDK1.5之前与Parallel Scavenge收集器搭配使用。二作为CMS收集器的后背预案
Parallel Old
使用多线程和标记-整理算法。JDK1.6才开始提供
CMS
以获取最短回收停顿时间的为目标的收集器。基于标记-清楚算法实现。
运作过程分为四个阶段。初始标记,并发标记,重新标记,并发清除
缺点是:对CPU资源非常敏感,无法处理浮动垃圾。收集结束时会产生大量空间碎片
G1
运行大致分为:初始标记,并发标记,最终标记,筛选回收
将整个Java堆分为多个大小相等的独立区域
分代收集,空间整合不会产生内存空间碎片,可预测的停顿。有计划的避免回收整个Java堆。
垃圾回收算法
标记-清除算法
两个阶段。先标记所有要被回收的对象,标记完成后再统一清除被标记的对象。
标记和清除的过程效率都不高
会产生大量不连续的内存碎片,可能会导致后续运行中分配大对象时内存不足提前出发垃圾收集
复制算法
将内存分为两块相等的区域,每次只用其中一块,当用完一块后,就把还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
移动堆顶指针,按顺序分配内存即可。代价过高 ,内存缩小为原来的一半
老年代不推荐使用
标记-整理算法
标记过程仍然与 标记-清楚算法一样
但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存
分代收集算法
把Java堆分为新生代和老年代,根据各个年代选择收集算法
新生代中每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法
老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用标记-清理或标记-整理算法来进行回收
内存分配与回收策略
对象优先在Eden区分配
对象通常在新生代的Eden区进行分配
Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
与Minor GC对应的是Major GC、Full GC。
Minor GC
指发生在新生代的垃圾收集动作,非常频繁,速度较快
Major GC
发生在老年代的GC,出现Major GC,经常会伴随一次Minor GC,同时Minor GC也会引起Major GC,一般在GC日志中统称为GC,不频繁。
Full GC
发生在老年代和新生代的GC,速度很慢,需要Stop The World。
大对象直接进入老年代
需要大量连续内存空间的Java对象称为大对象
-XX:PretenureSizeThreadshold参数来设置大对象的阈值,超过阈值的对象直接分配到老年代。
大对象的出现会导致提前触发垃圾收集
长期存活的对象进入老年代
每个对象有一个对象年龄计数器,对象在Survivor区每次经过一次Minor GC,年龄就加1
当年龄达到一定程度(默认15),就晋升到老年代,虚拟机提供了-XX:MaxTenuringThreshold来进行设置
动态对象年龄判断
在survivor区中相同年龄所有对象大小的总和大于survivor区的一半,年龄大于等于该年龄的对象就可以直接进入老年代
无需等到MaxTenuringThreshold中要求的年龄。
空间分配担保
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
冒险是指经过一次Minor GC后有大量对象存活,而新生代的survivor区很小,放不下这些大量存活的对象,所以需要老年代进行分配担保,把survivor区无法容纳的对象直接进入老年代。
虚拟机类加载机制
类的加载时机
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的。解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定
何时开始类加载的第一个阶段?
new,getstatic,pustatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
对类进行反射调用时,如果类没有进行过初始化,则需要先触发其初始化
当初始化一个类时,如果发现父类还没有初始化,则需要先触发父类初始化。
当虚拟机启动时,用户指定一个执行的主类,虚拟机会先初始化这个主类。
当使用jdk1.7动态语言支持时,如果一个实例最后解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
类的加载过程
加载
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
链接
验证
文件格式验证:验证字节流是否符合Class文件格式的规范
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
符号引用验证:确保解析动作能正确执行
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配
这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化
如果一个类被主动引用,就会触发类的初始化。
通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法
通过反射方式执行
初始化子类的时候,会触发父类的初始化
作为程序入口直接运行时
使用
类的使用包括主动引用和被动引用
引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化
定义类数组,不会引起类的初始化。
引用类的常量,不会引起类的初始化。
卸载
该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。类会卸载
加载该类的ClassLoader已经被回收。类会卸载
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。类会卸载
的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
类加载器
启动类加载器
这个类加载器负责放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。
扩展类加载器
这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接使用。
应用程序类加载器
这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个
自定义加载器
用户自己定义的类加载器。
双亲委派模型
一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成
只有当父加载器在自己的搜索范围内找不到指定的类时,子加载器才会尝试自己去加载。
保障了系统安全性
对象
创建对象的5种方式
使用new关键字 → 调用了构造函数
使用Class类的newInstance方法→ 调用了构造函数
使用Constructor类的newInstance方法 → 调用了构造函数
使用clone方法→ 没有调用构造函数
使用反序列化→ 没有调用构造函数
对象的生命周期
创建阶段(Creation)
1,为对象分配存储空间
2,开始构造对象
3,从超类到子类对static成员进行初始化
4,超类成员变量按顺序初始化,递归调用超类的构造方法
5,子类成员变量按顺序初始化,子类构造方法调用
2,开始构造对象
3,从超类到子类对static成员进行初始化
4,超类成员变量按顺序初始化,递归调用超类的构造方法
5,子类成员变量按顺序初始化,子类构造方法调用
应用阶段(In Use)
对象至少被一个强引用持有着
不可视阶段(Invisible)
当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。
简单说就是程序的执行已经超出了该对象的作用域了。
简单说就是程序的执行已经超出了该对象的作用域了。
不可到达阶段(Unreachable)
当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。
如果该对象已经重写了finalize()方法,则会去执行该方法的终端操作。
如果该对象已经重写了finalize()方法,则会去执行该方法的终端操作。
可收集阶段(Collected)
当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
终结阶段(Finalized)
当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
对象空间的重新分配
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。
对象的内存分配
指针碰撞法
如果堆中内存是绝对规整的。用过的内存放一边,空闲的放一边,中间放着一个指针作为分界点的指示器,那所分配内存就是把指针向空闲一边移动一段与对象大小相等的距离,即为“指针碰撞”
空闲列表法
如果堆中内存不规整,已使用内存和未使用内存相互交错,虚拟机就必须一个列表,记录哪些内存块可用,在分配时从列表中找到一块足够大空间划分给对象,并更新列表上记录,即为“空闲列表”
总结
选择何种分配方式,由堆是否规整决定,而堆是否规整由采用的垃圾收集器是否有压缩整理功能决定。
使用Serial,ParNew等带Compactg过程的收集器时,系统采用指针碰撞法
使用CMS这种基于Mark-Sweep算法的收集器时,系统采用空闲列表法
对象的访问定位
句柄定位
使用句柄访问时,Java堆中会划分出一块内存来作为句柄池,references中存储的就是对象的句柄地址。句柄中包含对象实列数据与类型数据各组的具体地址信息 references->句柄池->java堆
直接指针定位
如果是直接指针访问,Java堆的布局就必须考虑如何放置访问类型数据相关。
总结
句柄访问最大好处就是references中存储的是稳定的句柄地址,在对象移动(垃圾收集时移动对象是普遍行为)时只会改变句柄中的实列数据指针,references本身不需要修改。
直接指针访问的最大好处是速度快,节省了一次定位的实时间开销。
JAVA内存模型与线程
Java内存模型
主内存与工作内存
1. JAVA内模型规定所有的变量都存储在主内存中,每个线程都有自己的工作内存,
线程的工作内存中保存的是当前线程使用到的变量值的副本(主内村拷贝过来的)。
2. 线程对变量的所有操作都必须在工作内存中进行,
不能直接与主内存进行读写交.线程间相互的传值需要通过主内存完成。
线程的工作内存中保存的是当前线程使用到的变量值的副本(主内村拷贝过来的)。
2. 线程对变量的所有操作都必须在工作内存中进行,
不能直接与主内存进行读写交.线程间相互的传值需要通过主内存完成。
内存间的交互
lock(锁定):作用于主内存的变量。把一个变量标识为一条线程独占的状态
unlock(解锁):作用于主内存的变量.把一个处于锁定状态的变量释放出来。
read(读取):作用于主内存的变量。把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的值放入工作内存的变量副本中
use(使用):作用与工作内存的变量.它把工作内存中一个变量值传递给执行引擎,
每当虚拟机遇到一个需要使用到变量的值的字节码指令时就会执行这个操作。
每当虚拟机遇到一个需要使用到变量的值的字节码指令时就会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎收到的赋值给工作内存的变量,
每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,
它把工作内存中的一个变量的值传送到主内存中,以便随后的wirte操作使用。
它把工作内存中的一个变量的值传送到主内存中,以便随后的wirte操作使用。
wirte(写入):作用于主内存的变量,
它把store操作从工作内存中得到的变量值放入主内存中。
它把store操作从工作内存中得到的变量值放入主内存中。
happens-before原则
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系
8大规则
程序次序规则
一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则
一个unLock操作先行发生于后面对同一个锁额lock操作;
volatile变量规则
对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则
Thread对象的start()方法先行发生于此线程的每个一个动作
程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则
线程中所有的操作都先行发生于线程的终止检测
对象终结规则
一个对象的初始化完成先行发生于他的finalize()方法的开始
0 条评论
下一页