JVM笔记
一、内存结构
执行过程
- 类放在方法区 Method Area中
- 类创建的实例(对象)放在堆Heap中
- 堆里的对象调用方法时,会用到虚拟机栈 JVM Stacks,程序计数器 PC Register和本地方法栈 Native Method Stacks
- 每行代码由解释器Interpreter逐行执行
- 热点代码由即时编译器 JIT Compiler执行
- GC模块 垃圾回收模块会对堆中不再使用的实例进行回收
- java代码不方便实现的功能,必须调用底层操作系统的功能,就需要调用本地方法接口
1、程序计数器
- 作用:记住下一条jvm指令的执行地址
- 特点
- 线程私有的 (每个线程都有自己的程序计数器,随着线程创建而创建,随着线程销毁而销毁)
- 不会存在内存溢出
- 是一块较小的内存空间
2、虚拟机栈
2.1 定义
栈-线程运行需要的内存空间,每个线程都需要一个栈,多个线程运行就会有多个虚拟机栈
- 一个栈由多个栈帧组成
- 一个栈帧对应着一次方法的调用
栈帧-每个方法运行时需要的内存
如果方法一调用方法二,方法二会生成一个栈帧,压入栈中,方法运行完毕,弹出栈。
问题辨析
- 垃圾回收是否涉及栈内存?
垃圾回收涉及堆,并不涉及栈内存 - 栈内存分配越大越好吗?
栈内存分配越大,线程数会越少,所以不是栈内存分配越大,程序运行越快 ,因为总共的物理内存是一定的。 - 方法内的局部变量是否线程安全?
线程安全,当线程1运行一个方法,定义一个局部变量,进行操作,同时,线程2也调用运行同一个方法,会在线程2 中再定义一个局部变量,互不影响,所以方法内的局部变量是线程安全的。- 局部变量是线程私有的。
- 全局变量是线程共有的,是需要考虑线程安全问题(不是线程安全的)。
- 当变量是参数,或者是返回值的时候==(逃离了线程的作用范围)==也是需要考虑线程安全问题(不是线程安全的)。
2.2 栈内存溢出 StackOverflowError
- 栈帧过多导致栈内存溢出。(例如不正确的递归调用,递归时没有正确结束)
- 栈帧占用内存过大。(不太容易出现)
2.3 线程运行诊断
- 案例1:CPU占用过多(while死循环)
- 案例2:程序运行很长时间没有结束(发生死锁)
3、本地方法栈 Native Method Stacks
本地方法(Native)使用的内存空间-本地方法栈
有一些java代码不方便实现的功能,必须调用底层操作系统的功能,就需要调用本地方法接口的本地方法
4、堆
4.1定义
- 通过new关键词,创建的对象都会使用堆内存
特点 - 它是线程共享的,堆中的对象都需要考虑线程安全的问题
- 有垃圾回收机制
4.2 堆内存溢出 OutOfMemoryError
创建的对象不能被回收,并且仍在扩大对象所占用的内存。
4.3堆内存诊断
1.jps工具
- 查看当前系统有哪些java进程
2.jmap工具
- 查看堆内存占用情况 jmap -heap $id
3.jconsole工具
- 图形界面的,多功能的检测工具,可以连续监测
5、方法区
5.1定义
方法区是所有jvm虚拟机共享的区;它存储了跟类的结构相关的信息,有成员变量,方法数据和成员方法以及构造方法的代码部分,包括特殊的方法(类的构造器)
方法区在虚拟机启动时被创建,逻辑上是堆的组成部分
5.2 组成
在1.6被称为永久代
在1.8之后也是一种概念,永久代被替代,由元空间实现,包括类,类加载器和常量池,不再占用堆内存,不是由jvm管理内存结构,由本地内存管理。
5.3方法区内存溢出
- 1.8以前会导致永久代内存溢出
- 1.8之后会导致元空间内存溢出
场景
- spring
cglib 动态代理 - mybatis
5.4运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息。
- 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
5.5 StringTable特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder(1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
- 1.8将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,然后都会将串池中的对象返回
在这里串池中有ab 所以定义s2时给s2返回的是串池中的对象,所以是true,而串池中原本就已经有ab,所以尝试将s放入串池时会失败,也就是s并没有被放入串池中所以为false - 1.6将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把对象复制一份,放入串池,然后都会将串池中的对象返回
- 1.8将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,然后都会将串池中的对象返回
5.6 StringTable的位置
永久代只有FullGC的时候才会垃圾回收
5.7 StringTable垃圾回收
当内存分配失败的时候会进行垃圾回收
5.8 StringTable性能调优
如果应用里有大量的字符串,并且可能存在字符串重复的情况,我们可以选择将字符串入池,来减少对应字符串个数,减少内存的使用
intern入池
6、直接内存
6.1定义
- 常见于NIO操作时,用于数据缓冲区(ByteBuffer)
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
正常io
正常io进行读写的时候,通过调用本地System方法,来进行读写,进行读写时,首先将磁盘中的信息先拷贝到系统缓冲区,再从系统缓冲区拷贝到java缓冲区,由此可见拷贝了两次,浪费了内存,又浪费了时间
ByteBuffer(直接内存)
由图可见,直接内存在系统缓冲区和Java堆内存之上相当于共享的区域,Java代码可以直接访问,系统内存也可以直接访问,这就节省了很多的大量的时间
6.2分配和回收原理
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用free Memory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory方法来释放直接内存
显示的垃圾回收,就是由程序员在程序中运行的System.gc(),这是一个Full GC,比较影响性能,不仅回收老年代,也回收新生代,会造成程序暂停时间比较长
通过 -XX:DisableExplicitGC参数来使gc方法无效
二、垃圾回收(*)
1、如何判断对象可以回收
1.1引用计数法
当方法被引用时计数+1,当为0的时候被回收
弊端
当其他方法没用引用时理论上需要被回收但是A和B在循环引用,引用计数无法归0,导致无法被回收,造成内存泄露
1.2可达性分析算法
根对象:就是肯定不能被当作垃圾被回收的对象
先对所有方法和对象进行扫描,判断是否被根对象直接或间接引用,如果被引用就不能被垃圾回收,如果没有被引用,就可以被看作是垃圾,将来可以被垃圾回收。
1.3四种引用(*)
1.3.1强引用
例如new了一个对象,将这个对象通过等号=来辅助给一个变量,那么这个变量就是强引用了这对象。
- 当所有强引用都断开的时候,垃圾回收发生时就会被垃圾回收。
1.3.2软引用
- 当垃圾回收时,被软引用引用时并且内存不足时,会被回收掉
- 可以配合引用队列来释放软引用自身
1.3.3弱引用
- 当垃圾回收时,被弱引用引用时无论内存是否充足,都会被回收掉
- 可以配合引用队列来释放弱引用自身
1.3.4虚引用
- 必须配合引用队列,虚引用对象被垃圾回收时,会将ByteBuffer对象直接回收,然后将虚引用对象存放直接内存地址,并放入引用队列,来对直接内存进行垃圾回收
1.3.5终结器引用
- 垃圾回收时,先将终结器引用放入引用队列,然后会有一个优先级很低的线程,去检测引用队列,检测是否有终结器引用,如果有,先调用对象的finallize()方法,然后再对其进行垃圾回收
2、垃圾回收算法
2.1标记清除
优点:速度快
缺点:容易产生内存碎片
2.2标记整理
优点:没有内存碎片
缺点:效率低,速度慢
2.3复制
优点:速度快,没有内存碎片
缺点:占用内存较大
3、分代垃圾回收
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发Minor GC,伊甸园和from存活的对象使用copy算法复制到To区中,存活的对象年龄加1并且交换From和To
- Minor GC会引发stop the world (所有的其他用户线程都暂停,等待Minor GC完成,用户线程恢复运行)
- 当某个年龄到达预定值(最大寿命是15(4bit ---- 1111)),会晋升至老年代中
- 当新生代和老年代内存都不足时,会触发Full GC,将新生代和老年代的内存都进行一次垃圾回收STW的时间更长。
- 大对象:当对象大小大于伊甸园大小,并且小于老年代大小,此时会将该对象直接晋升到老年代中。
4、垃圾回收器
4.1串行
4.2吞吐量优先
1.8默认开启
4.3响应时间优先
有可能会造成碎片过多,导致并发失败,从而退化成串行,导致响应时间突然变长。
4.4 G1
定义:Garbage First
- 2004论文发布
- 2009JDK 6u14体验
- 2012 JDK 7u4官方支持
- 2017 JDK 9 默认(取代了CMS)
使用场景: - 同时注重吞吐量和低延迟,默认的暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的Region
- 整体上是标记+整理的算法,两个区域之间是复制算法
4.4.1 G1垃圾回收阶段
4.4.2 Young Collection
- 会STW
4.4.3 Young Collection +CM
- 再Young GC时会进行GC Root的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
4.4.4 Mixed Collection
会对E、S、O 进行全面垃圾回收
- 最终标记会STW
- 拷贝存活会STW
会回收垃圾较多的老年代区,并不是所有都拷贝。
4.4.5 Full GC
4.4.6 新生代跨代引用
- 新生代回收的跨代引用(老年代引用新生代)问题
4.4.7 Remark
在理论上,当B强引用C断开后,C被标记为白色,在结束后会被回收掉,然而时并发处理,此时如果有用户,将A强引用C,而C还被标记为白色,结束后仍会被回收,就出现了问题
为了解决这个问题,就出现了一个 写屏障,当强引用断开时,也就是状态改变时,会触发写屏障,将C放入处理队列 (stab_mark_queue) 中,置为灰色,结束后会逐一对队列中的对象进行判断,如果没有被引用就置为白色,被回收掉,如果仍有被引用就置为黑色。
CMS中是将A放入队列
4.4.8 JDK 8u20字符串去重
- 优点:节省大量内存
- 缺点:略微多占用了CPU时间,新生代回收时间略微增加
- 当所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复
- 如果它们值一样,让他们引用同一个char[]
- 注意,与String.intern()不一样
- String.intern()关注的是字符串对象
- 而字符串去重关注的是char[]
- 在JVM内部,使用了不同的字符串表
4.4.9 JDK 8u40 并发标记类卸载
4.4.10 JDK 8u60 回收巨型对象
4.4.11 JDK 9 并发标记起始时间的调整
5、垃圾回收调优(*)
5.1 调优领域
- 内存
- 锁竞争
- CPU占用
- io
5.2 确定目标
- 【低延迟】还是稿吞吐量,选择合适的回收器
- CMS,G1,ZGC
- ParallelGC
- Zing
5.3最快的GC是不发生GC
- 查看FullGC的内存占用,考虑下面几个问题
- 数据是不是太多?
- resultSet=statement.executeQuery(“select * from 大表”)
- 数据表示是否太臃肿?
- 对象图
- 对象大小
- 是否存在内存泄漏
- 数据是不是太多?
5.4 新生代调优
- 新生代特点
- 所有的new操作的内存分配非常廉价
- TLAB thread-local allocation buffer
- 死亡对象的回收代价是0
- 大部分对象用过即死
- Minor GC的时间远远低于Full GC
- 所有的new操作的内存分配非常廉价
最简单的新生代调优就是将新生代大小变大
- 但是真的是越大越好吗?
当新生代大小太大时,就意味着老年代的大小很小,新生代大小很充裕,老年代大小会很紧张,当老年代大小紧张时,会触发Full GC这要比Minor GC的时间更长
Oracle建议是新生代大小大于堆大小的25%小于50%
新生代能容纳所有【并发量*(请求-相应)】的数据
幸存区大到能保留【当前活跃对象+需要晋升的对象】
晋升阈值配置得当,让长时间存活的对象尽快晋升
5.5 老年代调优
5.6 案例
链接: gceasy.io
三、类加载器与字节码技术
1.类文件结构
1.1 魔数
1.2 版本
1.3常量池
1.4 访问标识与继承信息
1.5 Field信息
1.6 Method信息
1.7 附加属性
2.字节码指令
2.1 入门
2.2 javap工具
2.3 图解方法执行流程
2.4 i++分析
2.5 条件判断指令
- byte,short,char都会按int比较,因为操作数栈都是4字节
- goto用来进行跳转带指定行号的字节码
2.6 循环控制指令