JVM专题1
Day1 10.08
2-1认识jvm
jvm是什么
jvm就是java虚拟机,
虚拟机是通过软件模拟的具有完整硬件系统功能的,运行一个完全隔离环境的系统
特点: 完整 隔离
jvm是通过通过软件来模拟java字节码的指令集,也就是java程序的运行环境

JVM的主要功能
- 通过ClassLoader寻找找装载class文件
- 解释字节码成为指令并执行,提供class文件的运行环境
- 进行运行时期的内存分配和垃圾回收
- 与硬件交互的平台
2-3 JVM结构
字节码的指令集
JVM的指令集是基于字节码的,每条指令是由一个字节长度的操作码构成,这个操作码是知名要执行的操作. 有些指令后面会跟随0个或者多个操作数,
操作数是这个这个操作所需要的数据. 但是很多指令仅仅是由一个操作码构成,并不包含操作数, 比nop, pop, dup这些就没有操作数.
这种设计是好处是虚拟机可以通过操作码快速的识别执行对应的操作, 特别是在栈式虚拟机的机构下,操作数往往是直接从操作数栈里面获取或者操作的.
jvm的指令集会根据数据类型不同而提供不同的指令
2-4 如何学习指令集
特殊方法
实例初始化方法 是通过jvm的invokespecia指令调用
类或者接口的舒适化方法,不包含参数 ,返回void
类库
2-5 class字节码解析
class文件格式
- class文件是jvm的输入,jvm规范中定义了class文件的结构,class文件是jvm实现平台无关,技术无关的重要基础
- class文件是一组以8字节为单位的字节流,各个数据项目按照顺序紧凑排列
- 对于占用空间大于8字节的数据项,就按照高位在前的方式分割成多个8直接进行存储

3-1 类加载
类的生命周期

首先加载这个类,加载之后进入链接 那么链接里面又有三个小步骤(验证 准备 解析)
当链接处理完成之后 会进入初始化阶段
只有初始化之后的类才能被使用
如果确定不会被使用了就会被卸载
- 加载
查找并且加载类文件的二进制数据
- 链接
就是把已经读入内存的类二进制数据合并到jvm运行时的环境中去 主要包含
验证: 确保被加载类的正确性
准备: 为类的静态变量分配内存 并且初始化赋值0
解析: 把常量池的符号引用转换成直接引用
- 初始化
给类的静态变量赋值初始值
- 使用
- 卸载
类加载要完成的功能
- 通过类的全限定名来获取该类的二进制字节流
- 读进来之后 把二进制字节流转换成方法区的运行时的数据结构
- 在推上创建一个java.lang.class对象,用来封装类在方法区的数据结构,并且向外提供了访问方法区的数据结构的接口
加载类的方法
- 本地文件中加载
- 从jar等文件中加载
- 将java源文件动态编译成class
- 网络下载
jvm自带的类加载器类型
- 启动类加载器BootstrapClassLoader
- 平台类加载器PlantformClassLoader
在jdk8没有, 之前叫做扩展类加载器ExtensionClassLoader
为什么要把jdk8的扩展类加载器给废除呢?
在jdk8之前的版本,扩展类加载器是负责加载jre/lib/ext目录下的jar文件,如果想要扩展功能,就要亲自把jar包导入ext文件夹下面, 这种是存在一些风险的 因为任何一个人都可以将一个kar文件放入这个目录下面 假设这个jar文件携带病毒什么的 就会导致一些问题的发生,
- 应用程序类加载器AppClassLoader

3-2类加载器的使用
- 启动类加载器
用于加载启动的基础模块类,比如java.base, java.management, java.xml等
可以使用.getClass().getClassLoader()来调用

- 平台类加载器


在jdk8的场景下:
- 类加载启动器
负责将<java_home>/lib文件夹 或者-Xbootclasspath参数指定的路径中,而且虚拟机识别的类库加载到内存中
- 扩展类加载器
负责加载/lib/ext文件夹 或者java.ext.dirs系统变量指定路径中的所有类库
- 应用加载器
classpath路径的所有类库

