48

【JVM 知识体系框架】(不断更新) - - SegmentFault 思否

 4 years ago
source link: https://segmentfault.com/a/1190000020540570?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JVM 内存分布

  • 线程共享数据区:

方法区->类信息,静态变量
堆->数组对象

  • 线程隔离区

虚拟机栈-> 方法
本地方法栈->本地方法库 native

  • 堆、程序计数器
  • JVM 运行数据

1460000020597790?w=1936&h=1438

程序计数器

线程隔离 ,比较小的内存空间,当前线程所执行的字节码的行号
线程是一个独立的执行单元,由 CPU执行
唯一没有 OOM 的地方,由虚拟机维护,所以不会出现 OOM

执行的是Java方法

方法的调用就是栈帧入虚拟机栈的过程
栈帧:局部变量表(变量) 、操作数栈(存放a+b的结果 )、 动态链接(对对象引用的地址),方法出口(return的值)
线程请求的栈深度大于虚拟机所允许的深度StackOverflowError

本地方法栈

执行的是 native 方法的一块 java内存区域,一样有 栈帧
hotspot将 Java 虚拟机栈和本地方法栈合二为一
jvm标准是 java 虚拟机栈和本地方法栈分开

java内存中存放对象实例的区域,几乎所有的对象实例都在这里分配
所有线程共享
新生代、老年代
jmap -heap pid;

各个线程共享的内存区域
存储已被虚拟机加载的类的信息、常量、静态变量、即时编译器编译后的代码等数据
Hotspot用永久代实现方法区(让垃圾回收器可以管理方法区),对常量池的回收和卸载
方法区会抛出 OOM,当他无法满足内存分配需求时

运行时常量池

运行时常量池是方法区的一部分,Class 中除了字段、方法、接口的 常量池,存放编译器生成的字面量和符号引用,这部分内容由类加载后进入方法区的运行时常量池中存放。

StringTable是HashSet结构
方法区的一部分,受到方法区的限制,依然会 OOM

Java 对象创建过程

1460000020597791?w=1614&h=1588
<init> -> static方法 static代码块

  1. new 指令,判断在常量池中有没符号引用,有则已被加载过
  2. 判断类是否被加载、解析、初始化
  3. 为新生对象在java堆里分配内存空间

1) 指针碰撞(内存比较整齐)
1460000020597792?w=1022&h=922
步骤:1. 分配内存 2. 移动指针,非原子步骤可能出现并发问题,Java虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性
2)空闲列表(内存比较乱)
存储堆内存空闲地址
步骤:1.分配内存 2. 修改空闲列表地址 非原子步骤可能出现并发问题,Java虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性

  1. 将分配到内存空间都初始化零值
  2. 设置对象头相关信息 (GC分代年龄、对象的HashCode、元数据信息)
  3. 执行<init>方法

Java 对象内存布局

对象属性的值->实例数据
对象头 64 位机器存 64 位,32 位机器存 32 位,8 的倍数
1460000020597793?w=2220&h=1516

Java 对象的访问

  1. 直接指针访问

1460000020597794?w=1336&h=1188

1460000020597795?w=1956&h=1742
对比:

  1. 访问效率:直接指针访问效率高(hotspot采用这种方式)
  2. 垃圾回收:句柄访问效率高,垃圾回收只用更新句柄池,而直接指针访问方式则要更新 reference地址

垃圾回收算法

引用计数器

1460000020597796?w=2050&h=1798
当对象实例分配给一个变量时,该变量计数设置为 1,当任何其他变量被赋值为这个对象的引用的时,计数+1 (a =b,则b的引用对象实例计数器+1),当一个对象实例的某个引用超过了生命周期(方法执行完)或者被设置为一个新值,则该对象的实例引用计数器 -1
无法解决循环引用
可达性分析
GC Root (虚拟机栈中的引用的对象、本地方法栈中引用的对象、方法区静态属性引用的对象、方法区常量引用的对象)

标记-清除

1460000020597797?w=2438&h=1584
标记需要回收的对象,在标记完成后统一回收
不足:
1.效率问题,标记清除 2 个过程效率都不高
2.空间问题,标记清除后产生大量不连续的内存碎片,碎片过多当程序需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发一次垃圾回收动作

标记-复制

