对象内存布局
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding)。
-
对象头
- Mark Word
- MetaData
- 数组长度
-
实例数据
存放类的属性数据信息,包括父类的属性信息
-
对齐填充
由于虚拟机要求 对象起始地址必须是8字节的整数倍, 为了字节对齐
JVM整体结构及内存模型
-
JVM内存参数设置
JVM 内存分配
-
对象栈上分配
栈上分配依赖于逃逸分析和标量替换
JVM通过逃逸分析确定该对象不会被外部访问, 如果不会逃逸可以将该对象在栈上分配内存
-
新对象在eden区上分配
- eden 没有足够内存分配时,会发生minor GC
- 如果新增的对象需要空间超过eden 内存, 会将对象提前移动到老年代中
-
大对象直接进入老年代
-
长期存活的对象将进入老年代
-
对象动态年龄判断
在S区中,一批对象的总大小>内存大小的50% (-XX:TargetSurvivorRatio可以指定),那么>= 这批对象的年龄最大值的对象就可以直接进入到老年代了。
对象动态年龄判断机制一般是在minor gc之后触发的
-
老年代空间分配担保机制
垃圾收集算法
垃圾回收器
Serial收集器 (单线程)
-XX:+UseSerialGC -XX:+UseSerialOldGC
新生代使用
复制算法
,老年代使用标记-整理算法
。
Parallel Scavenge 收集器 (多线程)
-XX:+UseParallelGC(年轻代), -XX:+UseParallelOldGC(老年代)
重点关注
吞吐量
新生代使用
复制算法
,老年代使用标记-整理算法
。JDK8默认的新生代和老年代收集器
ParNew 收集器
-XX:+UseParNewGC
ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。
新生代使用
复制算法
,老年代使用标记-整理算法
。
CMS 收集器
CMS(Concurrent Mark Sweep) 以 最短 回收停顿时间(STW)为目标
回收算法:
标记清除算法
- 初始标记:暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
- **并发标记:**是从GC Roots的直接关联对象开始遍历整个对象图的过程。
- 重新标记:修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象。 主要用到三色标记里增量更新算法(见下面详解)做重新标记。
- 并发清理: 对未标记的区域做清扫, 期间有新增对象直接标记为黑色。
- 并发重置: 重置本次GC 过程中标记数据。
在并发标记的过程中, 通过
三色标记
来记录对象的是否遍历访问。
- 黑色:已全部扫描标记完成
- 灰色:部分扫描标记完成
- 白色:未扫描标记
多标-浮动垃圾
漏标-读写屏障
增量更新(IncrementalUpdate)
增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之
后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向
白色对象的引用之后, 它就变回灰色对象了。
原始快照(Snapshot At The Beginning,SATB)
原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后,
再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑
色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)
G1收集器
将堆空间分成大小相同的独立区域(Region),默认2048个
G1保留了 年轻代、老年代概念,但是没有物理隔阂了。
-XX:+UseG1GC
G1 (Garbage-First)是一款面向服务器的垃圾收集器, 主要针对配备多颗处理器及大容量内存的机器 . 以极高概率满足GC
停顿时间要求的同时,还具备高吞吐量性能特征.
回收算法:复制算法
- 初始标记:(STW) 暂停其他所有线程,并记录GC Root 直接引用的对象, 速度很快。
- 并发标记: 同
CMS
并发标记 - 最终标记:(STW) 同
CMS
重新标记 - 筛选回收:(STW) 对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间 制定回收计划
G1垃圾收集分类
YoungGC
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 `-XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的,那么就会触发Young GC
MixedGC
不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent
)设定的值则触发,回收所有 Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法
,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够 的空region能够承载拷贝对象就会触发一次Full GC
Full GC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这 个过程是非常耗时的。(Shenandoah优化成多线程收集了)
什么场景适合使用G1
-
50%以上的堆被存活对象占用
-
对象分配和晋升的速度变化非常大
-
垃圾回收时间特别长,超过1秒
-
8GB以上的堆内存(建议值)
-
停顿时间是500ms以内
ZGC收集器
-XX:+UseZGC
ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器
-
并发标记(Concurrent Mark):与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记(Mark Start)和最终标记(Mark End)也会出现短暂停顿,与G1不同的是, ZGC的标记是在指针上而不是在对象 上进行的, 标记阶段会更新染色指针中的Marked 0、 Marked 1标志位。
-
并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成分配集(Relocation Set)。ZGC每次回收都会扫描所有的 Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
-
并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。
1 ZGC的颜色指针因为“自愈”(Self‐Healing)能力,所以只有第一次访问旧对象会变慢, 一旦重分配集中某个Region的存活对象都复制完毕后,
2 这个Region就可以立即释放用于新对象的分配,但是转发表还得留着不能释放掉, 因为可能还有访问在使用这个转发表。
- 并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。
调优指令与工具
常用指令
-
jps 获取java线程
-
top -HP 获取每个线程,显示你的java进程的内存情况,pid是你的java进程号
printf “%x\n” 25226 // 需要将java 进程号转化为16进制, jstack中显示的进程号的都是16进制。
-
jmap 查看java内存信息
jmap -histo pid 查看内存信息
jmap -heap pid 查看堆信息
-
jstack 看看java线程信息
-
jstat 查看堆内存各部分的使用量,以及加载类的数量
jstat -gc pid 评估程序内存使用及GC压力整体情况
jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次)
-
jinfo 查看正在运行的Java应用程序的扩展参数
常用工具
- Arthas
- JProfile
- JVisualVM
Class常量池与运行时常量池
Class 常量池
Class常量池:可以理解为是Class文件中的资源仓库。 Class文件中除了包含类的版本、字段、方法、接口等描述信息外, 还有一项信息就是常量池(constant pool table),用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References)。
class 常量池是静态常量池,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息。
运行时常量池
运行时常量池: class 静态常量池被装入内存后,就成了运行时常量池。
- 运行时常量池位于永久代 (Jdk 1.6 之前)
- 去永久代后,运行时常量池位于方法区 (Jdk 1.7 之后)
字符串常量池
为提高性能和减少内存开销。
为字符串开辟一个字符串常量池,类似于缓存区
创建字符串常量时,首先查询字符串常量池是否存在该字符串
存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
字符串常量池的位置:
Jdk1.6及之前: 有永久代, 运行时常量池在永久代,运行时常量池包含字符串常量池
Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里
Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里