JVM 内存分布
- 线程共享数据区:
方法区->类信息,静态变量
堆->数组对象 - 线程隔离区
虚拟机栈-> 方法
本地方法栈->本地方法库 native - 堆、程序计数器
-
JVM 运行数据
程序计数器
线程隔离 ,比较小的内存空间,当前线程所执行的字节码的行号
线程是一个独立的执行单元,由 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 对象创建过程
- new 指令,判断在常量池中有没符号引用,有则已被加载过
- 判断类是否被加载、解析、初始化
- 为新生对象在java堆里分配内存空间
-
指针碰撞(内存比较整齐)
步骤:1. 分配内存 2. 移动指针,非原子步骤可能出现并发问题,Java虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性
2)空闲列表(内存比较乱)
存储堆内存空闲地址
步骤:1.分配内存 2. 修改空闲列表地址 非原子步骤可能出现并发问题,Java虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性
- 将分配到内存空间都初始化零值
- 设置对象头相关信息 (GC分代年龄、对象的HashCode、元数据信息)
- 执行
方法
Java 对象内存布局
对象属性的值->实例数据
对象头 64 位机器存 64 位,32 位机器存 32 位,8 的倍数
Java 对象的访问
-
直接指针访问
-
句柄访问
对比:
- 访问效率:直接指针访问效率高(hotspot采用这种方式)
- 垃圾回收:句柄访问效率高,垃圾回收只用更新句柄池,而直接指针访问方式则要更新 reference地址
对象在 JVM 过程
新生代:对象在 Eden 区出生后,当 Eden 区满时,发生一次 Minor GC后,如果对象还存活且能被s1区域容纳,使用复制算法将存活对象复制到s1,然后一次性清除 (Eden + s0) (此时对象年龄 +1,年龄达到 15 后进入老年代)然后将 s0 ,s1互换位置,最终得到一个空的新生代(Eden + s0, s1一直处于非活动被动状态)
新生代、老年代、永久代
-Xmn 设置年轻代 -XX:NewRatio 设置年轻代与老年代的大小比例
-XX:OldSize 设置老年代大小
-XX:MaxPermSize 设置永久代(方法区)
-Xmx -Xms 设置堆空间大小
垃圾回收算法
引用计数器
当对象实例分配给一个变量时,该变量计数设置为 1,当任何其他变量被赋值为这个对象的引用的时,计数+1 (a =b,则b的引用对象实例计数器+1),当一个对象实例的某个引用超过了生命周期(方法执行完)或者被设置为一个新值,则该对象的实例引用计数器 -1
无法解决循环引用
可达性分析
GC Root (虚拟机栈中的引用的对象、本地方法栈中引用的对象、方法区静态属性引用的对象、方法区常量引用的对象)
标记-清除
标记需要回收的对象,在标记完成后统一回收
不足:
1.效率问题,标记清除 2 个过程效率都不高
2.空间问题,标记清除后产生大量不连续的内存碎片,碎片过多当程序需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发一次垃圾回收动作
标记-复制
内存块 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 空间不够,则需要依赖老年代进行分配担保
标记-整理
老年代不适合复制算法
- 复制操作增多 2. 额外 50%空间浪费 3. 经常需要额外的空间分配担保 4.可能老年代中对象 100% 存活
步骤:
- 标记
- 整理 将存活的对象移动到一端(左上方),从不规整变成规整,然后直接清理掉边界以外的内存
垃圾收集器
Serial 收集器(新生代收集器)
单线程垃圾回收器,用户线程到安全点先暂定,然后 GC 线程单线程串行进行,等 GC 线程回收完,然后用户线程再继续
特点:Stop the world
场景:桌面应用 (gc时间短)
用于新生代,client 端
ParNew 收集器(新生代收集器)
Serial收集器的多线程版本
用于新生代,唯一能和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 系统重视响应的系统
触发阈值:老年代或永久代达到 92%
-XX:+CMSClassUnloadingEnabled 让 CMS 可以回收 Perm 区
步骤:
- 初始标记(不和用户线程一起运行,耗时短)—— 标记一下 GC Roots 能直接关联到的对象,速度很快
- 并发标记(和用户线程一起运行,耗时长) —— 并发标记阶段就是进行 GC RootsTracing,寻找 GC 引用链
- 重新标记(不和用户线程一起运行,耗时短)—— 为了修正并发标记期间因用户线程导致标记产生变动的标记记录
- 并发清除(和用户线程一起运行,耗时长)—— 扫描整个内存区域
缺点 :
- 对 CPU 资源非常敏感(并发标记阶段时间长,占用用户线程 CPU 时间)
- 无法处理浮动垃圾(程序在进行并发清除时,用户线程所产生的新垃圾)
- 标记-清除产生空间碎片
G1 收集器
面向服务端应用的垃圾收集器
Region->Remembered Set (解决 循环引用 )
检查 Reference (程序对reference类型写操作,检查 reference 引用类型)
步骤:
- 初始标记 —— 标记 GC Roots 能直接关联到的对象
- 并发标记 —— 从 GC Root 开始对堆中对象进行可达性分析,找出存活对象 ,这一阶段耗时较长,但可与用户程序并发执行
- 最终标记(Remembered Set Logs->Remembered Set)—— 修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs里面,最终标记阶段需要把 Remembered Set Logs的数据合并到 Remembered Set中
- 筛选回收(Live Data Counting and Evacuation)—— 只需要扫描 Remembered Set
优势:
- 基于“标记-整理” 为主和 Region 之间采用复制算法实现
- 可预测停顿,降低停顿时间,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型
- G1 直接对 Java 堆中的 Region 进行回收(新生代、老年代不再物理隔离,他们都是一部分 Region)
- 可预测的停顿时间模型,G1 跟踪各个 Regions 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region
堆内存分配
Java 堆分布图
对象分配的规则:
- 对象主要分配在新生代的 Eden 区 ( Eden区,From 区存活对象复制到 To区,Eden区,From区被回收,然后 To区和From区交换,再进行下一次垃圾回收 )
- 如果启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配
- 少数情况下也可能直接分配到老年代 (放不下From和To区的都直接放到老年代)
大对象分配
大对象是指需要大量连续内存空间的 Java 对象,最典型的大对象是是那种很长的字符串以及数组
-XX:PretenureSizeThreshold 设置大于该值的对象直接分配在老年代,避免在 Eden 区以及 2 个Survivior区之间发生大量的内存复制
逃逸分析和栈上分配
逃逸分析:分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,称为方法逃逸。甚至还有可能被外部线程访问到,比如赋值给类变量或其他线程中访问的实例变量,称为线程逃逸。
栈上分配:把方法中的变量和对象直接分配到栈上,方法执行完后自动销毁,不需要垃圾回收介入,从而提高系统性能
-XX:+DoEscapeAnalysis 开启逃逸分析(jdk1.8默认开启 )
-XX:-DoEscapeAnalysis 关闭逃逸分析
虚拟机调优命令
- ps -ef | grep java
- jps -m(启动参数) -l(类名) -v (JVM 参数)
-
jstat -gc 27660 250 20 监视虚拟机各种运行状态信息
- jinfo 27660 查看和调整进程虚拟机(未被显示指定的)参数信息
- 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 ) - jstack 线程快照(虚拟机内每一条线程正在执行的方法堆栈的集合,主要用于定位线程问题)
shutdownHook 在关闭之前执行的任务
jstack -l -F pid 强制输出
线程状态
- NEW
- RUNNABLE
- BLOCKED 一个正在阻塞等待一个监视器的线程处于这个状态(Entry Set)被动的阻塞
- WAITING 一个正在无限期等待另一个线程执行一个特别的动作的线程处于这一状态 (Wait Set)主动显式申请的阻塞
- TIMED_WAITING 一个正在限时等待另一个线程执行一个动作的线程处于这一状态
- TERMINATED 线程完成一个excution
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 &
互联网开发流程
Jconsole 内存分析思考过程
FullGC
Minor GC:当 Eden 区满,触发 Minor GC
FullGC:
- 调用System.gc() 建议虚拟机进行 Full GC,可通过 -XX:+DisableExplicitGC 来禁止 RMI 调用System.gc()
- 老年代空间不足 大对象直接进入老年代,长期存活的对象进入老年代,当执行 Full GC后空间仍然不足,则抛出 OutOfMemoryError,为了避免上面原因引起 Full GC,调优时尽量做到让对象在 Minor GC 阶段被回收,让对象在新生代多存活一段时间以及不要创建过大的对象和数组
- 空间分配担保失败 使用复制算法的 Minor GC 需要老年代的内存空间作为担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC
建议: - 减少-Xmx大小,缩短 GC 时间(堆内存设置越大,Full GC 时间越长,停顿时间也会越长)
- 集群部署
互联网问题
- 白名单问题
解决方法:list.contain->set.contain->布隆过滤器(用户量大和用户量小系统解决方案不一样) - 死锁
解决方法:jstack 以及 new thread带上名称 - 堆内存泄露
FullGC 出现正常频率为一天 1~2 次
解决方案:jmap , heap dump on oom + jhat - 堆外内存泄露
堆外内存( Unsafe的 allocateMemory ,ByteBuffer.allocateDirect,Runtime.getRuntime().freeMemory()可以查看不占用heap堆大小)
heap堆使用率很低,但是有 OOM 以及 Full GC
解决方法:btrace 跟踪DirectByteBuffer构造函数,非 Java层面使用 Google Perftools分析 - 不对等数据
解决方案: MQ
本文由博客一文多发平台 OpenWrite 发布!