1460000020597798?w=2480&h=1580
内存块 A存活的对象复制到内存块 B (Survivor to)里,然后将内存块A (Eden + Survivor from)清空,
只有少部分对象移动,更多的对象是要被回收的
Eden:Survivor from:Survivor to=8:1:1
98%对象“朝生夕亡”,新生代可用内存容量 90%(80%+10%),98%的对象可回收是一般情况,当小于 90%的对象被回收的时候(10%以上的对象存活时),则 Survivor to 空间不够,则需要依赖老年代进行分配担保

标记-整理

1460000020597799?w=2594&h=1670
老年代不适合复制算法

  1. 复制操作增多 2. 额外 50%空间浪费 3. 经常需要额外的空间分配担保 4.可能老年代中对象 100% 存活
  1. 整理 将存活的对象移动到一端(左上方),从不规整变成规整,然后直接清理掉边界以外的内存

垃圾收集器

Serial 收集器

1460000020597800?w=2134&h=1474
单线程垃圾回收器,用户线程到安全点先暂定,然后 GC 线程单线程串行进行,等 GC 线程回收完,然后用户线程再继续
特点:Stop the world
场景:桌面应用 (gc时间短)
用于新生代,client 端

ParNew 收集器

Serial收集器的多线程版本
1460000020597801?w=1596&h=1074
用于新生代,唯一能和CMS 收集器配合工作,运行在 server 模式下
-XX:ParallelGCThreads 限制垃圾收集器线程数 = CPU 核数(过多会导致上下文切换消耗)
并行:多条垃圾收集线程并行工作,用户线程仍然处于等待状态
并发:用户线程与垃圾收集器同时执行,用户线程和垃圾线程在不同 CPU 上执行

Parallel Scavenge 收集器

新生代收集器,复制算法,并行的多线程收集器
关注吞吐量优先的收集器(吞吐量 = CPU 运行用户代码执行时间/CPU 执行总时间 ,比如: 99%时间执行用户线程,1%时间回收垃圾,这时吞吐量为 99%)高吞吐量可以高效率利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多的交互任务
CMS 关注缩短垃圾回收停顿时间,适合与用户交互的程序,良好的响应速度能提升用户体验
-XX:MaxGCPauseMillis 参数 GC 停顿时间,参数过小会频繁 GC
-XX:GCTimeRatio 参数,默认 99%(用户线程时间占 CPU 总时间的 99%)

Serial Old 收集器

是Serial 收集器的老年代版本
单线程老年代收集器,采用“标记-整理”算法

Parallel Old 收集器

是 Parallel Scavenge收集器的老年代版本
多线程老年代收集器,采用“标记-整理”算法

CMS 收集器

获取最短回收停顿时间为目标的收集器,采用“标记-清除”算法,用于互联网、B/S 系统重视响应的系统
1460000020597802?w=1852&h=1106

  1. 初始标记(不和用户线程一起运行,耗时短)—— 标记一下 GC Roots 能直接关联到的对象,速度很快
  2. 并发标记(和用户线程一起运行,耗时长) —— 并发标记阶段就是进行 GC RootsTracing,寻找 GC 引用链
  3. 重新标记(不和用户线程一起运行,耗时短)—— 为了修正并发标记期间因用户线程导致标记产生变动的标记记录
  4. 并发清除(和用户线程一起运行,耗时长)—— 扫描整个内存区域
  1. 对 CPU 资源非常敏感(并发标记阶段时间长,占用用户线程 CPU 时间)
  2. 无法处理浮动垃圾(程序在进行并发清除时,用户线程所产生的新垃圾)
  3. 标记-清除产生空间碎片

G1 收集器

面向服务端应用的垃圾收集器
1460000020597803?w=2634&h=1526
Region->Remembered Set (解决 循环引用 )
检查 Reference (程序对reference类型写操作,检查 reference 引用类型)
步骤:

  1. 初始标记 —— 标记 GC Roots 能直接关联到的对象
  2. 并发标记 —— 从 GC Root 开始对堆中对象进行可达性分析,找出存活对象 ,这一阶段耗时较长,但可与用户程序并发执行
  3. 最终标记(Remembered Set Logs->Remembered Set)—— 修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs里面,最终标记阶段需要把 Remembered Set Logs的数据合并到 Remembered Set中
  4. 筛选回收(Live Data Counting and Evacuation)—— 只需要扫描 Remembered Set
  1. 基于“标记-整理” 为主和 Region 之间采用复制算法实现
  2. 可预测停顿,降低停顿时间,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型
  3. G1 直接对 Java 堆中的 Region 进行回收(新生代、老年代不再物理隔离,他们都是一部分 Region)
  4. 可预测的停顿时间模型,G1 跟踪各个 Regions 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region

