JVM 难学?那是因为你没认真看完这篇文章

一:虚拟机内存图解

JAVA 程序运行与虚拟机之上,运行时需要内存空间。虚拟机执行 JAVA 程序的过程中会把它管理的内存划分为不同的数据区域方便管理。

虚拟机管理内存数据区域划分如下图:

数据区域分类:

方法区:            (Method Area)

虚拟机栈 :        (VM Stack)

本地方法栈 :    (Native Method Stack)

堆:                   (Heap)

程序计数器:    (Program Counter Register)

直接内存 :       (Direct Memory)

说明:

1. 程序计数器

行号指示器,字节码指令的分支、循环、跳转、异常处理、线程恢复 (CPU 切换),每条线程都需要一个独立的计数器,线程私有内存互不影响, 该区域不会发生内存溢出异常。

2. 虚拟机栈

是线程私有的,声明周期与线程相同,虚拟机栈是 Java 方法执行的内存模型,每个方法被执行时都会创建一个栈帧,即方法运行期间的基础数据结构,栈帧用于存储:局部变量表、操作数栈、动态链接、方法出口等,每个方法执行中都对应虚拟机栈帧从入栈到处栈的过程。

是一种数据结构,是虚拟机中的局部变量表,对应物理层之上的程序数据模型。

局部变量表,是一种程序运行数据模型,存放了编译期可知的各种数据类型例如:

Boolean、byte、char、short、int、float、long、double、对象引用类型 (对象内存地址变量,指针或句柄),程序运行时,根据局部变量表分配栈帧空间大小,在运行中,大小是不变的异常类型:stackOverFlowError 线程请求栈深度大于虚拟机允许深度 OutOfMemory 内存空间耗尽无法进行扩展。

3. 本地方法栈

与虚拟机栈类似,虚拟机栈为 Java 程序服务,本地方法栈支持虚拟机的运行服务,具体实现由虚拟机厂商决定,也会抛出 stackOverFlowError、OutOfMemory 异常。

4. 堆

是虚拟机管理内存中最大的一部分,被所有线程共享,用于存放对象实例 (对象、数组),物理上不连续的内存空间,由于 GC 收集器,分代收集,所以划分为:新生代 Eden、From SurVivor 空间、To SurVivor 空间,allot buffer(分配空间),可能会划分出多个线程私有的缓冲区,老年代。

5. 方法区

与堆一样属于线程共享的内存区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码(动态加载 OSGI)等数据。理论上属于 java 虚拟机的一部分,为了区分开来叫做 Non-Heap 非堆。

这个区域可以选择不进行垃圾回收,该区域回收目的主要是常量池的回收,及类型的卸载 class, 内存区不足时会抛出 OutOfMemory 异常

运行时常量池:

方法区的一部分,Class 的版本、字段、接口、方法等,及编译期生成的各种字面量、符号引用,编译类加载后存放在该区域。会抛出 OutOfMemory 异常。

6. 直接内存

直接内存不属于虚拟内存区域,是一种基于通道与缓冲区的 IO 方式,可以使用本地函数直接分配堆外内存,在堆中存储引用的外部内存地址,通过引用完成对直接引用内存的操作,1.4 之后提供的 NIO 显著提高效率,避免了堆内存与 Native 内存的来回复制操作,不受虚拟机内存控制,会抛出 OUtOfMemory 异常。

二:对象访问内部实现过程

对象访问 涉及到对象的地址变更状态变更,内存地址移动,变量、接口、实现类、方法、父类型等。

** 一、 句柄方式 (访问)**

二、指针方式 (访问)

优缺点:

句柄访问方式:reference 中存储的是稳定的地址,对象变更时只会改变句柄实例数据指针,引用本身不需要修改

指针访问方式:优点速度快,节省了指针定位时间开销

三:内存区域控制参数及对应溢出异常

开发过程中,或程序运行过程中每次遇到 OutOfMemory 异常或 GC 异常或 StackOverflowError 异常我们都是一堆参数乱配,都把值调大,只是大体知道是跟 jvm 内存分配有关,具体应该怎么调,对应的异常应该调整那些参数,或者换句话说,jvm 内存分配区域中都分别对应那些参数大多数情况下都是不知道的,只是把相关的参数跳上去,预期结果都是应该起作用,到底能不能起作用,自己心里也没底。下面就来说一下 jvm 堆、栈、方法区等内存区域对应的参数,及每个区域可能抛出的异常类型,发生异常的场景分析。

一、参数类型

 1. 堆空间参数

2. 栈空间参数

3. 方法区空间参数

4. 本机直接内存参数

二、异常类型

1.OutOfMemory 异常

2.StackOverflowError 异常

三、辅助参数说明

1.-XX:+HeapDumpOnOutOfMemoryError 打印堆内存异常时打印出快照信息

