JVM 虚拟机 精华一页纸

1、内存管理 - 栈 or 堆

无论是java还是C,内存分配,本质上就是 栈和堆两个类型。简单来说,代码逻辑处理在栈上,数据在堆上。

I、JVM内存模型

堆:新生代(Eden,survivor),年老代(Gen) -- 分配对象、数组等

非堆(栈):虚拟机栈,本地方法栈 -- 栈帧 分配局部变量、操作需要的空间比如方法链接

方法区-(永久代) -- 分配代码、全局变量、静态变量

Object o = new Object()

首先代码在 方法区中。

执行时,Object o 会存放在 java栈 的本地变量表中。

new Object 会在 java堆中。

II、JVM 内存分配过程

a、创建的对象都在堆的新生代(Eden)上分配空间;垃圾收集器回收时,把Eden上存活的对象和一个Survivor 的对象拷贝到另一个Survivor

一般是MinorGC

b、大对象直接进入老年代

-- 所以对于大对象比较多的,年老代分配的内存要多一点,年轻代分配的少一点

--可以配置对象大小的门限

c、长期存活的对象进入老年代

--在多次MinorGC时仍然存活的,进入老年代

--可以配置门限MinorGC次数,默认15次

原因很简单,因为 年轻代一般是复制算法,多次复制代价很大

老年代是 Full GC

d、年龄判断,如果 Survivor 的同龄对象占所有对象的一半,大于这个年龄的就直接进入老年代

MinorGC时,检查晋升老年代的对象是否大于 老年代剩余空间,如果大于则进行 Full GC

e、空间分配担保

在发生Minor GC 时,虚拟机检测之前 晋升到老年代的空间平均大小 是否大于老年代剩余空间,如果大于则直接进行 Full GC;如果小于,则查看HandlePromotionFailure 设置是否允许担保失败。如果允许,就进行Minor GC,并把存活的对象移到老年代,如果不允许,则进行Full GC

III、内存溢出

a、OutOfMemoryError

首先,堆内存不够分配、肯定会出现 内存溢出的问题

永久代,加载的类太多,也会有

栈内存申请不到也有

本机native直接内存溢出

内存溢出会出现在各个内存区域

b、StackOverflowError

递归调用(没有关闭条件)

线程太多

c、内存溢出定位过程

使用内存映像分析工具(Eclipse Memory Analyzer),对dump出的文件进行分析

确认内存中的对象是否必要的。 即分清楚出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

如果是内存泄漏通过工具查看 泄漏对象到 GC root的引用链

如果不是泄漏,则 检查 虚拟机的堆参数(-Xmx -Xms),从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况。

2、内存(垃圾)回收

在描述 java 垃圾回收之前,想象一下 C ++ 内存如何内存管理 和 垃圾回收。

通常new 一片内存区域,存储一些数据,假设就是 new int[]

频繁的操作删除后,留下了很多内存碎片

然后一般都是 memcpy 把数据转移到内存的一端,一般是都移动到开始端。

事实上,所有的内存回收后的管理,基本都是 拷贝移动已有数据。比如 Redis的 ziplist 就是这么设计的。

垃圾回收两个问题:

I、如何判断 对象不再被使用?

a、首先想到的是记录每一个使用者 - 引用计数器,事实上早期的java垃圾回收就是如此。

引用计数有个很大的困扰,几个对象间的互相循环引用,怎么办?引用计数一直存在。

b、标记引用链 + 从根开始 - 根搜索法

通过引用链可以识别对象引用关系;从根开始,就能识别脱离主链的 循环引用的问题。这样利用有向图,从根开始寻找整个引用链,把不再链上的对象都进行标记。

什么样的对象适合做根对象 GCroot

静态变量 - 程序加载首先进内存的对象,全局根

栈帧的变量 - 程序当前执行到的对象,临时根 (因为执行完毕,栈帧的数据就会回收,执行过程中,作为当前流程开始的对象同样也是根)

II、如何操作回收不用的对象?

前面已经描述过C++ 内存回收方法,java也非常类似

标记清除法 - 前面发现的对象,标记完后,进行删除, 类似 delete,这样会产生很多碎片

复制算法 - 把存活的对象,统一拷贝到 另一块完整内存