3-3 双亲委派
jvm中的ClassLoader通常是采用双亲委派模型,他要求除了启动类加载器之外,其余的类加载器都应该有自己的父级加载器
这里的父子关系是组合并不是继承,工作流程如下
- 一个类加载器接收到了类加载请求后,首先搜索他的内建健在起定义的所有"具名模块"
- 如果找到合适的模块定义,就会使用该加载器来加载
- 如果class没有在这个加载器定义的具名模块中找到,那么就会委托给父级加载器,知道启动类加载器
- 如果父级架子阿奇反馈他也不能完成加载请求,比如在他的搜索路径下找不到这个类,那子类加载器才能自己来加载(自己加载就是在自己的classpath下面去找存不存在这个类)
- 在类路径下找到的类 将会成为这些加载器的无名模块
双亲委派模型的工作原理大白话就是
从子集的类加载器开始,现在自己的加载器里找,存不存在要加载的类,不存在就委托父级 父级也在自己模块里找有没有,没有接着父级就这样一路向上,一直到启动类加载器,最后启动类加载器也没有找到 就会一路反馈下来,告诉子集加载器说,我没有找到.那么每一层子集都会再向自己的classpath下面找找 如果一路下来还是找不到 就爆出claanotfound错误,如果找到就是成为这些加载器的无名模块
这个过程就是双亲委派模型,一直委派到启动类
3-5 双亲委派机制
模型说明
- 实现双亲委派的代码在java.lang.classloader的loadClass()访达中, 如果是自定义加载器的话 建议覆盖实现findClass()方法
- 如果一个类加载器能够加载某个类,就称为定义类加载器,所有能够成功范围该类的class的类加载器都称为初始类加载器
- 如果没有指定父加载器, 那么默认就是启动加载器
- 每个类加载器都有自己的命名空间,命名空间由该类加载器以及其所有父加载器所加载的类构成,不同的命名空间可以出现类的全路径名相同的情况
- 运行时候包是由同一个类加载器的类构成,决定两个类是否属于同一个运行时包,不仅要看全路径名是否一样,还要看定义类加载器是否相同,只有属于同一个运行时包的类才能实现相互包内可见
破坏双亲委派模型
为什么要破坏: 父加载器无法向下识别子加载器加载的资源
举个例子就是jdbc加载驱动
引入了线程上下文类加载器,可以通过Thread的setContextClassLoader()进行设置
另外一种典型的情况就是实现热替换,比如osgi模块化热部署,他的类加载器就不在是严格按照双亲委派,很多困难就在平级的类加载器中执行的了,
3-6 类链接和初始化
类链接的验证
- 类文件结构检查
按照jvm规范规定的类文件结构进行 验证字节流是否符合class规范,
- 元数据验证
对字节码描述的信息进行语法的分析,保证符合java语言的要求,比如这个类是否有父类,父类是否允许继承
如果父类是final就不允许继承 看看父类是不是抽象类
- 字节码的验证
通过对数据流和控制流进行分析,确保程序语法是合法和符合逻辑的 主要对方法体进行校验
- 符号引用验证
对类自身以外的信息,也就是常量池中各个符号进行引用,进行匹配校验
类链接的解析
解析就是把常量池的符号引用转换成直接引用的过程,包括:符号引用,以一组无歧义的符号来描述引用的目标,与jvm实现无关
解析主要针对: 类 接口 字段 类方法 接口方法 方法类型 方法语句
类的初始化
给类的静态变量赋值初始值,或者执行类构造器
- 如果类还没加载和连接,就要仙家寨和连接
- 如果类存在父类,而且父类还没有被初始化 那就先初始化父类
- 如果类存在初始化语句,就依次执行这些初始化语句
- 如果是接口的话 初始化一个类的时候,并不会先初始化他实现的接口
初始化一个接口的时候,并不会初始化他的父接口
只有当程序首次使用接口里面的变量或者调用接口的方法的时候 才会导致接口初始化
- 调用ClassLoader类的loadClass方法来装载一个类,并不会初始化这个类,不是对类的主动低使用
3-7 类的主动初始化
java程序对类的使用分为主动使用和被动使用,虚拟机必须在每个类或者接口 "首次主动使用"的时候才会初始化他们,被动使用类不会导致类的初始化,
主动使用的情况:
- 创建类的对象
- 访问某个类或者接口的静态方法
- 调用类的静态方法
- 反射某个类
- 初始化某个类的子类,而父类还没有初始化
- jvm启动运行的主类
- 定义了default方法的接口,当借口实现类初始化的时候
类的卸载
- 当代表一个类的class对象不再被引用,那么class打一下的生命周期也就结束了,
对应在方法区中的数据也会被卸载
- jvm自带的类加载器装载的类 是不会被卸载的 用户自定义的才会被卸载
4-1 jvm 内存分配