堆内存分配

Java 堆分布图
1460000020597804?w=2580&h=1654

对象分配的规则:

  1. 对象主要分配在新生代的 Eden 区 ( Eden区,From 区存活对象复制到 To区,Eden区,From区被回收,然后 To区对象拷贝到From区,再进行下一次垃圾回收 )
  2. 如果启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配
  3. 少数情况下也可能直接分配到老年代 (放不下From和To区的都直接放到老年代)

大对象分配

大对象是指需要大量连续内存空间的 Java 对象,最典型的大对象是是那种很长的字符串以及数组
-XX:PretenureSizeThreshold 设置大于该值的对象直接分配在老年代,避免在 Eden 区以及 2 个Survivior区之间发生大量的内存复制

逃逸分析和栈上分配

逃逸分析:分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,称为方法逃逸。甚至还有可能被外部线程访问到,比如赋值给类变量或其他线程中访问的实例变量,称为线程逃逸。
栈上分配:把方法中的变量和对象直接分配到栈上,方法执行完后自动销毁,不需要垃圾回收介入,从而提高系统性能
-XX:+DoEscapeAnalysis 开启逃逸分析(jdk1.8默认开启 )
-XX:-DoEscapeAnalysis 关闭逃逸分析

虚拟机调优命令

  1. ps -ef | grep java
  2. jps -m(启动参数) -l(类名) -v (JVM 参数)
  3. jstat -gc 27660 250 20 监视虚拟机各种运行状态信息

1460000020597805?w=1302&h=692

  1. jinfo 27660 查看和调整进程虚拟机(未被显示指定的)参数信息
  2. jmap 生成堆转储快照 -XX:+HeapDumpOnOutOfMemoryError

jmap -heap 9366;
jmap -histo 9366 | more; 显示堆中对象统计
jmap -dump:format=b,file=/Users/mousycoder/Desktop/a.bin 9366 生成dump文件
-Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/mousycoder/Desktop/
jhat /Users/mousycoder/Desktop/java_pid9783.hprof 图形分析Heap
select s.toString() from java.lang.String s where (s.value != null && s.value.length > 1000 )

  1. jstack 线程快照(虚拟机内每一条线程正在执行的方法堆栈的集合,主要用于定位线程问题)

shutdownHook 在关闭之前执行的任务
jstack -l -F pid 强制输出

  1. RUNNABLE
  2. BLOCKED 一个正在阻塞等待一个监视器的线程处于这个状态(Entry Set)被动的阻塞
  3. WAITING 一个正在无限期等待另一个线程执行一个特别的动作的线程处于这一状态 (Wait Set)主动显式申请的阻塞
  4. TIMED_WAITING 一个正在限时等待另一个线程执行一个动作的线程处于这一状态
  5. TERMINATED 线程完成一个excution

1460000020597806?w=2258&h=1472

JConsole

基于 JMX 的可视化监视、管理工具
开启 JMX 端口
nohup java -Xms800m -Xmx800m -Djava.rmi.server.hostname=192.168.1.250 -Dcom.sun.management.jmx
remote.port=1111 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -jar hc-charging-
server.jar &

互联网开发流程
1460000020597807?w=2394&h=1216
Jconsole 内存分析思考过程
1460000020597808?w=1510&h=1036

FullGC

Minor GC:当 Eden 区满,触发 Minor GC
FullGC:

  1. 调用System.gc() 建议虚拟机进行 Full GC,可通过 -XX:+DisableExplicitGC 来进制 RMI 调用System.gc()
  2. 老年代空间不足 大对象直接进入老年代,长期存活的对象进入老年代,当执行 Full GC后空间仍然不足,则抛出 OutOfMemoryError,为了避免上面原因引起 Full GC,调优时尽量做到让对象在 Minor GC 阶段被回收,让对象在新生代多存活一段时间以及不要创建过大的对象和数组
  3. 空间分配担保失败 使用复制算法的 Minor GC 需要老年代的内存空间作为担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC
  1. 减少-Xmx大小,缩短 GC 时间(堆内存设置越大,Full GC 时间越长,停顿时间也会越长)

互联网问题

  1. 白名单问题

解决方法:list.contain->set.contain->布隆过滤器(用户量大和用户量小系统解决方案不一样)

解决方法:jstack 以及 new thread带上名称

  1. 堆内存泄露