标记整理法 - 把存活对象移动到一端,剩下的内存统一清理,类似 memcpy,后delete

适用场景

复制算法,适用存活对象较少的场景,比如 新生代;标记整理算法和清除算法,适用于存活对象较多的场景。

III、垃圾回收器

除了标记清除法外,其他两种需要移动对象,都会造成程序的卡顿(移动过程中,对象不能被改变),这个问题数据库备份过程中也有同样的问题。

a、复制算法收集器 -- 基本都用在新生代

Serial收集器 - 单线程条件下运行 (一般client和默认的)

ParNew收集器 - 多线程条件下运行 (一般server模式适用)

Parallel Scanvenge收集器

ParNew VS Parallel Scanvenge

ParNew 关注卡顿时延; Parallel Scanvenge 关注系统吞吐量

b、标记整理算法收集器 -- 基本用在老年代

Serial Old收集器 - 单线程

Parallel Old收集器 - 多线程 关注吞吐量

c、标记清除算法收集器 -- 用在老年代

CMS(Concurrent Mark Sweep)收集器 关注时延(因为耗时最多的标记和清除不需要影响用户业务)

G1收集器(Garbage First)收集器

时延 or 吞吐量

可以这么理解,时延的目标是单次回收要尽快,减少单次时延,而整体卡顿累计时长可能更多,导致吞吐量下降;吞吐量则关注整体卡顿情况,累计时长要端,吞吐量要高,单次卡顿时延可能会较长。

在这两种策略下,关注时延的 可能是多次频繁小范围的GC、关注吞吐量的可能是 一次就彻底的大范围的GC

组合:

单线程版本 - Serial + Serial Old 用在 Client模式下 (一般很少使用)

吞吐量优先组合 - Parallel Scanvenge + Parallel Old(Serial Old 老版本) 用在 Server模式下

时延优先组合 - ParNew + CMS(Serial Old 备用)用在 Server 模式下

3、JVM 优化

I、JVM crash

JVM 宕机的问题分析,首先 JVM 是一个C++进程,同样可以采用 C++ coredump 的分析思路来分析 JVM (网上描述的,好像用jmap生成的dump不能用GDB调试)

a、定位的文件素材

crash 日志

生成 -XX:ErrorFile=/path/xxx.log;

执行命令-XX:OnError="string"

-XX:+ShowMessageBoxOnError -- 打开实时GDB调试

程序自带日志

coredump文件

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/xx.log

linx: kill -3 | windows : Ctrl + Break

用JDK 自带命令jmap ,或者工具 JConsole和VisualVM

如果不能生成 则检查linux的 ulimit 配置

如果都找不到 到 /var/log/message中 找 cat messages|grep java java线程相关信息

b、分析文件

crash 日志

日志头(概要信息): -- 得到在哪个大的部分出现的问题。粗略信息

SIGSEGV - 执行 JNI 时出现的问题,一般是 编译加载类、执行JVM 外部代码出现的问题

EXCEPTION_ACCESS_VIOLATION - 执行 JVM 自身的代码

EXCEPTION_STACK_OVERFLOW - 堆栈出错

执行代码类型 C J VM 等

线程信息: -- 得到crash时线程的工作情况

线程类型 - Java Thread | VMThread | CompilerThread | GCTaskThread | WatcherThread | ConcurrentMarkSweepThread

线程状态 - _thread_in_native | _thread_uninitialized | _thread_new | _thread_in_vm | _thread_in_Java | _thread_blocked

安全点 safepoint 和锁 Mutex

安全点是标记线程运行到一个区域,JVM将其挂起,以便执行GC 等JVM操作。没有运行到安全点的线程,GC是不能回收其内存的;如果线程一直不到安全点可能会出现假死状态

内存heap情况

各个内存区域使用情况

其他信息

JVM参数,系统环境

分析重点:概要信息里,判断Crash时正在执行什么信息;当前线程状态;还有内存使用情况。

经典问题:内存溢出,一般永久代因为分配较少,出现问题的情况比较多;堆栈溢出,主要是 jni 本地栈溢出的可能较多

threaddump / heapdump 文件

当前线程运行状态、线程堆栈信息

类、对象使用情况