运行时数据区:pc寄存器(程序计数器) java 虚拟机栈 java堆 方法区 运行时常量池, 本地方法栈
pc寄存器(程序计数器)
- 每个线程用友一个pc寄存器,是线程私有的 用来存储指向下一条指令的地址
- 在创建线程的时候,应该创建响应的pc寄存器
- 执行本地方法时候,pc寄存器的值是undefined
- pc寄存器是比较小的内存空间 只做一个简单的地址记录 是唯一一个在jvm规范中没有规定OutOfMemoryError的内存区域 也就是说不会被溢出的
java栈
描述的是java方法执行的线程内存模型 每个方法被执行时候 jvm同步创建栈贞
- 栈是由一系列贞组成 是线程私有的
- 贞是用于保存一个方法的局部变量 操作数栈(java没有寄存器 所有参数传递使用操作数栈) 常量池指针 动态链接 方法返回值等
- 每次方法调用都会创建一个贞,并且压栈,退出方法的时候 修改栈顶指针就可以把栈贞中的内容销毁
- 局部变量表存放编译期可以知道的壳子基本数据类型和引用类型,每一个slot存放32位数据, long dpuble占用2个槽位
- 栈的优点 存取速度比堆快, 仅次于寄存器
- 缺点: 存在栈中的数据大小,生存期是在编译期间已经决定好的 缺乏灵活性
java堆
用来存放系统创建的对象和数组 是所有线程共享java堆 通过new关键字创建的
- gc主要回收的就是堆空间 对分代的gc来说,堆也是分代的
- 优点: 运行期动态分配内存大小,可以自动进行gc回收 缺点: 效率相对比较慢

- 新生代是用放新分配的对象 新生代中经历过gc, 没有回收掉的东西就被复制到老年代
- 老年代存储一些大对象 大对象可能直接进入老年代
- 整个堆大小 = 新生代 + 老年代
- 新生代 = Eden + 存活区
- 以前的持久代 用来存放class Method等元信息的区域 从jdk8开始就去掉了 取而代之是元空间 元空间并不在虚拟机内 而且直接使用本地内存
对象的内存的分布



方法区
是线程共享的区域 用于保存装载类的结构信息
- 方法区是和元空间关联在一起 但是具体的是和jvm实现和版本有关
- jvm规范中是把方法区描述为堆的一个逻辑部分 但是他有一个别名是Non-heap(非堆) 应是为了与java堆区分开
运行时常量池
是class文件中每个类或者接口的常量池表, 在运行期间表示形式 通常包含 类的版本 字段 方法 接口
在方法区中进行分配
在加载类和接口到jvm就创建响应的运行时常量池
本地方法栈
在jvm中用来支持native方法执行的栈
栈 堆 方法区的交互关系

4-3 Trace跟踪
- 打印gc的简要信息
-Xlog:gc
- 指定gc log的位置 以文件输出
-Xlog:gc:garbage-collection.log
- 每次gc之后,都打印堆的信息
-Xlog:gc+headp=debug
gc日志格式
不同的回收器 格式不同
- gc发生的时间 也就是jvm启动以来的经过的秒
- 日志级别信息 和 日志类型标记
- gc识别号
- gc的类型 说明gc的原因
- 容量 gc前的 -> gc后的
- gc持续的时间,单位是秒, 有的回收器会有更详细的描述,比如user表示程序消耗的时间 sys表示系统内核消耗的时间 real表示操作开始到结束的时间
Java堆的参数
- Xms
初始化推大小,默认物理内存的64分之一 ,在现在jdk13以后 这个必须是1024的倍数


在创建类的时候设置在Argument 下面的VM arguments填写比如Xms 10m 是指定10m
- Xmx
最大堆的大小 默认是内存地址的4分之1
建议Xms和Xmx配置一样 避免每次gc之后调整堆的大小
- -XX:NewRatio
设置老年代和新生代的比值 如果xms=xmx情况下,而且设置了xmn的情况下,这个比值就不用设置
**-XX:NewRatio=1 是老年代比新生代的比例是1 **
- -XX:SurvivorRation
eden去和survivor区的大小比例
设置为8的时候 两个survivor去与一个eden区的比值是2比8 一个survivor占据整个新生的10分之1
- -Xss
Java栈的参数 通常只有几百k 决定了函数调用的深度
在实际开发如果出现了java.lang.StackOverflowError这个错误 很大的原因是递归调用或者死循环导致整个栈挂掉了
- -XX:MetaspaceSize
元空间设置 初始空间大小 一般用不上
6-1 gc基础和跟搜索算法
- 什么是垃圾
就是内存中已经不再被使用到的内存空间 比如之前在new一个对象时候 这个空间被使用了 但是现在备用被使用荒废了
- 判定垃圾
引用计数法:
给每个对象添加一个引用计数器,有访问就加1, 引用失效就减一 只要比0大 就不是垃圾
优点:: 简单高效 缺点: 不能解决对象之间的循环引用的问题
**循环引用是什么? **
假如说一个a对象里面有个b类型 也就是a对象在运行过程中会持有b对象或者指针
b对象同样可以持有c对象 而c对象最后饶了一大圈持有的是a对象
这样abc形成循环引用 a-->b--->c--->a 外部可能没有人在引用了但是内部引用会加1
可达性分析算法(根搜索算法)
这个是主流的
从根节点乡下搜索对象节点,搜索走过的路径成为引用链, 当一个对象到根之间没有连通的话,则该对象不可用

