JVM总结
类的生命周期
- 加载(loading)
- 链接(linking)
- 验证(verify)
- 准备(prepare)
- 解析(resolve)
- 初始化(initialization)
- 使用(using)
- 卸载
加载(Loading)
- 通过一个类的全限定名称获取此类的二进制字节流。(类的加载过程是序列化还是反序列化?)
- 将二进制字节流所代表的静态存储结构转化为方法区中运行的数据结构
- 在内存中生成一个代表这个类Class对象,作为方法区中各种数据访问的入口
验证
- 验证类的正确性,防止危害虚拟机安全
- 验证方式:文件格式验证,元数据验证,字节码验证,符号引用验证
准备
- 为类变量分配内存空间,并且为其分配默认值(不是我们在代码中赋的值)。
- 不包含用final修饰的static,这时属于常量了,在编译时就会分配了,此时会显式赋值。
- 不会为实例变量的进行初始化,类变量分配在方法区中,而实例变量会随着实例对象一起分配到Java堆中。
- 区分类变量、实例变量和常量。
解析
- 符号引用转换为直接引用(符号引用:使用一组符号代表引用的目标,)
- 事实上,解析操作往往在jvm初始化之后在进行
初始化
- 执行类的构造器方法
- 类变量(static)和静态代码块,若该类有父类,要保证先执行父类的方法,再执行本类的方法。
- 虚拟机必须保证一个类方法在多线程下被同步加锁
类加载器
在字节码加载的过程,有三个类加载器
- 引导类加载器:Bootstrap ClassLoader(C++写的,属于系统,没有父加载器)
- 拓展类加载器:Extention ClassLoader(自定义的ClassLoader,只要是直接或间接继承于ClassLoader都属于自定义的ClassLoader)
- 应用类加载器:Application ClassLoader(自定义的ClassLoader)
为什么需要自定义加载器
- 隔离加载类
- 修改类加载的方式
- 拓展加载源
- 防止源码泄露
如何判断两个类是同一个的类
- 类的全限定名称相同
- 类的加载器相同
自定义加载类的方式
- 实现ClassLoader或者其子类
- 实现loadClass()或者findClass()
双亲委派机制
加载某个类的class文件时,会向上委托,看看父类是否能够加载,如果父类不能加载,在向下委托。
优势:
- 防止字节码的重复加载
- 防止核心API被篡改,保护程序的安全
主动使用和被动使用
运行时数据区
主要包含五部分:
- 堆
- 类的实例对象
- 字符串常量池
- 静态变量
- 方法区(类似于接口,永久代和元空间都是HotSpot VM对方法区的实现,JDK1.8之前使用的是永久代,JDK1.8及以后使用的是元空间。元空间使用的是本地内存,不容易包存储异常)
- 程序计数器
- 虚拟机栈
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回值
- 一些附加信息
- 本地方法栈
各线程共享的区域:
- 堆
- 方法区
各线程私有的区域:
- 程序计数器
- 虚拟机栈
- 本地方法栈
程序计数器
存储下一条指令的地址
虚拟机栈
- 一个方法对应一个栈帧
栈帧
- 局部变量表(本地变量表)
- 操作数栈(或表达式栈)
- 动态链接(或指向运行时常量池的方法引用)
- 方法返回的地址
- 一些附加信息
局部变量表
-
基本数据类型、对象 引用和返回地址
-
存储单元是slot(变量槽),一个slot是4个字节
- 基本数据类型中long和double占两个slot,其他的都是占一个slot
-
this会存放在slot索引为0的位置上
静态变量和局部变量的区别
类变量有两次赋值的机会,一次在“准备”阶段(系统初始化),一次在初始化阶段(人为初始化)。而局部变量只有人为初始化。所以类变量可以在没有进行人为初始化时被使用,而局部变量则不行。
操作数栈
临时的存储空间,保存计算过程的中间结果。
动态链接
堆
在jvm启动使被创建
- 每个线程在堆中有对应的TLAB
- 新生代:老年代= 1:2
- 在新生代中Eden:survivor=8:2
新生代:
空间不足进行Minor GC,会发生STW
老年代:
空间不足进行Major GC,会发生STW
- 大对象直接放到老年代
- 长时间存活的对象放在老年代
- 动态对象年龄判断:相同年龄的对象大于survivor空间的一半,则将此年龄的对象(包括大于此年龄的)全部放进老年代,而不用达到指定的年龄
- 空间担保
TLAB:存放在Eden区,为每个线程分配了一个私有缓存区域。
逃逸分析
出现逃逸分析之后,堆不在是对象内存分配的唯一选择,对象也可以分配到栈上。
- 一个对象只在方法内部使用,则没有发生逃逸
- 一个对象在方法中定义之后,又被外部方法所引用,则发生了逃逸。
方法区
-
类型信息
-
常量
-
静态变量(JDK1.7及以后调到堆中)
-
即时编译器编译后的代码缓存(JIT代码缓存)
-
运行时常量池(其中字符串常量池在JDK1.7及以后调到堆中)
-
域信息
-
方法信息
方法区中的是运行时常量池,字节码文件中是常量池。
永久代为什么被替换
永久代的栈内存较小,容易溢出,元空间使用本地内存,空间更大。
字符串常量池为什么要调整
方法区不经常回收垃圾,而字符串经常改变,需要回收频繁,二者恰恰矛盾,所以讲字符串常量池调整到堆中。
对象创建的步骤
##对象的内存布局
- 对象头
- 实时数据
- 对齐填充
对象访问的方式
- 句柄访问
- 直接访问
执行引擎
将字节码指令转换为机器码指令
热点代码
分层编译
intern()的使用
垃圾回收
垃圾标记算法
- 引用计数算法
- 无法解决对象循环引用的问题,只要有对象再引用其计数就不节能为0,所以不能被回收
- 可达性分析算法
- 以GC Root为起点,查找所有能到达的对象
finalize()方法
垃圾回收算法
- 标记清除算法
- 会产生内存碎片
- 复制算法
- 不会产生内存碎片,但是会浪费一部分空间
- 标记压缩算法
- 不会产生内存碎片
分代算法
- 老年代使用标记压缩算法,新生代使用复制算法。
增量算法
分区算法:G1
内存泄露
安全点和安全域
引用
强引用:new一个对象
软引用:缓存。内存不足时才会回收
弱引用:也可用来作缓存。只要进行垃圾回收,就会被回收。
虚引用:
垃圾回收器
-
串行回收器:Serial,Serial Old
-
并行回收器:ParNew,Parallel Scavenge, Parallel Old
-
并发回收器:CMS,G1
CMS垃圾回收的过程:(采用的是标记清除算法)
- 初始标记(STW)
- 并发标记
- 重新标记(STW)
- 并发清除
标记压缩算法只适合在STW场景下使用。
G1
- 2048个 region,每个region是2的N次幂
- 整堆垃圾回收
- 单独的H去存放大对象,如果一个放不下,会找多个连续的H区来存储。
- 适合堆空间大的
垃圾回收器对比
参考:尚硅谷视频
链接:https://www.bilibili.com/video/BV1PJ411n7xZ?spm_id_from=333.999.0.0