分析重点:基本信息里面,生成堆栈时的 异常线程和异常原因; wait/lock 等信息

经典问题:内存溢出

分析顺序 crash日志 > thread dump > heap dump

II、JVM OOM 问题

分析的文件和 crash 一样的。分析过程也是类似;

另外,可以通过JDK工具和命令实时监控分析。

不过,OOM 不一定会出现crash的情况。一般都是分析 heap dump文件。

a、分析内存 堆、非堆的使用情况; 看看是否是内存分配参数设置不合理。

b、分析 出现OOM的线程,正在操作的情况。找到导致泄露的对象的 GC Root 链

c、分析 类实例数最多、最大的 类的使用情况。(大对象、多对象)

-- 找到上面这两种情况下的 类和对象 是否需要/需要这么多,分清是 泄露还是对象生命周期不合理

d、分析 GC 的情况

-- 看看Full GC的情况

III、性能优化

程序的性能优化,无非就是 CPU、内存、IO 三种资源的占用情况分析。

性能优化的关键在于,分段排查,逐步逼近的方式,确定问题代码所在

先测量

再逐步逼近

找到问题代码

分析原因,并给解决方法

第一步:Linux 命令查看

top -H -p 找到 java进程和线程 中最耗资源的线程

第二步:在各种日志中找到对应的线程进行分析

a、卡顿时间较长 or 处理很慢 - 一般是CPU在高负荷运转,说明线程在高负荷执行;GC 时间较长等

查看GC 时长,GC 频次,各种GC占比 使用的是什么垃圾处理器

(比如 GCViewer 工具),GC的具体情况 -XX:+PrintGCTimeStamps -Xloggc:/tmp/gc.log -XX:+PrintGCDetails

查看各个线程处理情况,lock wait/notify 等情况;长时间运行的线程

查看JNI线程使用情况

连续生成两次的 线程堆栈(core) 文件,对比,查看 对象变化,线程执行变化;如果执行方法没有变化的线程,一般就是有问题的线程

IV、JDK自带工具

a、命令行工具

内存信息 jmap工具

生成dump jmap dump:format=b,file=xxx pid

内存统计 jmap -heap

内存跟踪 jstat

线程堆栈跟踪 jstack

配置信息 jinfo

分析工具 jhat

利用命令行就是 jmap+jstack,然后详细信息通过jhat

b、可视化工具

JConsole

JVisualVM - 其中 BTTrace 可以嵌入到每个方法追踪每个方法的执行(通过类似 asm/CGLib 字节码加载替换)

对比两个 dump 的差异,找出对象的

V、性能分析工具

MAT

IBM Heap - 可以分析出OOM中的 内存占用最大的地方,可以溯源GC root,找到对象树

VI、配置建议

a、内存分配,各个代的分配,32位JVM下 堆分配 1G,年轻代 一半,年老代一半。持久代64M

-Xmx512m

-Xms512m

-- 最大最小保持一致,避免频繁扩容和收缩

-Xmn256m

-- 年轻代一般在 一半左右,官方推荐 3/8

-XX:PermSize=64m

-- 持久代一般固定在 64M左右

-XX:MaxPermSize=128m

b、垃圾收集器选择

-XX:+UseParNewGC

--设置年轻代使用 ParNew收集器 并行

-XX:+UseConcMarkSweepGC

--设置年老代为CMS收集器 并发

c、其他设置项:内存压缩,对象晋级等等。

-XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction=5

-- Full GC 5次后,进行内存碎片压缩

-XX:+HeapDumpOnOutOfMemoryError

-- 内存溢出时生成dump文件

d、启用运行期编译

Jit即时编译器

C1 – Client (简单优化

C2 – Server(激进优化 运行在Server模式下会更高效)

根据监控,针对热点代码进行优化(比如方法内联)

Jit的缺点是 编译有耗时,另外,对于一些类装载卸载比较多的场景也不适合。

Server模式启动时要慢一点,运行时效率很高

也可以指定,运行模式 解释模式-Xint,编译模式-XComp

比较理想的情况是,编译成Class,运行时可以动态Server模式;兼顾了效率和可移植性。

你可能感兴趣的:(JVM 虚拟机 精华一页纸)