能够作为根的对象:
- 虚拟机栈(栈贞局部变量)中的引用对象
- 方法区类静态属性引用的对象
比如 static a = new a
- 方法区中常量引用的对象
- 方法本地栈中JNI引用的对象
缺点: 每次都要分析是不是到跟 所以耗时间 出现了OopMap 是描述对象引用的数据类型 在初始化时候
引用分类
引用类型有强 弱 软 虚这四种
- 强引用
比如Object a = new A()这样 不会被回收 对象被大于等于一个变量所引用 处于可达状态
- 软引用
有可能被gc回收 是取决于内存情况, 如果内存不够就会被回收 够不会回收
- 弱引用
在gc运行的时候就会被回收
- 虚引用
根没有任何引用差不多,对象他自己根本感受不到引用 一般用于追踪对象被gc回收的状态
但一般不能单独使用 需要和引用队列一块使用
垃圾回收的基础
- 跨代引用
一个代中的对象引用了另一代中的对象
比如新生代里面的对象引用了老年代的对象
这种是极个别少数的 所以有个跨代引用假说
所以应该同时生死存亡
但是存在问题 由于新生代引用了老年代 所以如果扫描时候扫描时候两个代都要进行扫描
所以引用了另一个数据结构
记忆集(Remembered Set)
是一种记录从非收集区域指向收集区域的指针集合的抽象数据结构
也就是建立一个全局的数据结构 这个把老年代划分成若干个小块 标明哪一块存在跨代引用
卡精度: 每个记录精确到每一块内存区域,该内存区域内有对象含有跨代指针
卡表: 是记忆集的一种具体实现方式,定义了记忆集的记录精度和堆内存的映射关系
卡表里面的值谁来维护 怎么时候维护
通常在引用数据对象发生变化时候就要开始维护 通过写屏障来维护
写屏障可以看成是jvm对引用类型字段的复制,这个动作的aop
怎么判断是不是垃圾
- 根搜索算法判断不可用
- 看看是否有没有必要执行finalize方法
在第一次调用会覆盖finalize 如果说对象没有覆盖这个方法 有必要就不是垃圾
但不建议调用
- 上面俩走完之后 对象还没有被使用那就是垃圾
GC类型
- MinorGC / YoungGC
发生在新生代的收集动作
- MajorGc / OldGC
发生在老年代的GC 目前只有CMS收集器会有单独老年代的行为
- MixedGC
收集整个新生代以及部分老年代 目前只有G1收集器会有这个行为
- FullGC
收集整个java堆和方法区的GC
Stop-The-World
STW是java的一种全局暂停现象,多半由于gc引起的 所谓全局停顿 就是所有java代码停止运行 native代码可以执行 但是不能和jvm交互
他的危害是长时间服务暂停 明天响应 对于ha系统来说 可能会引起主备切换
gc收集类型
- 串行收集
gc单线程内存回收,会暂停所有用户的用户线程 比如Serial
- 串行收集
多个gc线程并发工作 这个时候用户线程是暂停的 比如Parallel
- 并发收集
用户线程和gc线程同时执行 不一定是并行 可以是交替执行 不需要暂停用户线程 比如cms
判断类无用的条件
- jvm中这个类所有的对象都被收回
- 加载这个类的ClassLoader已经被回收
- 没有任何地方引用这个来的class对象
- 无法在任何地方通过反射访问这个类
垃圾回收算法
- 标记清除法(Mark-Sweep)
分成标记和清除两个阶段 先标记出要回收的对象 然后统一回收这些对象

优点:简单
缺点: 效率不高 会产生很多次标记清楚 会产生大量不连续的内存碎片导致分配大对象的时候触发gc
- 复制算法(Copying)
把内存分成两块完全相同的区域,每次使用其中一块,当一块用完了就把这一块还存活的对象拷贝到另一块 然后把这一块清除掉

优点:简单 高效 不用考虑内存碎片的问题
缺点: 内存有点浪费 在jvm实现中 是将内存分为一块较大的Eden区和两块较小的Survivor去 每次使用Eden和一块Survive 回收时把存活的对象复制拷贝到另一块Survivor里面
hotSpot默认的Eden和survivor的比是8比1 也就是每次能用90^的新生代空间
如果Survivor空间不够 就要依赖老年代进行分配担保 把放不下的对象直接放入老年代
分配担保
当新生代进行gc后 新生代的存活区放不下 就要把这些对象放置到老年代里面 也就是老年代为新生代的gc做空间分配担保 具体过程:
- 在发生MinorGC之前,jvm就会检查老年代的最大可用连续空间 是否大于新生代所有对象的总空间 如果大于 可以确保MinorGC是安全的
- 如果小于的话 那么jvm就会检查是否设置了允许担保失败 如果设置了 继续检查老年代的最大可用连续空间 是否大于历次晋升到老年代对象的平均大小 如果大于则进行一次MinorGC 如果不大于 就是进行FUllGC
标记整理法(Mark-Compact)
因为复制算法在存活对象比较多的时候 效率低而且有空间的浪费 所以老年代一般不会使用复制算法 老年代一般用标记整理算法 也就是说复制算法多半在新生代使用
- 标记过程和标记清除一样 但是后续不是直接清除可回收对象 而是让所有存活对象都向一端移动 然后直接清除边界以外的内存