FullGC 出现正常频率为一天 1~2 次
解决方案:jmap , heap dump on oom + jhat

  1. 堆外内存泄露

heap堆使用率很低,但是有 OOM 以及 Full GC
解决方法:btrace ,directMemory增大

  1. 不对等数据

解决方案: MQ

Class 文件

  • class 文件是一组以 8 位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件中 ,中间没有添加任何分隔符
  • 当遇到 8 位字节以上的空间数据项时,则会按照高位在前的方式分隔成若干个 8 位字节进行存储
  • class 文件中有 2 种数据类型,无符号数和表

1460000020597809?w=1202&h=484

结构:
1.魔数
2.class 文件版本
3.常量池
4.访问标志
5.类索引、父类索引、接口索引集合
6.字段表集合
7.方法表集合
8.属性表集合

CAFEBABE
1460000020597810?w=222&h=364

1460000020597811?w=952&h=120
1460000020597812?w=546&h=466
1460000020597814?w=672&h=342

1460000020597815?w=944&h=1032

1460000020597816?w=1030&h=108

字段表集合

1460000020597817?w=1200&h=52
1460000020597818?w=902&h=452

1460000020597819?w=1194&h=52
1460000020597820?w=454&h=340

1460000020597821?w=1194&h=58

字节码指令

1460000020597822?w=1184&h=256
1460000020597823?w=1192&h=426

加载存储指令

1460000020597824?w=1172&h=390

1460000020597825?w=1180&h=448

类型转换指令

1460000020597826?w=1200&h=316

对象创建与访问指令

1460000020597827?w=1016&h=412

操作数栈管理指令

1460000020597828?w=1190&h=320

控制转移指令

1460000020597829?w=1202&h=348
1460000020597830?w=800&h=244

方法调用和返回指令

1460000020597831?w=1162&h=402
1460000020597832?w=1160&h=252

异常处理指令

1460000020597833?w=1100&h=150

1460000020597834?w=1128&h=446
1460000020597835?w=1142&h=474

类加载机制

1460000020597836?w=1140&h=174

类加载时机

1460000020597837?w=972&h=326

1460000020597838?w=1132&h=440

不被初始化情况

1460000020597839?w=926&h=158

类加载过程

1460000020597840?w=1200&h=366

1460000020597841?w=1164&h=266

1460000020597842?w=550&h=440

1460000020597843?w=1144&h=378

1460000020597844?w=1126&h=454

1460000020597845?w=1112&h=360

类、接口解析

1460000020597846?w=1164&h=466

字段的解析

1460000020597847?w=1174&h=566

类方法解析

1460000020597848?w=1178&h=620

接口方法解析

1460000020597849?w=1196&h=462

1460000020597850?w=1162&h=240
1460000020597851?w=1190&h=118
1460000020597852?w=1152&h=118
1460000020597853?w=1146&h=262

1460000020597854?w=1126&h=272

1460000020597855?w=1116&h=358

自定义类加载器优势

1460000020597856?w=1148&h=264

双亲委派模型

1460000020597857?w=1156&h=406
1460000020597858?w=828&h=546

运行时栈帧结构

1460000020597859?w=280&h=270
1460000020597860?w=1204&h=190
1460000020597861?w=882&h=898

局部变量表

1460000020597862?w=1140&h=166

1460000020597863?w=1244&h=596
1460000020597864?w=1228&h=592

栈帧指向该栈所属方法引用

方法的返回地址和附加信息

方法返回地址

1460000020597865?w=1078&h=90

1460000020597866?w=1144&h=110

1460000020597867?w=1156&h=284
invokestatic、invokespecial、invokevirtual、invokeinterface、invokedynamic

静态分派调用

编译期间决定,主要是重载,支持最优选择重载

动态分派调用

针对重写
1460000020597868?w=1096&h=298

## 动态语言支持
1460000020597869?w=1152&h=424

happens-before

1460000020597870?w=1108&h=436

1460000020597871?w=950&h=396
多线程中重排序用同步来保证,不然会出现莫名其妙问题

锁内存语义

1460000020597872?w=748&h=480
1460000020597873?w=1102&h=122

final

1460000020597874?w=1092&h=410
1460000020597875?w=1102&h=94

JVM调优

  • newRadio = 2/ 新生代老年代比例
  • Xss5m 设置最大调用深度

GC ROOT

1460000020597876?w=1000&h=208
1460000020597877?w=1346&h=876

  1. 面试前看下知识体系导图
  2. 坚持就胜利

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK