声明: 1. 本文为我的个人复习总结, 并非那种从零基础开始普及知识 内容详细全面, 言辞官方的文章
2. 由于是个人总结, 所以用最精简的话语来写文章
3. 若有错误不当之处, 请指出
JVM 内存结构:
JVM内存结构 = 类加载器 + 执行引擎 + 运行时数据区(堆, 虚拟机栈, 本地方法栈, 方法区, PC寄存器)
方法区、永久代、元空间:
逻辑上的规范叫做方法区, 真正具体的实现 在1.7叫做永久代, 在1.8叫做元空间(不使用JVM内存了, 而是使用操 作系统的物理内存)
1.7时 永久代的常量池这部分 搬到堆中; 1.8时又从堆中搬出来, 永久代也彻底消失, 改为叫元空间
栈是线程私有, 堆和方法区是线程共享
程序计数器 不会出现内存溢出
方法区存储: 普通常量, final修饰的常量引用, 字符串, 类元数据信息、字节码、即时编译器需要的信息等
注意 final修饰的常量引用在方法区, 指向的对象在堆中
类文件结构:
-
魔数:class 文件标志
-
文件版本
-
常量池(class常量池):存放字面量和符号引用
字面量是类相关的常量,如字符串类型的属性 或 声明为final的常量值等
符号引用包含三类:类和接口的全限定名 & 方法的名称和描述符 & 字段的名称和描述符
-
访问标志:识别一些类的访问信息
包括:这个 Class 是类还是接口,是否为 public, abstract, final类型
-
当前类索引 this_class:类索引用于确定这个类的全限定名
-
属性表集合:字段表, 方法表中都可以携带自己的属性表集合, 以用于描述一些信息
class常量池 运行时常量池 和 字符串常量池:
运行时常量池
=class常量池
内容+字符串常量池
内容
class常量池
只是个中间媒介场所, 存放字面量和符号引用, 在运行时它会被加载到 运行时常量池
字符串常量池
存放 普通的字符串常量
运行期间动态生成的常量 如 String 类的 intern( )方法,会被放入运行时常量池
字符串常量池的字符串 在内存不足时也是可以被回收的
逻辑上都是方法区的一部分
1.7时 永久代的常量池这部分 搬到堆中, 1.8时又从堆中搬出来, 并称为元空间
使用栈存储对象, 栈上分配:
栈其实也可以用来存储对象, 弹栈操作就像是垃圾回收;
不过需要先进行内存的逃逸分析, 如果这个内存逃出了局部方法的作用范围, 就不应该使用栈上分配了;
因为弹栈是要清空栈帧的, 这个栈上对象也就被回收了, 而此对象内存逃逸出去了可能还依旧被别人使用着
解释器 & JIT即时编译器:
-
解释器是把字节码解释成二进制码, 每一遍都要重新解释
-
JIT是后端编译器, 把字节码编译成二进制码, 那样以后遇到相同的字节码就不需要重新编译了;
JIT进行即时编译也是消耗较大的, 所以只用来将热点代码进行编译缓存, 普通非热点代码用JIT编译反而会拖慢速度
Java对象:
Java对象=对象头+实例数据+对齐填充
对齐填充 是为了 凑够8的倍数, 使寻址起来方便
JVM只要堆内存不超过32G, 默认都是开启指针压缩的
对象头:
markword(8Byte)+klass(压缩后4Byte, 否则8Byte)+length(4Byte, 这个只有数组才有)
所以new一个Object 即最小对象, 占用16字节(32位JVM 或 开启指针压缩的64位JVM)
- markword存储 哈希码, 对象分代年龄, 锁相关信息
- klass指针, JVM通过此指针来 确定对象属于哪个类
Calss是元数据模板
Class实例的位置在堆中, 方法区中Class的数据结构(类元数据信息) 指向了 堆中的Class实例
类加载器 什么时候被回收?
此类加载器加载过的所有 放在堆中的Class实例都被回收完了, 此类加载器才会被回收
类什么时候被卸载(即元空间的内存什么时候回收)?
- 该类所有的对象都已经被回收
- 且 该类不再被别的类使用
- 且 加载该类的 ClassLoader 已经被回收
一个函数对应一个栈帧, 栈帧拥有: 局部变量表、操作数栈、动态链接、方法出口信息
- 部分符号引用 在类加载阶段的时候就转化为直接引用(调用属性),这种转化就是静态链接
- 部分符号引用 在运行期间转化为直接引用(调用方法),这种转化就是动态链接
对象的访问定位:
- 使用句柄, 多了一步先找到句柄, 句柄里存的是指针
- 使用直接指针
JVM 内存参数:
堆空间内存设置:
按大小设置:
-
-Xms 最小堆内存
-
-Xmx 最大堆内存
-
-Xmn 设置新生代的大小
保留区域: 必要时才会使用, Xmx-Xms=保留内存
建议将 -Xms 与 -Xmx 设置为大小相等, 即不需要保留内存, 不需要从小到大增长, 这样性能较好
按比例设置:
-XX:NewRatio=2:1 表示老年代占两份,新生代占一份
-XX:SurvivorRatio=4:1 表示新生代分成六份,伊甸园占四份,from 和 to 各占一份
元空间内存设置:
代码缓存内存设置:
如果 -XX:ReservedCodeCacheSize >= 240m,则代码分成三个区域:
-
non-nmethods JVM 自己用的代码
-
profiled nmethods 部分优化的机器码
-
non-profiled nmethods 完全优化的机器码
如果 -XX:ReservedCodeCacheSize < 240m,则它们存在一起 不进行区分
GC垃圾回收:
GC回收的主要是堆区
垃圾回收时会阻塞别的线程(STW), 因为垃圾回收可能要改变对象的地址, 期间不能让别的线程使用
内存泄露: 不再使用的内存没有被回收
造成原因:
1. 可能是短期内来不及回收(不停创建不可变的String对象),
2. 或者是它还被别的没必要的强引用指向着(ThreadLocal key设为弱引用原理)
3. 静态属性过多, 可能导致内存泄漏(生命周期太长了, 所指向的对象就无法被回收)
内存溢出: 内存不够了
- 使用Executors造的线程池, 等待队列容量太大, 或者允许开辟的线程数太多
- 查询数据量太大, 如MyBatis中没有使用过滤或分页查询
判断是否为垃圾:
-
引用计数法: 记录此对象被多少引用指向着; 当对象没有被任何引用指向着时, 就视为垃圾
这种方法不好, 无法解决对象之间互相循环引用的问题:
当A使用着B, B也使用着A时, 就会导致A, B两个对象永远无法被回收
-
可达性分析法: 没被GCRoots对象 直接或间接使用着的对象, 就视为垃圾
-
三色标记法: 即用三种颜色记录对象的标记状态
- 黑色 已标记
- 灰色 标记中
- 白色 未标记
并发漏标问题:
先进的垃圾回收器都支持并发标记,即在标记过程中,用户线程仍然能工作
缺点: 如果标记存活对象后 用户线程修改了引用指向, 使引用指向了原本的垃圾对象, 那么就存在漏标问题, 将会把存活对象给回收掉
解决方案:
进行重新标记: GC会将并发标记过程中修改了 引用指向 的引用记录下来(即记录了漏标的对象);
然后阻塞用户线程, 对这部分漏标的对象进行重新标记
可作为GCRoots对象的有哪些?
- 虚拟机栈中 引用的对象
- 方法区中 static属性引用的对象
- 方法区中 final常量引用的对象
- native 方法中引用的对象
比例划分:
新生代:老年代 = 1:2
eden区:from区:to区 = 8:1:1
三种垃圾回收算法:
标记的是存活的对象, 而不是垃圾
-
标记清除
清除未标记的 对象占用的内存
优点: 省空间, 回收速度快
缺点: 产生大量内存碎片
-
标记整理
在标记清除后, 多了一步整理操作, 即将存活对象向一端移动,可以避免内存碎片产生
优点: 省空间, 不会产生内存碎片
缺点: 回收速度慢, 因为多了一步整理操作
-
标记复制
-
from 存储新创建的对象, to 处于空闲
-
将 未被标记的存活对象从 from区 复制到 to 区, 复制的过程中完成了碎片整理
-
复制完成后,交换from和to的位置
优点: 回收速度快, 不会产生内存碎片
缺点: 占用2倍的内存空间
-
优缺点是从 占用空间, 回收速度, 和内存方面考虑的
分代:
堆区分为不同的代: 新生代(伊甸园+from区+to区)+老年代
from区又称为幸存者0区, to区称为幸存者1区
不同的代(新生代, 老年代)使用不同的垃圾回收算法:
-
新生代 采用标记复制算法
因为标记复制算法 在对象存活率较高时 会进行比较多的复制操作, 效率会变低; 所以此算法适用于对象存活率低的
-
老年代 采用标记整理算法
从新生代 晋升到老年代的条件:
- 年龄达到15岁
- 或 幸存区内存不足
老年代对象存活率高, 不好回收, 且空间大
GC 规模:
- Minor GC 新生代 的垃圾回收,暂停时间短
- Mixed GC 新生代+老年代部分区域 的垃圾回收,是G1垃圾回收器特有的
- Full GC 新生代 + 老年代所有区域 的垃圾回收,暂停时间长,应尽力避免
直接内存
并不是虚拟机运行时数据区的一部分
使用JDK的Unsafe类进行操作
NIO的DirectBuffer用的就是直接内存
优点: 速度快, 因为
- 避免了 堆内内存 到 堆外内存的数据拷贝操作
- 避免了GC垃圾回收损耗
对于 需要频繁进行内存间数据拷贝 且 生命周期较短的 暂存数据,建议存储到直接内存
内存不足溢出时, 也可能导致 OutOfMemoryError 错误的出现
垃圾回收器:
-
Parallel GC
多个GC线程并行处理
- eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程
- old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
- 注重吞吐量
GC吞吐量: 单次暂停时间长点,但是总的来说一段时间内处理数据量多
-
ConcurrentMarkSweep GC (CMS)
Gc&用户线程 并发运行
- 工作在老年代,支持并发标记,采用并发清除算法
- 并发标记时 不需暂停用户线程; 重新标记时 仍需暂停用户线程
- 新生代回收代价小,没必要并发标记,STW时长是可以忍受的
- 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
注重响应时间
-
G1 (是最好的)
-
划分成多个区域,每个区域都可以充当 eden, survivor, old, humongous(专为大对象准备)
跨代引用 & 记忆集优化:
跨代引用:
由于对象之间会存在少量的跨代引用(老年代对象依赖着新生代对象时),如果要进行一次新生代垃圾收集,除了需要遍历新生代对象,还要额外遍历整个老年代的所有对象,这会给内存回收带来很大的性能负担。
记忆集优化:
没必要为了少量的跨代引用去扫描整个老年代,只需在新生代建立一个Remembered Set(记忆集)
这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。
此后发生GC时,老年代只有那些包含了跨代引用的小块内存才会被扫描
-
分成三个阶段:新生代回收、并发标记 & 重新标记、Mixed Gc
-
如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
-
老年代对象占用超过堆空间的45%时, 才进行部分回收那些 回收性价比最高的垃圾(垃圾密度更高的区域)
响应时间与吞吐量兼顾
-
类加载:
类加载器 & 双亲委派机制 单独写成一个文档
沙箱安全机制:
沙箱 是一个限制程序运行的环境, 对权限进行限制, 对系统资源的访问进行限制
类加载的过程:
-
加载: 将类的字节码载入方法区,并创建 类.class实例
加载是懒惰执行
-
链接
-
验证 验证类是否符合 Class 规范,并进行合法性、安全性检查
-
准备 为
static变量
分配空间并赋默认值 为
static final 的基本数据类型 或String类型 的常量
分配空间并赋指定的值(编译期间就 已经可以计算出来了) -
解析 将常量池的 符号引用(class文件里的字符符号) 解析为 直接引用(实际分配内存后的物理地址)
-
-
初始化: 为
static变量
赋指定的值
static final 的非String的其他引用类型 的常量
赋指定的值
- 使用
static final 的基本数据类型 或String类型 的常量
(即准备阶段就赋指定的值的那部分, 在编译期就已经可以计算出值了)时, 不会触发类加载 - 使用
static final 的非String的其他引用类型 的常量
时, 会触发类加载 - 使用static 非final的变量时, 都会触发类加载
如果整数较小不超过short最大值, 则直接就以字面量形式写到字节码上; 否则, 数据就放到常量池里, 然后去常量池里取
类的生命周期:
加载、验证、准备、解析、初始化、使用 和卸载
四种引用:
-
强引用 正常new的对象, Student stu=new Student( )这样
-
软引用 仅有软引用指向该对象时, 首次垃圾回收时不会回收该对象, 如果下一次回收时内存仍不足才会回收该对象
引用自身的释放, 需要配合引用队列来释放
实际应用: 反射时的那些Method类Field类
-
弱引用 仅有弱引用指向该对象时, 只要发生垃圾回收,就会回收该对象
引用自身的释放, 需要配合引用队列来释放
实际应用: ThreadLocalMap 中的 Entry类的对象
-
虚引用 仅有虚引用指向该对象时, 在任何时候都可能被垃圾回收, 用来 释放外部内存&自身占用内存
流程:
1: 传入到虚引用类的构造器里的Java对象, 在GC时 会将这些Java对象加入到引用队列
2: 后续借此队列找到这些Java对象调用其clean的逻辑, 清理释放掉外部不再使用的内存
引用自身的释放 和 外部内存的释放, 需要配合引用队列来释放
实际应用: Cleaner 释放 DirectByteBuffer 关联的直接内存
当一个对象既被弱引用指向着, 又被强引用指向着时, 是不会在下一次GC时被当作垃圾进行回收的