垃圾收集器
- 新生代里面的收集器
Serial 串行收集器
ParNew 并行收集器
Parallel Scvengen

串行收集器Serial
- Serial和Serial Old收集器都是一个单线程的收集器 在垃圾收集的时候 会Stop-the-World

在于每一次gc时候暂停所有线程
优点L简单 对于单cpu 因为没有多线程交互的开销 可能更高效 是默认Client模式下的新生代收集器
使用-XX:+UseSerialGC来开启
新生代: 复制算法 老年代: 标记整理算法
并行收集器
- ParNew收集器使用多线程进行gc回收,在gc时候 会Stop-the-World
- 只运行在新生代 使用复制算法

- 在并发能力好的cpu里面,他停顿的时间比串行收集器短 但是对于单cpu或者并发能力弱的cpu 因为是多线程的交互 所以可能比串行收集器差一点
- 在server模式下首先的新生代收集器 而且能和CMS收集器配合使用
- 不再使用-XX:UseParNewGC来单独来开启
- 要想开启 就使用cms
- -XX:PArallelGCTHreads: 指定线程数 建议和cpu数量一致
新生代ParallScavenge收集器
- 新生代ParallelScavenge收集器/ParallelOld收集器是一个应用于新生代的 使用复制算法 并行的收集器
- 和ParNew类似 但更加关注吞吐量 能最高效的理由cpu 适合运行后台应用

- 使用-XX:UseParallelGC开启
- -XX:+UseParalleOldGC来开启老年代使用ParallelOld收集器 使用这俩组合的收集器
- -XX;MaxGCPauseMillis
设置gc最大停顿时间
- 新生代使用复制算法 老年代使用标记整理算法
CMS收集器
- 这个收集器分为
初始标记: 只标记gc roots能关联到的对象
并发标记: 进行gc roots Tracing的过程
- 重新标记
修正并发标记期间 因错误运行导致标记发生变化的那一部分对象

- 在初始标记和重新标记这两个阶段会发生stw
- 本身使用标记清除法 多线程并发收集的垃圾收集器
- 最后的重置线程 是指清空个你手机相关的数据并重置 为下一次收集做准备
- 优点
低停顿 并发执行
- 缺点
并发执行 对cpu资源压力大
无法处理在处理过程中产生的垃圾 可能导致FullGC
采用的是标记清除算法 可能会导致大量的 碎片 从而在分配大对象是可能触发FUllGC的
- 开启
-XX:UseConcMarkSweepGC
使用的是ParNew + Cms + Serial Old的组合 So作为cms处所的备用收集器
G1收集器
- 面向服务端的收集器 在jdk13默认的收集器 特点:
G1把内存换分成多个独立区域Region
G1采用分代思想 保留了新生代和老年代 但他们不是物理隔离 而是一部分Region集合 而且不需要Region是连续的

- g1能充分利用多cpu 多核环境 尽量缩短stw
- g1整体上采用标记整理算法 局部是通过复制算法 所以不会产生内存碎片
- g1的停顿是可以预测的 能够明确指定在一个时间段内 消耗在gc上的时间不能超过多少时间
- 根cms相似 也分为4个阶段
初始标记: 只标记gc roots能直接关联到的对象
并发标记: 进行gc roots teacing过程
最终标记: 类似于cms重新标记 修正并发标记期间 因为程序运行导致标记发送变化那部分对象
筛选回收: 会根据时间来进行价值最大化回收


- 开启
-XX:+UseG1GC 开启g1 默认就是g1
-XX:MaxGCPauseMillis=n
最大gc停顿时间 这个是软目标 jvm尽可能停顿小于这个时间
-----------------------------
类加载机制