2.-XX:+HeapDumpPath 快照输出路径

3.-Xmn 指定 eden 区的大小 -XX:SurvirorRation 来调整幸存区的大小

4.-XX:PretenureSizeThreshold 设置进入老年代的阀值

四、参数说明、对应场景的异常

1. 堆内存参数

-Xms:堆最小值(新生代和老年代之和)

-Xmx:堆最大值(新生代和老年代之和)

当最小值 = 最大值时,这时堆内存是不可扩展的。

例:-Xms80M -Xmx80M 

通常将 -Xmx 和 -Xms 设置为一样的大小来减少 gc 的次数,堆内存不足时抛出 OutOfMemoryError 异常。

2. 栈内存参数

-Xss

例:-Xss128k

单线程下无论栈帧太大还是栈容量太小,及引用深度超过虚拟机允许深度都会抛出 StackOverflowError 每个方法压入栈的帧大小是不一致的。多线程下当每个线程分配栈帧太大内存不能够扩展时抛出 OutOfMemoryError 异常线程栈帧越大,可创建的线程越少。

3. 方法区参数

-XX:PermSize 方法区内存最小值

-XX:MaxPermSize 方法区内存最大值

各个线程共享的内存区域,主要用来存储类的元数据、常量、静态变量、即时编译器编译后的代码等数据

例:-XX:PermSize=20M -XX:MaxPermSize=20M

异常类型 OutOfMemoryError :

原因:常量过多,或代理反射等使用频繁

4. 本机直接内存参数

-XX:MaxDirectMemorySize

例:-XX:MaxDirectMemorySize=10M

不足时抛出 OutOfMemory 异常

四:垃圾收集算法

经典的垃圾回收算法以下几种

一、标记–清除算法 (Mark-Sweep)

回收前状态:

回收后状态:

优缺点:

算法执行分为两个阶段标记与清除,所有的回收算法,基本都

基于标记回收算法做了深度优化

缺点:效率问题,内存空间碎片(不连续的空间)

二、复制算法 (Copying)

回收前状态:

Eden 内存空间 8

Survivor1 空间(From 空间)1

Survivor2 空间 (To 空间) 1

Eden 内存空间与 Survivor 空间 8:1

回收后状态:

Survivor1 空间(From 空间)1

Eden 内存空间与 Survivor 空间 8:1

优缺点:

比较标记清除算法,避免了回收造成的内存碎片问题,

缺点:以局部的内存空间牺牲为代价,不过空间的浪费比较小,默认 8:1 的比例 1 是浪费的。

复制也有一定的效率与空间成本

三、标记整理算法 (Mark-Compact)

回收前状态:

回收后状态:

优缺点:

避免了,空间的浪费,与内存碎片问题。

缺点:整理时复制有效率成本。

五:垃圾收集器

一、七种垃圾收集器

(1) Serial(串行 GC)-XX:+UseSerialGC

(2) ParNew(并行 GC)-XX:+UseParNewGC

(3) Parallel Scavenge(并行回收 GC)

(4) Serial Old(MSC)(串行 GC)-XX:+UseSerialGC

(5) CMS(并发 GC)-XX:+UseConcMarkSweepGC

(6) Parallel Old(并行 GC)-XX:+UseParallelOldGC

(7) G1(JDK1.7update14 才可以正式商用)

二.1~3 用于年轻代垃圾回收:年轻代的垃圾回收称为 minor GC

三.4~6 用于年老代垃圾回收(当然也可以用于方法区的回收):年老代的垃圾回收称为 full GC

G1 独立完成 "分代垃圾回收"

注意:并行与并发

并行:多条垃圾回收线程同时操作

并发:垃圾回收线程与用户线程一起操作

四、常用五种组合

Serial/Serial Old

ParNew/Serial Old:与上边相比,只是比年轻代多了多线程垃圾回收而已

ParNew/CMS:当下比较高效的组合

Parallel Scavenge/Parallel Old:自动管理的组合

G1:最先进的收集器,但是需要 JDK1.7update14 以上

五. Serial/Serial Old

年轻代 Serial 收集器采用单个 GC 线程实现 "复制" 算法(包括扫描、复制)

年老代 Serial Old 收集器采用单个 GC 线程实现 "标记 - 整理" 算法

Serial 与 Serial Old 都会暂停所有用户线程(即 STW)

说明:

STW(stop the world):编译代码时为每一个方法注入 safepoint(方法中循环结束的点、方法执行结束的点),在暂停应用时,需要等待所有的用户线程进入 safepoint,之后暂停所有线程,然后进行垃圾回收。

适用场合:

CPU 核数 <2,物理内存 <2G 的机器(简单来讲,单 CPU,新生代空间较小且对 STW 时间要求不高的情况下使用)

-XX:UseSerialGC:强制使用该 GC 组合

