JVM学习笔记
1.运行时数据区
2 运行时数据区及线程概述
JVM将内存划分为俩种类型的数据区域
- 线程共享:JVM启动时创建,退出时才销毁
- 线程私有:线程创建时创建,线程退出时销毁
2.1 运行时数据区
JVM内存布局规定了Java运行过程中内存申请,分配,管理的策略,保证高效运行。
不同JVM在内存划分和管理机制方面有部分差异,探讨经典JVM布局HotSpot
划分,线程共享为堆区,方法区,其他私有
- 本地方法栈
- 程序计数器
- 虚拟机栈:以栈帧为基本单位构成,栈帧包括局部变量表,操作数栈,动态连接,方法返回地址,附加信息
- 堆区:新生代,老年代
- 方法区
2.2 线程
在HotSpot中,每个线程都与操作系统本地线程直接映射。OS将线程调度到可用的CPU上,本地线程创建成功,调用Java线程中run方法。
当正常执行结束(未出现异常,捕获了异常),Java线程和本地线程都会被回收,并释放相应资源。
出现未捕获异常,Java线程终止,本地线程再决定JVM是否需要终止,当只有守护线程时,JVM自动退出
- 守护线程:系统性服务,垃圾回收线程,编译线程,手动创建守护线程
- 非守护线程:用户线程,系统工作线程
3 程序计数器
对物理寄存器的抽象模拟
看作为当前线程执行的字节码的行号指示器,字节码解释器通过改变计数器的值来选取下一条指令。
既没有垃圾回收也没有内存溢出
每个线程都有自己的计数器,生命周期与线程的一致。
- 程序计数器作用:
CPU不停切换线程,切换回来以后,需要知道从哪继续执行。 - 为什么线程私有:
为准确记录各个线程正在执行的字节码地址,不会互相干扰
4 虚拟机栈
跨平台性,不同平台CPU不同,所以Java指令不能基于内存器,而设计为基于栈
可以跨平台,指令集小,编译器容易实现,缺点是性能较低,实现同样功能需要更多指令。
每个线程都会创建一个虚拟机栈,内部由许多栈帧构成,每个栈帧对应一个Java方法调用。
每一个方法被调用至到执行完毕,对应入栈到出栈过程
栈解决程序运行问题
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
- 不存在垃圾回收,存在内存溢出
- 栈大小可动态扩展的或者固定不变的(HotSpot固定)。如果固定大小,如果线程请求的栈容量超过最大内存,抛异常。
如果动态扩展,且无法申请足够内存,或者无内存创建对应栈,会抛出OOM异常。使用-Xss设置最大栈空间。
栈帧是一个内存区块是一个数据集,维系着方法执行过程中的各种数据信息,一个活动线程上,一个时间点上只会有一个活动的栈帧(栈顶),即只有当前正在执行的方法的栈帧是有效的。
方法正常return或抛异常都会导致栈帧被弹出。
5 局部变量表
定义一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,数据类型包括基本数据类型,对象引用,returnAddress类
基本类型存值,引用类型存指向对象的引用
线程私有数据
容量大小在编译期就确定的。
对一个方法而言,参数和局部变量越多,局部变量表就越膨胀,栈帧就越大。
虚拟机通过变量表完成参数值到参数变量列表的传递过程,方法结束后销毁
最基本存储单元是Slot变量槽,32位以内占用一个,64位(long double)用俩位
JVM为每一个slot分配一个访问索引,通过这个索引可以访问到局部变量表指定的局部变量值
当方法被调用时,方法参数和方法体内部定义的局部变量将按照顺序被复制到局部变量表上的每一个slot
如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量有可能会复用过期的变量slot,可以节省资源
局部变量初始化:
静态变量有俩次初始化,一次系统初始化对其赋0值。另一次赋定义的值。
局部变量没有系统初始化过程,必须手动初始化
方法执行时,虚拟机使用局部变量表完成方法传递,变量也是重要垃圾回收根节点,变量表中直接或间接引用的对象都不会被回收
6 操作数栈
栈帧中除了局部变量表还有操作数栈(表达式栈)
用于保存计算过程中中间结果,同时作为计算过程中变量临时存储空间
JVM执行引擎的一个工作区,最大深度在编译期就定义好了,32位一个单位
未采用索引来进行数据访问,而采用入栈出栈完成一次数据访问。
JVM的解释引擎是基于栈的执行引擎,其中栈指的就是操作数栈
7 栈顶缓存技术
当一个栈的栈顶或附近元素被频繁访问,就会将其缓存到CPU的寄存器中,将原本在内存读写操作变成了在寄存器操作,提升执行效率
7 堆 Heap
堆是线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。
每个程序对应一个JVM,也对应一个堆
堆在JVM启动是创建,大小也被确定
JVM管理的最大内存空间,大小根据参数调节,可以处在物理上不连续的内存空间中,但逻辑上连续
7.1 堆栈关系
堆中放对象
栈中对象引用,指向堆中位置
7.2 堆空间划分
分区原因:优化gc性能
- 新生代:
分为Eden和Suvivor区(s0,s1)
生命周期短的, - 老年代:
7.3 内存分配过程
- new的对象放入Eden区,有大小限制,大对象直接老年代
- 当Eden区满,JVM对该区垃圾回收(Minor gc)
- 将剩余对象移动到s0区,该对象有年龄计数器,设置为1
- 再次触发垃圾回收,会对Eden和S0区进行垃圾回收,没有回收的移动到s1,s0过来的年龄为2,Eden为1
- 再次触发,没有被回收放入s0,接着再s1,每移动一次,年龄加1
- 当年龄超过参数设置大小时,进入老年代
- 老年代内存不足,进行Major gc或Full gc,如果gc后没法进行对象保存就会oom
7.4 JVM参数
-Xms: 堆起始内存,默认物理内存1/64
-Xmx: 堆最大内存,一般xms和xmx设置同样大小,否则控缩容造成压力,默认物理内存1/4
-XX:NewRatio:老年代占整个堆比,默认2,即新:老 = 1:2
运行查看参数:
- jstat -gc pid
7.5 线程本地分配缓存区TLAB
Thread Local Allocation Buffer,堆中一部分线程私有部分
在堆内存中的Eden区分配一块内存,多线程同时分配内存时,使用tlab避免线程不安全问题
堆区是共享线程,频繁创建对象从堆区划分内存线程不安全,加锁影响分配
对象在TLAB空间分配内存失败,JVM会加锁确保数据原子性
7.6 逃逸分析
有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
对象作用域在一个方法中定义被使用,没有逃逸
如果经过逃逸分析发现,对象没有逃逸出方法,可能会被优化成栈上分配,也无需gc了
TaoBaoVM,GCIH将生命周期较长的对象从Heap移出到Heap外,且gc不能管理GCIH内部的对象
7.6.1 优化
- 栈分配:随着方法结束,栈空间回收也回收掉栈上对象
- 同步省略:一个对象被发现只能从一个线程访问到,对于这个对象操作可以不考虑同步
- 分离对象或标量替换:有的对象不需要作为连续的内存结构存在也可以访问,那么这个对象可以部分或全部存储栈中
9.方法区
9.1 关系
运行时数据区
- 内存结构:堆 栈 方法区 程序计数器 虚拟机栈
- 线程共享:
共享:堆 方法区
私有:虚拟机栈 本地方法栈 程序计数器
9.2 概念
供各个线程共享的运行时内存区域,在JVM启动时创建,可以不连续
空间可以大小固定或拓展
JDK7之前,把方法区称为永久代
JDK8移除永久代
9.3 内部结构
class文件的一部分信息加载到方法区,存储类型信息,常量,静态变量,代码缓存等
- 类型信息
对于class,接口,枚举,注解类型,存储
9.3 JVM参数
-XX:MetaspaceSize:超过该值FullGc
-XX:MaxMetaspaceSize
二.垃圾回收
13 概述
当一个对象没有任何指针指向他时,视为垃圾
GC主要作用于方法区和堆
虚拟机自动执行释放对象内存
GC:Gabage Collection 垃圾收集,创建对象时,GC开始监控这个对象的地址,大小以及使用情况。
GC采用有向图方式记录和管理堆中所有对象,当一些对象不可达时就回收这些空间。
14 GC算法
GC可分为俩个阶段:
- 标记:标记那些对象是垃圾,引用计数算法,可达性分析算法
- 清除:清除垃圾对象,标记清除算法,复制算法,标记压缩算法
14.1 对象存活判断
14.1.1 引用计数法
每个对象保存一个整型引用计数器属性,为0可进行回收。
- 缺点:存储空间开销大,加减法时间开销。无法处理循环引用(AB相互引用,无第三个引用,引起内存泄漏)
14.1.2 可达性分析算法
以GC Roots(活跃的引用)为起始点,按照从上至下方式搜索被跟对象集合所连接的目标对象是否可达,该路径为引用链。
该算法必须在一个能保障一致性的快照中进行,必须STW
MAT工具
GC Root包含:
- 栈中对象引用:方法中使用到的参数,局部变量
14.2 对象的finalization机制
对象终止机制来自定义对象销毁前的处理逻辑,finalize方法
手动调close,调用该方法的缺点:
- 导致对象复活:当前对象的this赋值给了有效的变量引用
- 影响GC性能
- 工作效率低:处理的优先级很低
14.3 清除垃圾对象
14.3.1 标记清除算法
GC需要停止应用程序,会产生碎片
14.3.2 复制算法
将内存分为俩快,每次只使用一块,GC复制活着的对象到另一块内存,清除正在使用的内存所有对象。
空间时间效率都不高,适合存活对象少,新生代。
14.3.3 标记压缩算法
先标记引用对象,再将所有存活对象压缩到内存一端,按顺序排放,最后清除边界外所有空间
缺点移动对象需要修改引用地址
14.4 分代收集算法
完全代替以上算法
- 新生代:复制算法
- 老年代:标记清除、标记压缩算法
14.5 增量收集算法
上述算法都需要STW(stop the world),所有线程会暂停,影响体验
GC线程和应用线程交替运行,每次回收一小部分
14.6 分区收集
将堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收,控制一次回收多少个小区间。
15 GC相关概念
15.1 内存溢出
OOM
- 堆内存设置不够
- 代码里有大对象
- 老版本jdk:永久代大小有效且垃圾回收不积极
15.2 内存泄漏
对象不再用到,但GC不能回收
- 单例模式:单列生命周期和应用程序一样长,持有对外部对象引用,这个就不能回收
- 未手动关闭资源
- 静态集合类内存泄露:长生命周期对象持有短生命周期引用
static List list = new ArrayList();
public void test(){
Object o = new Object();
list.add(o)
}
- 内部持有外部类:内部类被长期引用,外部类也不会被回收
- 变量不合理作用域:作用域大于使用范围,以及没有及时设置为null(使用完变量设置为null会被回收)
- 改变哈希值:对象存入Hash后,不能修改参与计算hashcode的字段,否则俩个对象不同,之前的无法被检索到。需要存Hash时要保存hashCode不可变。
- 缓存泄露:对象引用放入缓存容易遗忘,使用weak-hashmap当缓存,当key没有引用时,就会自动丢弃
- 监听器和回调:注册回调但没有显式取消就会积聚,只保存弱引用
15.3 四种引用
引用就是记录一个对象的地址,然后通过这个地址值找到这个对象并使用这个对象
153.1 强引用
User u = new User(); 只要引用还存在就不会被垃圾回收
15.3.2 软引用
软引用是用来描述一些还有用,但非必需的对象
在系统将要发生内存溢出之前,垃圾收集器收集完垃圾对象的内存之后,内存仍然吃紧,此时垃圾收集器会把软引用的对象列入回收范围之中进行第二次回收,如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
SoftReference<Object>sf = new SoftReference<Object>(对象);
15.3.3 弱引用
被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
15.3.4 虚引用
为一个对象设置虚引用的唯一目的在于跟踪垃圾收集过程。
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
15.4 Stop The World
stw把所有工作线程都停掉
15.5 安全点
stw在特定位置进行GC的位置,通常选择运行时间长的指令位置,如方法调用,循环跳转等
- 抢先式中断:GC中断所有线程,某个线程不再安全点,重新恢复该线程,跑到安全点
- 主动式中断:应用线程跑到安全点查看GC的中断标志,中断挂起
15.6 安全区域
在一段代码片段中,对象引用关系不会发生任何变化,在这个区域GC都是安全的
- 线程进入安全区域时,如果发生GC,jvm会忽略这些线程
- 线程离开安全区域时,会检查是否完成GC,完成则运行,否则得到可以离开的信号为止
16 垃圾收集器
16.6 G1
基于并行和并发的收集器,把堆内存分割为很多区域,物理不连续,逻辑上连续,避免进行全区域的垃圾收集。跟踪每个Region里面垃圾堆积的价值大小,优先回收价值最大的区域
- 并行与并发:多个GC线程同时工作,拥有和应用线程交替执行的能力
- 分代收集
- 空间整合:region避免碎片
- 可预测停顿时间模型:
3.字节码与类加载器
17 .class文件结构
19 类加载过程
引用类型需要把.class文件加载到JVM才可以使用,class文件加载到内存,再到类卸载出内存会经历7个阶段
分别是加载、验证、准备、解析、初始化、使用和卸载,
其中,验证、准备和解析3个阶段统称为链接(Linking),整个过程称为类的生命周期
19.1 加载
将Java类的class文件加载到机器内存中,并在内存中构建出Java类的原型,也就是类模板对象。所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVM将从class文件中解析出的常量池、类字段、类方法等信息存储到类模板对象中。
JVM必须完成以下3件事情。(1)通过类的全名,获取类的二进制数据流。(2)解析类的二进制数据流为方法区内的数据结构(Java类模型)。(3)创建java.lang.Class类的实例,作为方法区中访问类数据的入口
20 类加载器
类加载器从文件系统或者网络中加载class文件到JVM内部,至于class文件是否可以运行,则由执行引擎决定,类加载器将加载的类信息存放到方法区。类加载器在整个装载阶段,只能影响到类的加载,而无法改变类的链接和初始化行为。
同一命名空间不会出现同一名称包含包名的类
20.1 分类
- 显式:代码直接使用Class对象方法
- 隐式:JVM自动加载
- 启动器类加载器:
- 自定义类加载器:
20.2 双亲委派模型
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载
20.2.1 加载顺序
20.2.1 优势
避免类的重复加载,确保一个类的全局唯一性,Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父类加载器已经加载了该类时,子类加载器就没有必要再加载一次。这样做可以保护程序安全,防止核心API被随意篡改,比如JVM不允许定义一个java.lang.String的类,会出现java.lang.SecurityException,类加载器会做安全检查。
20.2.2 劣势
顶层的类加载器无法访问底层的类加载器所加载的类
20.2.3 破坏模型
- 重写loadClass()方法破坏双亲委派模型,我们前面讲过双亲委派模型就是通过这个方法实现的,这个方法可以指定类通过什么加载器来加载
- 线程上下文类加载器,能够使父类加载器调用子类加载器进行加载。
- 热替换:定制的类加载器的父类加载器必须设置为null或者重写findClass()方法,加载类的时候调用findClass()方法即可,不去调用loadClass()方法,当通过loadClass()方法进行类的加载时,如果该类没有加载过,会委托给应用程序类加载器进行加载,这样就不会实现热部署了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
2022-04-17 JSP 脚本段