装载 ---> 链接 ---> 初始化
装载
1. ClassFile 转换成一个字节流 然后被类加载器所找到
2. 其次 找到之后 将字节流代表的静态存储结构转换成方法区的运行时的数据结构
3. 在堆中生成一个代表这个类的java.lang.class对象 作为方法区中数据访问入口
链接
1. 验证 :
其中验证主要是文件格式的验证 元数据的验证 字节码的验证 引用符号的验证
这4个验证并不是严格按照这个顺序走下来的
2. 准备阶段
按照官方的话叫做为类的静态变量分配内存并且赋值(当前类型的默认值)
比如 private static int a = 1;
在准备阶段值就是0 这个1是在后续阶段才会赋值的
3. 解析
解析是从运行时常量符号引用动态的确认具体值的过程 也就是符号引用转换成直接引用的过程
符号引用 : 在一个反编译过后的class文件 在Constant pool常量池里面的
Filedref 字面上是引用 但是并没有真正引用到一个地址
直接引用: 字面符号指向真正的内存
初始化
方法执行到了Clint 执行到了class init
类加载器有哪些
这个问题会引出双亲委派

类加载器可以分为两大类 1. jvm自带的类加载器一共有3个 2.自定义加载
首先说一下jvm自带的类加载器 自带的类加载器有3个 遵循双亲委派机制
1. 启动类加载器
主要加载java的核心库 rt.jar
2. 扩展类加载器
加载jre/lib/ext目录
这个扩展类加载器是在jdk8之前有 但是 在jdk8之后被废弃掉了改成平台类加载器了 为什么要废除呢?
因为之前的扩展类加载器是加载jre/lib/ext目录的jar文件 如果想要扩展 就需要亲自把jar包导入ext目录下 但这个存在风险 因为所有人都可以导入进去 如果将一个携带病毒的jar文件导入进去 那就会造成一些问题
3. 应用程序类加载器
加载classpath目录
第二类是自定义加载器
通过继承java.lang.ClassLoader类,并重写findClass方法
双亲委派机制
双亲委派机制是类加载器中使用的一种加载模式和顺序。
在加载类时,类加载器首先会检查是否有父类加载器。如果有,它会先请求父类加载器尝试加载该类。如果父类加载器还有父类加载器,它会继续向上请求,直到达到顶层的启动类加载器。
只有当所有父类加载器都无法加载这个类时,子类加载器才会尝试自己加载这个类。
双亲委派机制的主要作用是保护核心类库,防止用户定义的类与核心库的类发生冲突。这样可以确保如果我们定义了与核心类库同名的类,类加载器会优先加载核心类库的类,而不会加载自定义的类。因此,类加载的过程是自顶向下的,确保核心类库中的类能够优先加载,保证JDK核心类库的安全。
但是双亲委派机制有一个缺点
就是父加载器无法向下识别子加载器加载的资源 因为类加载器的委派几种是子类加载器向父类加载器递归的 而父类加载器不会中东查看子类加载器加载的类或者资源
举一个例子比如说jdbc在加载驱动的时候 因为jdbc的驱动通常是应用程序类加载器加载的 而DriverManager 是通过父加载器加载的 所以DriverManager没办法直接识别或者使用子加载器加载的jdbc驱动类
解决这个问题就需要打破双亲委派机制
1. 引入线程上下文类加载器
可以通过Thread的setContextClassLoader()方法来设置逃过双亲委派行为
Thread.currentThread().setContextClassLoader(MyClassLoader.class.getClassLoader());
2. 直接打破双亲委派
实现热替换 比如osgi模块化热部署 他的类加载器就不按照双亲委派了
运行时数据区

线程共享(我的数据你每个线程都能拿得到) : 堆 方法区
--
方法区 :在启动的时候就会创建 里面放的: 类信息 静态变量 常量 编译过后的代码
如果内存不够 会报出OutOFMemoryError这个异常
--
堆 : 堆也是jvm启动时候就会自动创建 存放的类实例以及数组也就是通过new关键字生产的
线程私有(仅当前线程拿得到) : 程序计数器 本地方法栈 java虚拟机栈
程序计数器: 里面存放的是
本地方法栈:
java虚拟机栈: 线程私有 和线程同时创建 最小存储单元是桢 简称栈桢
栈帧结构