-XX:PrintGCApplicationStoppedTime:查看 STW 时间

六.ParNew/Serial Old:

ParNew 除了采用多 GC 线程来实现复制算法以外,其他都与 Serial 一样,但是此组合中的 Serial Old 又是一个单 GC 线程,所以该组合是一个比较尴尬的组合,在单 CPU 情况下没有 Serial/Serial Old 速度快(因为 ParNew 多线程需要切换),在多 CPU 情况下又没有之后的三种组合快(因为 Serial Old 是单 GC 线程),所以使用其实不多。

-XX:ParallelGCThreads:指定 ParNew GC 线程的数量,默认与 CPU 核数相同,该参数在于 CMS GC 组合时,也可能会用到

七.Parallel Scavenge/Parallel Old:

特点:

年轻代 Parallel Scavenge 收集器采用多个 GC 线程实现 "复制" 算法(包括扫描、复制)年老代 Parallel Old 收集器采用多个 GC 线程实现 "标记 - 整理" 算 ParallelScavenge 与 Parallel Old 都会暂停所有用户线程(即 STW)

说明:

吞吐量:CPU 运行代码时间 /(CPU 运行代码时间 +GC 时间)CMS 主要注重 STW 的缩短(该时间越短,用户体验越好,所以主要用于处理很多的交互任务的情况)Parallel Scavenge/Parallel Old 主要注重吞吐量(吞吐量越大,说明 CPU 利用率越高,所以主要用于处理很多的 CPU 计算任务而用户交互任务较少的情况)

参数设置:

-XX:+UseParallelOldGC:使用该 GC 组合

-XX:GCTimeRatio:直接设置吞吐量大小,假设设为 19,则允许的最大 GC 时间占总时间的 1/(1+19),默认值为 99,即 1/(1+99)

-XX:MaxGCPauseMillis:最大 GC 停顿时间,该参数并非越小越好

-XX:+UseAdaptiveSizePolicy:开启该参数,-Xmn/-XX:SurvivorRatio/-XX:PretenureSizeThreshold 这些参数就不起作用了,虚拟机会自动收集监控信息,动态调整这些参数以提供最合适的的停顿时间或者最大的吞吐量(GC 自适应调节策略),而我们需要设置的就是 -Xmx,-XX:+UseParallelOldGC 或 -XX:GCTimeRatio 两个参数就好(当然 -Xms 也指定上与 -Xmx 相同就好)

注意:

-XX:GCTimeRatio 和 -XX:MaxGCPauseMillis 设置一个就好

不开启 -XX:+UseAdaptiveSizePolicy,-Xmn/-XX:SurvivorRatio/-XX:PretenureSizeThreshold 这些参数依旧可以配置,以 resin 服务器为例

-Xms2048m -Xmx2048m -Xmn512m -Xss1m -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:+UseParallelOldGC -XX:GCTimeRatio=19 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps View Code

适用场合:

很多的 CPU 计算任务而用户交互任务较少的情况不想自己去过多的关注 GC 参数,想让虚拟机自己进行调优工作

八、调优方法

8.1 新对象预留新生代

由于 fullGC(老年代) 的成本远比 minorGC(新生代和老年代)的成本大,所以给应用分配一个合理的新生代空间,尽量将对象分配到新生代减小 fullGC 的频率

8.2 大对象进入老年代

将大对象直接分配到老年代,保持新生代对象的结构的完整性,以提高 GC 效率, 以通过 -XX:PretenureSizeThreshold 设置进入老年代的阀值

8.3 稳定与震荡的堆大小

稳定的对大小是对垃圾回收有利的,方法将 -Xms 和 -Xmx 的大小一致

8.4 吞吐量优先

尽可能减少系统执行垃圾回收的总时间,故采用并行垃圾回收器

-XX:+UseParallelGC 或使用 -XX:+UseParallelOldGC

8.5 降低停顿

使用 CMS 回收器, 同时减少 fullGC 的次数

九、获取 gc 信息的方法

9.1 -verbose:gc 或者 -XX:+PrintGC  获取 gc 信息

9.2 -XX:+PrintGCDetails  获取更加详细的 gc 信息

9.3 -XX:+PrintGCTimeStamps  获取 GC 的频率和间隔

9.4 -XX:+PrintHeapAtGC  获取堆的使用情况

9.5 -Xloggc:D:\gc.log  指定日志情况的保存路径

十、jvm 调优实战 -tomcat 启动加速

在 tomcat 的 bin/catalina.bat 文件的开头添加相关的配置

六:监控工具

监控工具:一般问题定位,性能调优都会使用到。

(一)、jps

Jps 是参照 Unix 系统的取名规则命名的,而他的功能和 ps 的功能类似,可以列举正在运行的饿虚拟机进程并显示虚拟机执行的主类以及这些进程的唯一 ID(LVMID,对应本机来说和 PID 相同),他的用法如下:

