笨蛋总结JVM
由于Java语言将自己的内存控制权交给了虚拟机,所以需要了解虚拟机的运行机制
(主要用于回顾JVM)
笨蛋总结JVM
- 笨蛋总结JVM
- 1.运行时数据区域
- 线程私有区域
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- 线程共享区域
- 堆
- 方法区
- 1.2程序计数器
- 1.2.1什么是程序计数器
- 1.2.2程序计数器的特点
- 运行速度最快的一处内存存储区域
- 每个线程都有自己独有的程序计数器,程序计数器的生命周期和线程的生命周期一致
- 字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令
- 在任何时间中的一个线程只有一个方法正在执行,就是当前方法
- 在Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域
- 1.2.3总结
- 1.3Java虚拟机栈
- 1.3.1什么是Java虚拟机栈
- 1.3.2虚拟机栈的操作
- 入栈:
- 出栈:
- 遵循 ”先进后出 / 后进先出“
- 1.3.3什么是栈帧
- 1.3.3栈帧-局部变量表
- 1.3.3.1什么是局部变量表
- 1.3.3.2最小单位变量槽
- 1.3.3.3总结
- 1.3.4栈帧-操作数栈
- 1.3.4.1什么是操作数栈
- 1.3.4.2总结
- 1.3.5栈帧-动态连接
- 1.3.5.1什么是动态连接
- 1.3.5.2总结
- 1.3.6栈帧-方法返回地址
- 1.3.6.1正常退出 / 异常退出
- 正常退出:一个方法正常执行完,会遇到返回指令。这种情况下会有一个预先定义好类型的返回值返回给调用方法
- 异常退出:一个方法执行时,若发生了异常,且该异常没有在方法中进行捕获/抛出处理,则会触发方法的退出,不会有返回值返回给调用方
- 1.3.6.2什么是方法返回地址
- 1.3.6.3总结
- 1.3.7栈帧-附加信息
- 1.3.8栈帧的扩展
- 1.3.9Java方法的两种返回方式
- 1.3.10两类异常情况
- 若线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
- 若Java虚拟机栈容量可以动态扩展,当栈扩展到无法申请到足够的内存时,将抛出OutOfMemoryError异常
- 1.3.11总结
- 1.4Java本地方法栈
- 1.4.1什么是本地方法栈
- 1.5堆
- 1.5.1什么是堆
- 1.5.2总结
- 1.6方法区
- 1.6.1什么是方法区
- 1.6.2与堆区的共同点
- 1.6.3方法区的特性
- 1.6.4Java8之后的方法区
- 1.6.5总结
- 1.7运行时常量池
- 1.7.1什么是运行时常量池
- 1.7.2总结
- 1.8本地内存和直接内存
- 1.8.1本地内存是什么
- 1.8.2直接内存是什么
- 1.8.3Java程序内存和本地内存
- 2.Java中的引用
- 2.1直接引用
- 2.2引用的强度分类
- 2.2.1强引用
- 2.2.2软引用
- 2.2.3弱引用
- 2.2.4虚引用
- 3.可达性分析算法
- 3.1引用计数法
- 3.2可达性算法
- 3.3记忆集Remembered Sets
- 4.垃圾回收算法
- 4.0简单了解垃圾回收
- 4.0.1垃圾回收概念
- 吞吐量:吞吐量越高,垃圾回收的效率越高
- 最大暂停时间:最大暂停时间越短,用户使用系统时受到的影响就越小
- 堆使用效率:不同垃圾回收算法对堆内存的使用方式是不同的,从堆使用效率来说,标记清除 > 复制算法
- 4.0.2如何判断是否可以在堆中被回收
- 4.0.3在方法区中的回收,需要同时满足
- 4.0.4System.gc()的使用
- 4.1标记-清除算法
- 4.2复制算法
- 4.3标记-整理/压缩算法
- 4.4分代收集算法
- 5.垃圾收集器
- 5.1七大垃圾回收器
- 5.1.1年轻代-Serial垃圾回收器
- 5.1.2老年代-SerialOld垃圾回收器
- 5.1.3年轻代-ParNew垃圾回收器
- 5.1.4老年代-CMS(Concurrent Mark Sweep)垃圾回收器
- CMS执行步骤
- 5.1.5年轻代-Parallel Scavenge垃圾回收器
- 5.1.6老年代-Parallel Old垃圾回收器
- 5.1.7G1垃圾回收器
- 6.内存分配与回收策略
- 6.1对象优先在Eden区分配
- 6.2大对象直接进入老年代
- 6.3长期存活的对象将进入老年代
- 6.4动态对象年龄判定
- 6.5空间分配担保
- 7.类文件结构
- 7.1文件的组成
- 7.2文件组成各部分详解
- 7.2.1魔数与Class文件版本
- 7.2.2常量池
- 7.2.3访问标志
- 7.2.4类索引、父类索引、接口索引
- 7.2.5字段表属性
- 7.2.6方法属性
- 7.2.7属性表属性
- 8.类加载机制
- 8.1类的生命周期
- 8.1.1类的加载
- 8.1.1.1类加载阶段需要完成以下三件事
- 8.1.2类的连接-验证
- 8.1.3类的连接-准备
- 8.1.4类的连接-解析
- 包括:
- 符号引用:
- 直接引用:
- 8.1.5类的初始化
- 初始化步骤
- 类初始化时机
- 8.2类加载器和类加载器机制
- 8.2.1类加载器是什么
- 8.2.2类加载器的分类
- 8.2.3类加载器
- 8.3JDK8及8以前版本的类加载器
- 8.3.1虚拟机底层实现
- 启动类加载器Bootstrap
- 8.3.2Java中实现
- 扩展类加载器Extension:
- 应用程序类加载器Application:
- 8.4双亲委派机制(解决类到底由谁加载)
- 8.4.1双亲委派机制的作用
- 保证类加载的安全性
- 避免重复加载
- 8.4.2类加载的双亲委派机制
- 加载的流程
- 8.4.3双亲委派优先级
- 8.4.4主动加载一个类
- 使用Class.forName方法,使用当前类的类加载器去加载指定的类
- 获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载
- 8.4.5父类加载器
- 8.4.6打破双亲委派机制
- 自定义类加载器
- 线程上下文类加载器
- 线程上下文类加载器是否打破了双亲委派机制
- 打破
- 未打破
- OSGi模块化
- 8.4.7类加载器在JDK8和JDK9中发生的变化
- 9.常用配置参数
- 设置堆内存
- 指定GC算法
- 指定GC并行线程数
- 打印GC日志
- 指定GC日志文件
- 指定Meta区的最大值
- 设置单个线程栈的大小
- 指定堆内存溢出时的自动进行
1.运行时数据区域
Java对于内存的管理是采取的分区管理,根据不同的区域,管理上也是大有不同。
其中主要分为线程私有区域、线程共享区域
线程私有区域
-
程序计数器
-
Java虚拟机栈
-
本地方法栈
线程共享区域
-
堆
-
方法区
1.2程序计数器
1.2.1什么是程序计数器
就是在Java虚拟机中当前线程所执行的字节码的信号指示器,标示了下一条需要执行的字节码指令。也就是说整个线程都是通过程序计数器根据下一条需要执行的字节码指令不断地推进,从而让线程不断地运行
1.2.2程序计数器的特点
-
运行速度最快的一处内存存储区域
-
每个线程都有自己独有的程序计数器,程序计数器的生命周期和线程的生命周期一致
(因为整条线程都是程序计数器不断地推进向前的,当程序计数器停止,则线程也就停止了)
-
字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令
-
在任何时间中的一个线程只有一个方法正在执行,就是当前方法
-
若线程正在执行的是Java方法,程序计数器记录的是JVM字节码指令地址
-
若线程正在执行的是native本地方法,程序计数器就未指定值underfined
native方法:(C / C++方法,因为虚拟机的底层是由C++方法写的)
-
-
在Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域
1.2.3总结
- 内存小,运行速度最快
- 程序计数器唯一,生命周期与线程一致
- 改变程序计数器的值来执行下一条字节码指令
- 执行Java方法,记录JVM字节码指令地址
- 执行native方法,不指定值,为underfined
1.3Java虚拟机栈
1.3.1什么是Java虚拟机栈
也称Java栈,每个线程在创建时都会创建一个虚拟机栈,内部保存的每一个栈帧对应着Java方法的每一次调用
1.3.2虚拟机栈的操作
-
入栈:
调用一个新方法时,就构建一个栈帧压入到Java虚拟机栈中
-
出栈:
当一个方法执行完毕时,就将其对应的一个栈帧出战
-
遵循 ”先进后出 / 后进先出“
1.3.3什么是栈帧
线程调用一个方法的执行和退出就对应着一个栈帧的入栈和出栈。栈顶部的第一个栈帧叫做当前栈帧,对应线程需要执行的最新方法。
栈帧内部包括
- 局部变量表
- 操作数栈
- 方法返回地址
- 动态连接信息等
1.3.3栈帧-局部变量表
1.3.3.1什么是局部变量表
是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表所需要的容量大小在编译期就能确定下来,且在整个方法运行期间都不会发生容量空间的改变。
1.3.3.2最小单位变量槽
-
局部变量表的容量以变量槽为最小单位
-
通过索引定位的方式使用局部变量表,索引值的范围从0开始至局部变量表最大的变量槽数量
-
若访问的是32位数据类型的变量,索引N就代表使用第N个变量槽
-
若访问的是64位数据类型的变量,则会同时使用第N和N+1两个变量槽
两个相邻的共同存放一个64位数据的两个变量槽,虚拟机不允许采用任何方式单独访问其中的某一个
-
-
若执行实例方法,则局部变量表的第0位索引的变量槽默认是用于传递方法所属对象实例的引用
this
-
为节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的。
当字节码程序计数器的值已经超出了某个变量的作用域,则这个变量对应的变量槽就可以交给其他变量来重用
1.3.3.3总结
- 存放方法参数 / 方法内部定义的局部变量
- 若局部变量表执行实例方法,则第0位索引存放
this
- 访问32位 / 64位数据类型的变量,其索引表示的变量槽也不一样
- 变量槽可以重复利用
- 变量槽是局部变量表的最小容量单位
1.3.4栈帧-操作数栈
1.3.4.1什么是操作数栈
用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。当一个方法执行过程中,会有各种字节码指令堆操作数栈出栈和入栈操作
1.3.4.2总结
- 保存计算过程中的结果
- 计算过程中变量临时的存储空间
- 在编译阶段保证操作数栈中元素的数据类型与字节码指令的序列严格匹配
- 在类加载过程中的类检验阶段的数据流分析阶段会再次验证
1.3.5栈帧-动态连接
1.3.5.1什么是动态连接
-
每个栈帧都包含一个指向运行时常量池中该
栈帧所属方法
的引用,持有该引用就是为了支持方法调用过程中的动态连接 -
存放了被调用方法的实际地址或指向该地址的指针(当需要调用另一个方法时,会从栈帧中获取动态连接信息,跳转到该地址执行另一个方法)
-
在Java源文件被编译到字节码文件时,所有的变量和方法引用都作为符号引用保存在Class文件的常量池中
-
一个方法调用另外一个方法时,通过常量池中指向方法的符号引用来表示
动态连接的作用是为了将这些符号引用转换为调用方法的直接引用
1.3.5.2总结
- 存放被调用方法的实际地址
- 用于方法之间的调用
- 将常量池中指向方法的符号引用转为直接引用
1.3.6栈帧-方法返回地址
1.3.6.1正常退出 / 异常退出
-
正常退出:一个方法正常执行完,会遇到返回指令。这种情况下会有一个预先定义好类型的返回值返回给调用方法
-
异常退出:一个方法执行时,若发生了异常,且该异常没有在方法中进行捕获/抛出处理,则会触发方法的退出,不会有返回值返回给调用方
1.3.6.2什么是方法返回地址
- 方法正常退出时,主调方法的程序计数器的值可以作为返回代码的值(标识从哪里继续执行),栈帧中很可能会保存该计数器值
- 方法异常退出时,返回地址要通过异常处理器表来确定,栈帧中一般不会保存这部分信息
- 方法的退出就是当前栈帧出栈的过程。此时需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置程序计数器值等,让调用者方法继续执行
1.3.6.3总结
- 分正常退出和异常退出,正常退出有返回值,异常退出没有返回值,会通过异常处理器来进行处理
- 方法退出就是当前栈帧出栈的过程
1.3.7栈帧-附加信息
栈帧中允许携带Java虚拟机实现相关的一些附加信息,比如对程序调试提供支持的信息,但具体的信息取决于具体的虚拟机实现
1.3.8栈帧的扩展
- 在同一条活动的线程上,同一时间点只会有一个活动的栈帧,即只有当前正在执行的方法的栈帧才是有效的,被称为当前栈帧
- 不同线程中所包含的栈帧是不允许相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧
- 相同线程中所包含的栈帧是基于方法调用相互引用的
- 若当前方法调用其他方法,方法返回时,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
1.3.9Java方法的两种返回方式
- 正常的函数返回,使用return指令
- 抛出异常
以上两种方式都将导致栈帧被弹出
1.3.10两类异常情况
-
若线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
-
若Java虚拟机栈容量可以动态扩展,当栈扩展到无法申请到足够的内存时,将抛出OutOfMemoryError异常
1.3.11总结
- Java虚拟机栈的生命周期和线程一致
- 只有将栈帧进行入栈和出栈两个操作
- 只有两类异常
- StackOverflowError异常
- OutOfMemoryError异常
- 栈帧主要有局部变量表(存放方法参数/方法内的局部变量)、操作数栈(方法内计算过程中的临时存放值)、动态连接(存放被调用者方法的地址)、方法返回地址(正常退出返回代码的地址,异常退出根据异常器的处理)
程序计数器通过下一条字节码指令推动着线程不断地执行,而每个线程都会在创建时创建一个Java虚拟机栈,当线程调用Java方法时,在Java虚拟机栈中通过栈帧的入栈和出栈来存取或计算Java方法内部的相关数据
1.4Java本地方法栈
1.4.1什么是本地方法栈
- 一个Java方法调用非Java代码的接口
- 本地方法栈与Java虚拟机栈是非常相似的
- 区别在于
- Java虚拟机栈为虚拟机执行Java方法(字节码)服务
- 本地方法栈则是为虚拟机使用到的本地(Native)方法服务
1.5堆
1.5.1什么是堆
-
是被所有线程共享的一块内存区域,几乎(除了基本类型、直接内存、final、static修饰的常量)所有的对象实例都是在堆中进行分配内存的
-
Java堆是垃圾收集器管理的内存区域,以G1收集器的出现为分解,往前的收集器基本是采用分代收集理论进行设计
-
垃圾分代的唯一目的是优化GC性能
-
Java中的堆可以处于物理上不连续的内存空间,只要逻辑上是连续的就行
- 实现时既可以是固定大小,也可以是可扩展的,主流虚拟机都是可扩展的(-Xmx,-Xms控制)
- 若堆中没有内存完成实例分配,并且堆无法再扩展时,就会抛出OutOfMemoryError异常
1.5.2总结
- 被所有线程共享,几乎所有的对象实例都是在堆中进行分配内存
- 是垃圾收集器管理的内存区域
- 物理上的内存空间可以不连续,只要保证逻辑上连续
1.6方法区
1.6.1什么是方法区
- 是各个线程共享的内存区域,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据
- Java虚拟机规范中将方法区描述为堆的逻辑部分,通过别名 Non-Heap(非堆)来于Java堆区进行区分
1.6.2与堆区的共同点
- 在物理上的内存空间不要连续
- 方法区可以选择固定大小以及可扩展
1.6.3方法区的特性
- 方法区的大小决定了系统可以放置多少类,当类太多,则会导致方法区溢出,虚拟机会抛出内存溢出OutOfMemoryError异常
- 方法区只是JVM定义的一个概念,根据不同的厂商会有不同的实现,永久代是Hotspot虚拟机特有的概念,Java8的时候被元空间取代了。
- 永久代和元空间都可以理解为是方法区的落地实现
- 元空间存储类的元信息,静态变量、字符串常量池等并入Java堆中相当于永久代的数据被分到了Java堆和元空间中
- Java7使用
-XX:PermSize
和-xx:MaxPermSize
来设置永久代参数 - Java8使用
-XX:MetaspaceSize
和-xx:MaxMetaspaceSize
来设置元空间参数
- 主要包含类的元信息、运行时常量池、字符串常量池
1.6.4Java8之后的方法区
- 移除永久代,替换为元空间
- 永久代的class metadata 转移到了native memory(本地内存,而不是虚拟机)
- 永久代的interned Strings 和class static varibles转移到了Java堆
- 永久代参数(PermSize MaxPermSize)-> 元空间参数(MetaspaceSize MaxMetaspaceSize)
1.6.5总结
-
方法区被各个线程共享
-
只是一个逻辑部分,各个厂商有不同的实现
-
Java7(永久代)和Java8(元空间)的方法区存储类型不一样
1.7运行时常量池
1.7.1什么是运行时常量池
- 是方法区的一部分
- Class文件中除了有类的版本 / 字段 / 方法 / 接口等描述信息还有一项是常量池表,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放
- JVM为每个已加载的类型(类或接口)都维护一个运行时常量池,在加载类和接口到虚拟机后创建
- 运行时常量池相对于Class文件常量池的另一重要特性:具备动态性
- 当常量池无法再申请到内存时会抛出OutOfMemoryError异常(属于方法区的一部分)
1.7.2总结
- 属于方法区,具备动态性
- 无法申请到内存时会抛出OutOfMemoryError异常
1.8本地内存和直接内存
1.8.1本地内存是什么
-
既不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域
-
JDK8将方法区(永久代)移除,使用元数据区(元空间)来代替,并将元数据区从虚拟机运行时数据区移除,转移到了本地内存中,受本机物理内存的限制
-
当申请的内存超过了本机物理内存时,才会抛出OutOfMemoryError异常
-
Java虚拟机在执行的时候会把管理的内存分配到不同的区域,这些区域称为虚拟机内存;
对于虚拟机没有直接管理的物理内存,也会有一定的利用,被利用但不在虚拟机内存的地方称为本地内存。
1.8.2直接内存是什么
- 受本机物理内存的限制
- 通过JDK1.4加入的NIO(NewInputOutput)类,引入了基于通道和缓冲区的I/O方式,可以使用本地方法库直接分配堆外内存(直接内存),通过存储在Java堆里面的DirectByteBuffer对象作为内存的引用操作,避免了在Java堆和Native堆中来回复制数据,显著提高性能
- 是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率
1.8.3Java程序内存和本地内存
- Java程序内存 = JVM内存(堆、方法区、Java虚拟机栈、本地方法栈、程序计数器) + 本地内存(操作系统分配和管理的内存)
- 本地内存 = 元空间 + 直接内存
2.Java中的引用
2.1直接引用
-
无论是对象的访问定位,还是对象是否可以被回收的判断等,都离不开引用。
-
Java中虚拟机HotSpot通过直接引用来访问Java对象的,直接引用就是说指针是直接指向对象实例的
若想要获取对象的类型数据信息,则需要再调用对象里维护的类型数据指针
2.2引用的强度分类
- Java中引用类型的强弱会决定对象是否能被垃圾回收,主要分四种,强度递减分别是:强软弱虚
2.2.1强引用
- 即GC Root对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收;通过强引用引用的对象就是不可被回收的,可以被保留,最常见的引用类型就是
Object obj = new Object()
,这种new产生的引用就是强引用
2.2.2软引用
- 相对于强引用是一种比较弱的引用关系,当程序使用完一个对象后,就会解除强引用对象;若一个对象只有软引用关联到它,当程序内存不足时,就会将引用中的数据进行回收,释放一定的堆内存,弱回收之后内存不足,才会抛出内存溢出异常
2.2.3弱引用
- 区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。在JDK1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。弱引用对象本身也可以使用引用队列进行回收
2.2.4虚引用
- 最弱的一种引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到系统对应的通知。虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,弱发现还有虚引用,就会将这个虚引用加入到与之关联的引用队列中。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现
3.可达性分析算法
- 在堆中是Java虚拟机进行垃圾回收的主要场所,其次垃圾回收场所是方法区
3.1引用计数法
-
引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1
-
优点:实现简单
-
缺点:
-
每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
-
存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题
A、B实例对象在栈上已经没有变量引用了,由于计数器还是1无法回收,出现了内存泄漏
-
3.2可达性算法
-
可达性分析算法指的是如果从某个普通对象到GC Root对象是可达的(也就是普通对象能通过引用链找到GC Root对象),对象就不可被回收;若引用链不存在就可以被回收。
-
(一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,即GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的)
-
优点:
- 解决了引用计数法的循环引用问题
- 不需要在对对象的引用发生变化时增加或减少引用的技术
可达性分析算法将对象分为两类,且对象与对象之间存在引用关系:
- 垃圾回收的根对象(GC Root),GC Root一般不会被回收
- 普通对象
- 可以作为GC Roots的对象包括
- 在虚拟机栈中引用的对象,例如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
- 在方法区中类静态属性引用的对象,例如Java类的引用类型静态变量
- 在方法区中常量引用的对象,例如字符串常量池(String Table)里的引用
- 在本地方法栈中的Native方法引用的对象
- Java虚拟机内部的引用,例如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)等,还有系统类加载器
- 所有被同步锁(Synchroinzed关键字)持有的对象
- 反映Java虚拟机内部情况的
JM XBean
、JVM TI
中注册的回调、本地代码缓存等
3.3记忆集Remembered Sets
-
当对堆进行部分内存区域回收的时候,就会存在跨区域引用的问题。
-
若存在跨区域的引用关系,那么这种引用即便不是固定GC Roots范畴,也应该被纳入GC Roots集合的补充,一起进行可达性分析判断
(例如所有堆内存被划分为(A、B、C、D、E)五个区,当我们这次只对A、B进行回收时,就需要判断C、D、E中是否有引用A、B中的对象)
-
查找跨区的引用关系:
-
全区域扫描
- 将回收区以外的所有内存区域扫描一遍,看看哪些是有引用回收区里面的对象的
-
记忆集
-
列出了从外部指向本块的所有引用,这种引用记录会在引用关系创建,更改时进行维护。
当需要进行这种外部引用关系分析时,直接读取记忆集中的内容就行
-
-
4.垃圾回收算法
4.0简单了解垃圾回收
4.0.1垃圾回收概念
-
吞吐量:吞吐量越高,垃圾回收的效率越高
-
最大暂停时间:最大暂停时间越短,用户使用系统时受到的影响就越小
-
堆使用效率:不同垃圾回收算法对堆内存的使用方式是不同的,从堆使用效率来说,标记清除 > 复制算法
-
一般来说
- 堆内存越大,最大暂停时间越长,想要减少最大暂停时间,就会降低吞吐量
4.0.2如何判断是否可以在堆中被回收
检查对象是否被引用,若对象被引用了,则说明该对象还在使用,不允许被回收
4.0.3在方法区中的回收,需要同时满足
- 该类所有实例对象都已经被回收,在堆中不存在任何类的实例对象以及子类对象
- 加载该类的类加载器已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用
4.0.4System.gc()的使用
- 调用System.gc()不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求
具体是否需要执行垃圾回收Java虚拟机会自行判断
4.1标记-清除算法
- 标记阶段,使用可达性分析算法,从GC Root开始通过引用链遍历出所有的存活对象,并对存活对象进行标记。
- 清除阶段,从内存中删除没有被标记的也就是非存活对象
-
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象
-
缺点:碎片化问题,内存可能会出现细小的可用内存单元;
分配速度慢,由于内存碎片的存在,需要维护空闲链表,极有可能每次需要遍历到链表的最后才能获得合适的内存空间
4.2复制算法
- 将堆内存平均分配成两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)
- 在垃圾回收GC阶段,将From中存活对象复制到To空间
- 清理掉From空间的内存
- 将两块空间的From和To名字互换
-
-
将堆内存分割成两块From空间 / To空间,对象分配阶段,创建对象
-
GC阶段开始,将GC Root搬运到To空间
-
将GC Root关联的对象,搬运到To空间
-
清理From空间,并把名称互换
-
-
优点:吞吐量高,只需要遍历存活对象并复制到To空间
没有碎片化,在复制对象后按顺序放入To空间,不存在碎片化内存空间
-
缺点:内存使用效率低:每次只能让一半的内存空间来为创建对象使用
-
性能比标记-整理算法好,但是不如标记-清除算法
4.3标记-整理/压缩算法
- 是针对标记-清理算法中产生内存碎片问题的一种解决方案
- 标记阶段,使用可达性分析算法,从GC Root开始通过引用链遍历出所有的存活对象,并对存活对象进行标记。
- 整理阶段,将存活对象移动到堆的一端,清理掉存活对象的内存空间
-
优点:内存使用效率高,整个堆内存都可用,不会像复制算法一样只使用半个堆内存
没有碎片化,在整理阶段将对象向内存的一侧移动,剩下的空间都是可以分配对象的有效空间
-
缺点:整理阶段效率不高,整理算法有多种,但是有的整理算法性能不高,可以使用较为高效的整理算法
4.4分代收集算法
-
采用分代收集算法,根据对象存活周期将内存划分为几块,不同块采用适当的收集算法
-
一般分为新生代和老年代
-
新生代每次在垃圾收集时都发现有大批对象死去,而每次会收回存活的少量对象,将会逐步晋升到老年代中存放
-
新生代:
- 绝大多数对象都是朝生息灭的
- 复制算法
-
老年代:
- 大多数是熬过多次垃圾收集过程的对象
- 标记-清除算法 / 标记-整理算法
-
分代回收时,创建出来的对象,首先会被放入Eden伊甸园区
-
随着对象在Eden区越来越多,若Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC
-
Minor GC会把需要Eden和From需要回收的对象回收,把没有回收的对象放入To区
-
接下来,S0会变成To区,S1会变成From区,当Eden区满时再往里放入对象,依然会发生Minor GC
-
此时会回收Eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入S0
注意:每次Minor GC都会为对象记录他的年龄,初始值为0,每次GC完加1
-
如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代
-
当老年代中空间不足,无法放入新的对象时,现场时Minor GC如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收
-
若Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常
5.垃圾收集器
5.1七大垃圾回收器
5.1.1年轻代-Serial垃圾回收器
-
Serial是一种单线程串行回收年轻代的垃圾回收器
-
回收年代及算法:年轻代-复制算法
-
优点:单CPU处理器下吞吐量非常出色
-
缺点:多CPU下吞吐量不如其他垃圾回收器,堆若偏大会让用户线程处于长时间的等待
-
适用场景:Java编写的客户端程序或硬件配置有限的场景
(运行在客户端模式下的默认新生代收集器)
5.1.2老年代-SerialOld垃圾回收器
- SerialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收
-XX:+UseSerialGC
新生代、老年代都使用串行回收器- 回收年代及算法:老年代-标记整理算法
- 优点:单CPU处理器下吞吐量非常出色
- 缺点:多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待
- 适用场景:与Serial垃圾回收器搭配使用,或者在CMS特殊情况下使用
5.1.3年轻代-ParNew垃圾回收器
- ParNew垃圾回收器本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收
-XX:+UseParNewGC
新生代使用ParNew回收器,老年代使用串行回收器- 回收年代及算法:年轻代-复制算法
- 优点:多CPU处理器下停顿时间较短
- 缺点:吞吐量和停顿时间不如G1,所以在JDK9之后不建议使用
- 适用场景:JDK8及之前的版本中,与CMS老年代垃圾回收器搭配使用
5.1.4老年代-CMS(Concurrent Mark Sweep)垃圾回收器
-
CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程等待的时间
-
XX:+UseConcMarkSweepGC
-
回收年代及算法:老年代-标记-清除算法
-
优点:系统由于垃圾回收出现的停顿时间较短,用户体验好
-
缺点:
-
内存碎片问题
CMS会在Full GC时进行碎片的整理,但是会导致用户线程暂停,可以使用
-XX:CMSFullGCsBeforeCompaction=N 参数(默认为0)调整N次Full GC之后再整理
-
退化问题:在某些特定的情况下会退化成Serial Old的单线程垃圾回收器
若老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代
-
浮动垃圾问题:在清理过程中,有些垃圾可能会回收不掉
无法处理在并发清理过程中产生的”浮动垃圾“,不能做到完全的垃圾回收
-
-
适用场景:大型的互联网系统中用户请求数据量大、频率高的场景,比如订单接口、商品接口等
CMS执行步骤
- 初始标记,用极短的时间标记出GC Roots能直接关联到的对象
- 并发标记,标记所有的对象,用户线程不需要暂停
- 重新标记,由于并发标记阶段有些对象会发生变化,存在错标、漏标等情况,需要重新标记
- 并发清理,清理死亡的对象,用户线程不需要暂停
5.1.5年轻代-Parallel Scavenge垃圾回收器
- Parallel Scavenge是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点
- 回收年代及算法:年轻代-复制算法
- 优点:吞吐量高,而且手动可控。为了提高吞吐量,虚拟机会动态调整堆的参数
- 缺点:不能保证单次的停顿时间
- 适用场景:后台任务,不需要与用户交互,并且容易产生大量的对象,比如大数据的处理,大文件的导出
- 最大暂停时间:
-XX:MaxGCPauseMillis=n
设置每次垃圾回收时的最大停顿毫秒数 - 吞吐量:
-XX:GCTimeRatio=n
设置吞吐量为n(用户线程执行时间 = n / n+1) - 自动调整内存大小:
-XX:+UseAdaptiveSizePolicy
设置可以让垃圾回收器根据吞吐量和最大停顿的毫秒数自动调整内存大小
5.1.6老年代-Parallel Old垃圾回收器
- Parallel Old是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集
- 参数:
-XX:+UseParallelGC
或-XX:+UseParallelOldGC
可以使用Parallel Scavenge+Parallel Old这种组合 - 回收年代及算法:老年代-标记-整理算法
- 优点:并发收集,在多核CPU下效率较高
- 缺点:暂停时间比较长
- 适用场景:与Parallel Scavenge配套使用
5.1.7G1垃圾回收器
-
JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器
-
Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小
-
CMS关注暂停时间,但是吞吐量方面会下降
-
G1则是将两种垃圾回收器的优点融合
-
支持巨大的堆空间回收,并有较高的吞吐量
-
支持多CPU并行垃圾回收
-
允许用户设置最大暂停时间
-
-
G1的整个堆会被划分成多个大小相等的区域,称之为区Region(以Region作为单位),区域不要求是连续的
-
分为Eden、Survivor、Old区
-
Eden区和Survivo区结合起来就是年轻代
-
所有的Old区结合在一起就是老年代
-
Region的大小通过堆空间大小/2048计算得到,也可以通过参数
-XX:G1HeapRegionSize=32m
指定(其中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M -
Region的跨区域引用使用记忆集的方式,通过记录别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内
-
-
如何保证收集线程与用户线程互不干扰地运行
-
回收过程中改变对象的引用关系,必须保证其不能打破原本的对象图结构,导致标记结果出现错误
G1收集器是通过原始快照算法SATB(标记时对象之间的引用状态不变,即使有改变也不在本轮进行垃圾回收)来实现的
-
回收过程中新创建对象,G1为每一个Region设计了两个名为TAMS的指针,将Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置的范围内
-
-
G1垃圾回收的两种方式
-
年轻代回收(Young GC),回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数
-XX:MaxGcPauseMillis=n(默认200)
设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间 -
混合回收(Mixed GC)
-
- 执行流程
-
新创建地对象会存放在Eden区,当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行Young GC
-
标记处Eden 和 Survivor区域中的存活对象
-
根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域
G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域
-
后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬到另一个Survivor区
-
当某个存活对象的年龄到达阈值(默认15)将会被放入老年代
-
部分对象若大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就会被放入Humongous区,若对象过大会横跨多个Region
-
多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时(-XX:InitiatingHeapOccupancyPercent默认45%)会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法完成
-
混合回收分为:
- 初始标记:标记GC Roots引用的对象为存活(需要停顿)
- 并发标记:将第一步中标记的对象引用的对象,标记为存活对象和可回收对象,再处理下SATB记录的有引用变动的对象(无需停顿)
- 最终标记:处理并发阶段结束后,用于处理仍有一些引用改变漏标的对象,以及并发阶段产生新的对象时,重新递归标记且未在当前快照里的新的对象(需要停顿)
- **并发清理:**使用复制算法将存活对象复制到别的Region不会产生内存碎片(需要停顿)
-
-
G1堆老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1名称的由来
-
最后清理阶段使用复制算法,不会产生内存碎片
注意:
- 若清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC。单线程执行标记-整理算法,此时会导致用户线程的暂停。所以尽量保证应该堆内存有一定多余的空间
6.内存分配与回收策略
6.1对象优先在Eden区分配
- 大多数情况下,对象在新生代Eden区中进行分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次新生代GC
6.2大对象直接进入老年代
- 大对象就是指需要大量连续内存空间的Java对象,即长度很长的字符串、元素数量庞大的数组
- 大对象会直接进入老年代,若大对象被分配在新生代,又因为新生代采用复制算法,若大对象存活很久的话,复制开销也将会很大
6.3长期存活的对象将进入老年代
- 对象头中存储了对象的分代年龄,新生代的对象每经历一次Minor GC,年龄就会增加一岁,当年龄达到了一定程度(默认15,- XX:MaxTenuringThreshold(主要是控制新生代需要经历多少次GC晋升到老年代中的最大阈值)),就会晋升为老年代
6.4动态对象年龄判定
- 并不是永远要求对象的年龄必须达到- XX:MaxTenuringThreshold才能晋升老年代,若在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到- XX:MaxTenuringThreshold中要求的年龄
6.5空间分配担保
- Minor GC可能会导致一大批对象从新生代进入老年代,若老年代放不下怎么办
- 每次Minor GC之前都得检查老年代的空间是否能容纳所有新生代的对象
- 若可以就是安全的
- 若不可以,虚拟机会先查看-XX :HandlePromotionFailure参数的设置值是否允许担保失败
- 若允许,则会继续检查老年代的最大可用的连续空间是否大于历次晋升到老年代对象的平均大小
- 若大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的
- 若小于,就进行Full GC
- 若不允许,这时就要改为进行一次Full GC
- 若允许,则会继续检查老年代的最大可用的连续空间是否大于历次晋升到老年代对象的平均大小
- 每次Minor GC之前都得检查老年代的空间是否能容纳所有新生代的对象
7.类文件结构
7.1文件的组成
- 字节码文件中保存了源代码编译后的内容,以二进制的方式存储,无法直接用记事本打开阅读
Class文件主要包括如下的数据类型
7.2文件组成各部分详解
7.2.1魔数与Class文件版本
-
文件无法通过文件扩展名来确定我呢见类型,文件扩展名可用随意修改,不会影响文件的内容
-
软件是使用文件的字节头(文件的前几个字节)去校验文件的类型,若软件不支持该种类型就会出错
文件类型 字节数 文件头 JPEG(jpg) 3 FFD8FF PNG(png) 4 89504E47 bmp 2 424D XML(xml) 5 3C3F786D6C AVI(avi) 4 41564920 Java字节码文件(.class) 4 CAFEBABE -
在Java字节码文件中,将文件头称为Magic魔数
-
主副版本号指的是编译字节码文件的JDK版本号
主版本号用来标识大版本号,JDK1.0-1.1使用了45.0-45.3,JDK1.2是46之后每升级一个大版本就加1;
副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号
(主版本号-44 = 当前的JDK版本)
-
版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容
-
高版本JDK可以执行低版本文件,但是低版本JDK不能执行高版本文件
7.2.2常量池
-
紧接着主、次版本号之后是常量池入口
-
常量池是class文件中关联最多的数据类型,也是占用class文件最大的数据项之一
也是第一个出现的表类型数据项目
-
主要存放两类常量
- 字面量:文本字符串、final常量值等
- 符号引用:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符,方法句柄和方法类型,动态调用点和动态常量等
7.2.3访问标志
- 在常量池结束之后,紧接着的2个字节代表访问标志,主要描述
- Class是类还是接口
- 是否定义为public类型
- 是否定义为abstract类型
- 若是类的话,是否被声明为final,是否为注解,是否为枚举值等
7.2.4类索引、父类索引、接口索引
-
主要是确定该类型的继承关系,即继承了哪些父类,实现了哪些接口等
-
类索引和父类索引都是一个u2数据类型(存储无符号的2字节整数),而接口索引是一个u2类型的数据集合
(一个类可以继承一个父类,但是可以实现多个接口)
7.2.5字段表属性
- 用于描述接口或类中声明的变量,字段(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量,联想一个类中字段的定义信息,
- 主要包括:
- 作用域(public、private、default、protected)
- 是否是static
- 是否是final
- 数据类型(基本类型、对象、数组)等等
7.2.6方法属性
- 与字段表的定义类似,包括作用域、是否是static、是否是Synchronized等
7.2.7属性表属性
- Class文件、字段表、方法表都可以携带自己的属性表集合,描述某些场景专有的信息
8.类加载机制
8.1类的生命周期
-
描述了一个类从加载–>使用–>卸载的整个过程
8.1.1类的加载
- 类的全限定名:指一个Java类的完全限定名,包括包名和类名。在Java中,类名必须是唯一的,而包可以有相同的名称。因此,使用全限定类名可以确保唯一地识别一个类
8.1.1.1类加载阶段需要完成以下三件事
- 通过类的全限定名来以类的二进制流的方式获取该类的字节码信息
- 将该字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表该类的java.lang.Class对象,作为方法区该类的各种数据的访问入口
- 在加载阶段,就只需要访问堆中的Class对象而不需要访问方法区中的所有信息
- 类加载器可以是系统的,也可以是自定义的
- 类的二进制字节流的获取渠道
- 从本地文件系统加载
- 从数据库中获取
- 从zip,jar等文件中获取
- 从网络下载等
8.1.2类的连接-验证
- 主要是检测Java字节码文件是否遵守了Java虚拟机规范中的约束
- 主要包含:
-
**文件格式验证,**比如文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本要求
-
**元信息验证,**比如类必须有父类,默认有父类Object
-
**验证程序执行指令的语义,**比如方法内的指令执行中跳转到其他方法总
-
**符号引用验证,**比如是否访问了其他类中private的方法等
-
8.1.3类的连接-准备
- 准备阶段只会为静态变量(static)分配内存并设置初始值,其初始值根据不同的基本数据类型和引用数据类型的初始值再区分
- 注意:
- 准备阶段只给类变量分配内存,不会给实例变量分配内存
- 准备阶段正常只会赋零值
- final修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值
8.1.4类的连接-解析
-
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
将使用符号描述所引用的目标转为直接引用目标的地址
-
包括:
- 类或接口解析
- 字段解析
- 方法解析
- 接口方法解析
-
符号引用:
使用一组符号来描述所引用的目标,与虚拟机实现的内存布局无关,各种虚拟机实现的内存布局可以各不相同,但是能接受的符号引用必须都是一致,需要符合Java虚拟机规范
-
直接引用:
是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,若有了直接引用,那引用的目标必定已经在虚拟机的内存中存在
8.1.5类的初始化
-
初始化阶段是为类的静态变量赋予正确的初始值(准备阶段是将变量赋零值)
JVM负责对类进行初始化,主要是对类变量进行初始化
-
对类变量进行初始值的设定,主要有两种方式:
-
声明类变量是指定初始值
public static int value = 250
-
使用静态代码块是为类变量指定初始值
public static int value; static {value=250 }
-
初始化步骤
- 若该类没有被加载和连接,则程序先加载该类
- 若该类的直接父类还没有被初始化,则先初始化其直接父类
- 若该类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机
- 只有当对类的主动使用才会导致类的初始化,类的主动使用有以下六种
- 调用类的静态方法
- 访问某个类或接口的静态变量,或对该静态变量赋值
- 初始化某个类的子类,则该类的父类也会被初始化
- 以new的方式创建类的实例
- 反射(如Class.forName(“com.abc.def.Test”))
- Java虚拟机启动时会被标明启动类的类,直接使用java.exe命令来允许某个主类
8.2类加载器和类加载器机制
8.2.1类加载器是什么
- 类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术
8.2.2类加载器的分类
8.2.3类加载器
- 在Java程序中,对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性
- 若一个类被两个不同的加载器加载,即使来源是同一个Class文件,那这也是两个不同的类
- 主要体现在Class对象的equals()方法,isInstance()方法的返回结果,以及使用instance-of关键字做对象所属关系判定的各种情况
8.3JDK8及8以前版本的类加载器
8.3.1虚拟机底层实现
启动类加载器Bootstrap
- 启动类加载器是由Hotspot虚拟机提供,由C++编写的类加载器,主要用于加载Java中最核心的类
- 负责加载存放在<JAVA_HOME>\lib目录的类库
8.3.2Java中实现
- 扩展类加载器和应用程序类加载器都是JDK中提供的、使用Java编写的类加载器
扩展类加载器Extension:
- 扩展类加载器(Extension Class Loader)是JDK中提供的使用Java编写的类加载器,主要用于加载扩展Java比较通用的类(不是特别重要)
- 负责加载存放在<JAVA_HOME>\lib\ext目录的类库
应用程序类加载器Application:
- 应用程序类加载器Application加载的jar包覆盖了启动类加载器Bootstrap和扩展类加载器Extension,主要用于加载应用classpath使用的类文件/第三方依赖中的字节码文件
- 负责加载用户类路径
8.4双亲委派机制(解决类到底由谁加载)
8.4.1双亲委派机制的作用
-
保证类加载的安全性
通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性
-
避免重复加载
双亲委派机制让一个类只能被同一个类加载器加载,就可以避免同一个类被多次加载,减少加载过程中的性能开销
8.4.2类加载的双亲委派机制
- 当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载
- 向下委派加载起到了加载优先级的作用
加载的流程
-
当类加载器发现一个类在自己的加载路径中就会去加载这个类,
-
若没有发现,则从下往上继续查找是否被加载过,
-
若发现被加载了则直接将这个类进行加载,
-
若一直到最顶层的类加载器都没有被加载,则由顶向下进行加载。
-
若所有类加载器都无法加载该类,则会抛出类无法找到的错误
8.4.3双亲委派优先级
- 若一个类重复出现在三个类加载器的加载位置,则由启动类加载器加载,根据双亲委派机制,其优先级是最高的
- 若项目中创建java.lang.String类,不会被加载,因为根据自底向上的查找类加载器过程中会发现启动类加载器早就加载了
8.4.4主动加载一个类
-
使用Class.forName方法,使用当前类的类加载器去加载指定的类
-
获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载
//获取main方法所在类的类加载器,应用程序类加载器 ClassLoader classLoader = Demo.class.getClassLoader(); System.out.println(classLoader);//使用应用程序类加载器加载 com.aaa.bbb.CCC Class<?> clazz = classLoader.loadClass("com.aaa.bbb.CCC"); System.out.println(clazz.getClassLoader());
8.4.5父类加载器
-
每个java实现的类加载器中保存类一个成员变量叫"父"(Parent)类加载器,可以理解为是其上级而不是继承关系
-
应用程序类加载器的父类加载器是扩展类加载器
-
扩展类加载器的父类因为启动类加载器使用C++编写,获取不到,所以为null
-
启动类加载器使用C++编写,没有父类加载器
8.4.6打破双亲委派机制
自定义类加载器
- 重写findClass方法,因为当两个自定义类加载器加载相同限定名的类,不会冲突,因为在同一个java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类
线程上下文类加载器
- 由启动类加载器委派应用程序类加载器去加载类
线程上下文类加载器是否打破了双亲委派机制
-
打破
- 因为这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制
-
未打破
- 因为JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,依旧是通过启动类加载器向下委派直至委派给应用程序类加载器去加载类,类的加载依然遵循双亲委派机制
OSGi模块化
-
OSGi模块化框架,存在同级之间的类加载器的委托加载。OSGi还使用类加载器实现了热部署(不停止服务,动态地更新字节码文件到内存)的功能
8.4.7类加载器在JDK8和JDK9中发生的变化
-
JDK8及之前的版本中
- 扩展类加载器和应用程序类加载器的源码位于rt.jar包
-
JDK9引入了module
-
启动类加载器使用java编写
-
扩展类加载器被替换成了平台类加载器
-
9.常用配置参数
-
设置堆内存
-Xmx4g -Xms4g
-
指定GC算法
-XX :+UserG1GC -XX:MaxGCPauseMillis=50
-
指定GC并行线程数
-XX :ParallelGCThreads=4
-
打印GC日志
-XX :+PrintGCDetails -XX: +PrintGCDateStamps
-
指定GC日志文件
-Xloggc : gc.log
-
指定Meta区的最大值
-XX: MaxMetaspaceSize=2g
-
设置单个线程栈的大小
-Xss1m
-
指定堆内存溢出时的自动进行
-XX :+HeapDumpOnOutOfMemoryError
-XX :HeapDumpPath=/usr/local/