局部变量表 :
存储局部变量的表 方法只能干的局部变量 和方法的参数 属于存储单元 不能直接使用
操作数栈 :
也是一个栈 是以压栈 出栈的方法来存储操作数的
int a = 1 int b =1 int c = a + b
首先会把int a = 1放入局部变量表
int c = a+b把a=1取出来放入操作数栈 b=1也取出来 把a+b放入操作数栈进行操作 计算完成得到结果 int c = 2放入局部变量表
方法返回地址:
一个方法执行之后 只有两种情况可以退出
1. 方法遇到了返回的字节码指令
return
2. 异常返回
动态链接 :
将符号引用转成直接引用的过程
堆为什么要分代
如果不分区 堆就是一个整体的话 也是可以的 但是在堆里面分配对象 当对象很多的时候 会把内存占满了 因为垃圾回收主要就是回收堆里面的没用废弃掉的对象
每次回收的时候 对整体进行找垃圾清理垃圾 会不方便
对象会有个生命周期 对象会根据生命周期的消亡而结束 所以不需要全盘扫描整个堆 根据生命周期的长短划分成两块 比如一个对象生命周期很长 那就放在old区 生命周期很短的放在新生区
回收一次就加1 一直到15 就会把对象放入老年代
为什么是15呢? 因为对象的分代年龄是存储在xx 他的取值范围是0000 - 1111 用4个二进制位 取最大值1111就是15
那么young区为什么又要划分呢?
内存碎片 内存空间不连续性
担保机制
如果对象放eden区放不下 那么old区会默认作为对象的担保空间这个对象会直接进入老年代 哪怕他的分代年龄不是15 进入之后
判定垃圾的方法
判定垃圾
1. 引用计数法 (重点:循环引用)
给每个对象添加一个引用计数器,有访问就加1, 引用失效就减一 只要比0大 就不是垃圾
优点: 简单高效
缺点: 不能解决对象之间的循环引用的问题
循环引用是什么?
假如说一个a对象里面有个b类型 也就是a对象在运行过程中会持有b对象或者指针
b对象同样可以持有c对象 而c对象最后绕了一大圈持有的是a对象
这样abc形成循环引用 a-->b--->c--->a 外部可能没有人在引用了但是内部引用会加1
循环引用会导致内存泄漏 内存泄漏的堆积会导致内存溢出
2. 可达性分析算法(根搜索算法)
这个是主流的
从根节点向下搜索对象节点,搜索走过的路径成为引用链, 当一个对象到根之间没有连通的话,则该对象不可用
gc root的本质是一组活跃的指针引用 指向真实的对象 并不是对象
缺点:每次GC都需要重新分析哪些对象与根节点(GC Roots)相连。由于程序在运行时不断创建和销毁对象,因此每次GC时都需要进行一次完整的对象图遍历,以确定对象是否可达。
判定为gc的不可达对象 那么这个对象真的判死刑了吗?
并不是
在垃圾收集阶段 会先去判断是否有没有必要执行finalize()方法
没必要(对象没有重写Finalize方法 或者 虚拟机已经调用过finalize方法)
判断对象死
对象被判定为不可达对象并且 没有必要执行finalize方法

上传文件 一般情况下都会重写finalize方法
垃圾回收算法
一. 标记清除算法
标记: 找到所有的gc root
而gc root是什么呢?
1. 虚拟机栈中的引用对象
2. 方法区类静态属性引用的对象
3. 方法区中的常量
清除: 递归便利全堆 把所有没有标记的对象全部清理掉
缺点: 效率很慢 因为递归便利全堆
还有一个最大的缺点 当在多线程环境下 会造成错误回收的问题 也就是不该回收的被回收了 该回收的没有回收
解决方式呢 可以改成串行的方式 stop the world 在gc回收的过程 所有的业务线程都会被强制暂停 但是会造成业务线程的卡顿
还有一个致命缺点就是直接标记删除会导致内存空间的不连续性 也就是内存碎片
-----
二. 复制算法
工作流程大概是
1. 先把内存分区
分成两部分 from空间 和 to空间 每次都使用一半 另一半为空闲空间 所有的对象最开始在from空间
2. 标记存活对象
3. 复制存活对象到to空间
4. 清理内存 当所有存活对象都被复制完毕 to空间成为新的工作空间 而from空间被清除
缺点:
1. 内存浪费 每次都要内存的一般来作为空闲的空间 也就是实际只有一半的空间是可以使用的
2. 不适合老年代 对于老年代 他的生命周期很长 这些对象的存活率很高 造成频繁复制带来额外开销
---
分配担保
目的是如果存活区的空间不够的情况下 确保老年代有足够的空间存放晋升的对象 避免在MinorGC的时候出现内存不够的情况
当新生代进行gc后 新生代的存货区放不下的话 就会把这些对象放到老年代里面 也就是老年代为新生代的gc做空间分配的担保
具体过程是:
发生MinorGC之前 jvm就会检查老年代的最大可用连续空间 是否大于新生代所有对象的总空间 如果大于 那可以确保MinorGC的安全
如果小于 那么jvm就会检查是否设置了允许担保失败 如果设置了 就会继续检查老年代的最大可用连续空间 是否 大于历次晋升到老年代对象的平均大小
如果大于 则进行一次MInorGC
如果不大于 就进行一次FULlGC
-----
三. 标记整理算法
也就是标记整理清除算法
标记: 与标记清除算法一样 从跟节点开始 标记所有存活对象
整理: 将存活的对象向堆的一端进行移动 重新排列存活对象 保证所有存活的对象都紧密排列在一起 避免咯内存碎片
清除: 清除所有没有标记的对象
----
优点: 避免了内存碎片 缺点: 移动对象的开销 尤其是在老年代中 存活对象越多 移动的开销就越大
---
综上所述:
新生代可以采用复制算法,因为新生代的对象大多数生命周期都很短 频繁的创建和销毁 复制算法可以快速的回收这些对象
老年代: 在cms垃圾回收器里面 对于老年代使用标记清除算法 其他的垃圾回收器采用的是标记整理算法
gc的类型
MInorGC
是发生在新生代的垃圾回收 因为新生代的对象生命周期都很短 所以 在短时间内就会变成垃圾 所以MinorGC的概率较高
特点
1.回收新生代中的eden区和survivor区的对象
2.如果经过多次MInorGC对象依旧存活 那就会进入老年代
3. 当新生代Eden区满了之后就会触发MInorGC
MajorGC / FUllGC
是针对老年代 或者整个堆(包括新生代和老年代)的垃圾回收
1. 开销通常比MInorGC大 因为老年代的对象存活时间比较长 回收的时候需要扫描的对象也更多
2. 这个发生的频率较低 但是一次gc的停顿时间比较长
3. 当老年代内存不够容纳新生代晋升的对象的时候 或者老年代的对象需要回收的时候 就会触发MajorGC
4. fullGC在特殊情况下 不仅会清理老年代 还会清理新生代
Mined GC
目标是整个新生代以及部分老年代 只有g1有这个行为
Full Gc
收集整个java堆和方法区的垃圾 包括新生代 老年代 方法区的回收 一般full gc等价于old gc
常见的垃圾回收器
Serial GC