Jps [option] [hostid]

jps -q 只输出 LVMID

jps -m 输出 JVM 启动时传给主类的方法

jps -l 输出主类的全名,如果是 Jar 则输出 jar 的路径

jps -v 输出 JVM 的启动参数

**(二)、jstat **

 jstat 主要用于监控虚拟机的各种运行状态信息,如类的装载、内存、垃圾回收、JIT 编译器等,在没有 GUI 的服务器上,这款工具是首选的一款监控工具。其用法如下:

jstat [option vmid [interval [s|ms] [vount] ] ]

jstat 监控内容 线程好 刷新时间间隔 次数

jstat –gc 20445 1 20    : 监视 Java 堆,包含 eden、2 个 survivor 区、old 区和永久带区域的容量、已用空间、GC 时间合计等信息

jstat –gcutil 20445 1 20: 监视内容与 -gc 相同,但输出主要关注已使用空间占总空间的百分比

jstat –class 20445 1 20: 监视类的装载、卸载数量以及类的装载总空间和耗费时间等

…….-gccapcity……: 监视内容与 -gc 相同,但输出主要关注 Java 区域用到的最大和最小空间

…….-gccause……..: 与 -gcutil 输出信息相同,额外输出导致上次 GC 产生的原因

…….-gcnew……….: 监控新生代的 GC 情况

…….-gcnewcapacity..: 与 -gcnew 监控信息相同,输出主要关注使用到的最大和最小空间

…….-gcold……….: 监控老生代的 GC 情况

…….-gcoldcapacity..: 与 -gcold 监控信息相同,输出主要关注使用到的最大和最小空间

…….-gcpermcapacity.: 输出永久带用到的最大和最小空间

…….-compiler…….: 输出 JIT 编译器编译过的方法、耗时信息

…….-printcompilation: 输出已经被 JIT 编译的方法

(三)、jinfo

jinfo 的作用是实时查看虚拟机的各项参数信息 jps –v 可以查看虚拟机在启动时被显式指定的参数信息,但是如果你想知道默认的一些参数信息呢?除了去查询对应的资料以外,jinfo 就显得很重要了。jinfo 的用法如下:

Jinfo [option] pid

(四)、jmap

map 用于生成堆快照(heapdump)。当然我们有很多方法可以取到对应的 dump 信息,如我们通过 JVM 启动时加入启动参数 –XX:HeapDumpOnOutOfMemoryError 参数,可以让 JVM 在出现内存溢出错误的时候自动生成 dump 文件,亦可以通过 -XX:HeapDumpOnCtrlBreak 参数,在运行时使用 ctrl+break 按键生成 dump 文件,当然我们也可以使用 kill -3 pid 的方式去恐吓 JVM 生成 dump 文件。Jmap 的作用不仅仅是为了获取 dump 文件,还可以用于查询 finalize 执行队列、Java 堆和永久带的详细信息,如空间使用率、垃圾回收器等。其运行格式如下:

Jmap [option] vmip

监控堆栈信息主要用来定位问题的原因,生成堆栈快照

…….-dump……: 生成对应的 dump 信息,用法为 -dump:[live,]format=b,file={fileName}

…….-finalizerinfo……: 显示在 F-Queue 中等待的 Finalizer 方法的对象(只在 linux 下生效)

…….-heap……:显示堆的详细信息、垃圾回收器信息、参数配置、分代详情等

…….-histo……:显示堆栈中的对象的统计信息,包含类、实例数量和合计容量

…….-permstat……:以 ClassLoder 为统计口径显示永久带的内存状态

…….-F……:虚拟机对 -dump 无响应时可使用这个选项强制生成 dump 快照

例子:jmap -dump:format=b,file=yhj.dump 20445

(五)、jstack

Jstack 用于 JVM 当前时刻的线程快照,又称 threaddump 文件,它是 JVM 当前每一条线程正在执行的堆栈信息的集合。生成线程快照的主要目的是为了定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部时长过长导致线程停顿的原因。通过 jstack 我们就可以知道哪些进程在后台做些什么?在等待什么资源等!其运行格式如下:

Jstack [option] vmid

-F 当正常输出的请求不响应时强制输出线程堆栈

-l 除堆栈信息外,显示关于锁的附加信息

-m 显示 native 方法的堆栈信息

(六)、jconsole

在 JDK 的 bin 目录下, 监控内存,thread, 堆栈等

(七)、jprofile

类似于 jconsole, 比 jconsole 监控信息更全面,内存,线程,包,cup 类,堆栈,等等 

欢迎工作一到五年的 Java 的工程师朋友们加入的 Java 架构开发:685-16-7-672

本群提供免费的学习指导架构资料以及免费的解答

不懂得问题都可以在本群提出来之后还会有职业生涯规划以及面试指导