- 单线程垃圾回收器 gc过程中会暂停所有用户线程(Stop-the-World)
- 新生代: 复制算法 老年代: 标记整理算法
- 适合单核cpu环境或者小内存应用 由于只能一个线程执行gc 所以效率比较低 但是简单
Parallel GC

- 基于多线程进行gc回收 在gc的时候会暂停所有的用户线程(Stop-the-world)
- 只仅仅运行在新生代 使用复制算法
- 在并发能力比较好的cpu里面 他的停顿时间比Serial串行收集器端 但是对于单cpu或者并发能力弱的cpu 因为是多线程的交互 所以可能比Serial还要差一些
- 在server模式下首选的新生代收集器 而且还能和cms收集器配合使用
- 想要开启 就使用cms
- -XX:ParallelGCThreads 指定线程数 建议和cpu数量保持一致
- 适用于处理批量任务 后台服务
CMS收集器

- 这个收集器分为
初始标记: 只标记gc roots能关联到的对象
并发标记: 进行gc roots Tracing的过程
- 重新标记
修正并发标记期间 因错误运行导致标记发生变化的那一部分对象
- 在初始标记和重新标记这两个阶段会发生stw
- 本身使用标记清除法 多线程并发收集的垃圾收集器
- 最后的重置线程 是指清空个你手机相关的数据并重置 为下一次收集做准备
- 优点
低停顿 并发执行
- 缺点
并发执行 对cpu资源压力大
无法处理在处理过程中产生的垃圾 可能导致FullGC
采用的是标记清除算法 可能会导致大量的 碎片 从而在分配大对象是可能触发FUllGC的
- 开启
-XX:UseConcMarkSweepGC
使用的是ParNew + Cms + Serial Old的组合 So作为cms处所的备用收集器
G1

- 面向服务端的收集器 在jdk13默认的收集器 特点:
G1把内存换分成多个独立区域Region
G1采用分代思想 保留了新生代和老年代 但他们不是物理隔离 而是一部分Region集合 而且不需要Region是连续的
- g1能充分利用多cpu 多核环境 尽量缩短stw
- g1整体上采用标记整理算法 局部是通过复制算法 所以不会产生内存碎片
- g1的停顿是可以预测的 能够明确指定在一个时间段内 消耗在gc上的时间不能超过多少时间
- 根cms相似 也分为4个阶段
初始标记: 只标记gc roots能直接关联到的对象
并发标记: 进行gc roots teacing过程
最终标记: 类似于cms重新标记 修正并发标记期间 因为程序运行导致标记发送变化那部分对象
筛选回收: 会根据时间来进行价值最大化回收
什么是STW
全称就是Stop-the-world 暂停世界
就是在gc过程中,需要将jvm内存冻结的一种状态,在stw状态下 java的线程除了执行gc线程以外的所有线程都是暂停的
虽然native方法可以执行 但是无法和jvm进行交互
gc各自算法的优化就是为了减少stw
聊一下g1
g1垃圾回收器主要引用于老年代和新生代 在jdk之后默认使用的就是g1
划分多个区域, 每个区域都可以充当eden, surivor, old, humongous 其中这个humongous 是给大对象准备的
采用的复制算法
分三个阶段 新生代回收 并发标记 混合收集

浙公网安备 33010602011771号