欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-07-23 21:06:01
一:虚拟机内存图解 JAVA程序运行与虚拟机之上,运行时需要内存空间。虚拟机执行JAVA程序的过程中会把它管理的内存划分为不同的数据区域方便管理。 虚拟机管理内存数据区域划分如下图: 数据区域分类: 方法区 (Method Area) 虚拟机栈 (VM Stack) 本地方法栈 (Native Me ......

一:虚拟机内存图解

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

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

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

数据区域分类:

方法区            (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、对象引用类型(对象内存地址变量,指针或句柄),程序运行时,根据局部变量表分配栈帧空间大小,在运行中,大小是不变的异常类型:*Error 线程请求栈深度大于虚拟机允许深度 OutOfMemory 内存空间耗尽无法进行扩展。


3. 本地方法栈

与虚拟机栈类似,虚拟机栈为Java程序服务,本地方法栈支持虚拟机的运行服务,具体实现由虚拟机厂商决定,也会抛出 *Error、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异常。


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

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

 一、 句柄方式 (访问)

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

二、指针方式 (访问)

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

优缺点:

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

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


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

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


一、参数类型

 1.堆空间参数

2.栈空间参数

3.方法区空间参数

4.本机直接内存参数

 


二、异常类型

1.OutOfMemory异常

2.*Error异常


三、辅助参数说明

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

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


3.方法区参数

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

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

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

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

异常类型 OutOfMemoryError :

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


4.本机直接内存参数

-XX:MaxDirectMemorySize

例:-XX:MaxDirectMemorySize=10M

不足时抛出OutOfMemory异常


四:垃圾收集算法

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

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

回收前状态:

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

回收后状态:

 

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

优缺点:

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

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

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


二、复制算法(Copying)

回收前状态:

Eden内存空间 8

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

Survivor1空间(From空间)1

 

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

Survivor2空间(To空间) 1

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

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

 

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

回收后状态:

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

Survivor1空间(From空间)1

 

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

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

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

优缺点:

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

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

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


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

回收前状态:

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

回收后状态:

 

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

优缺点:

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

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


五:垃圾收集器

一、七种垃圾收集器

(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才可以正式商用)

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

二.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服务器为例

<jvm-arg>-Xms2048m</jvm-arg> <jvm-arg>-Xmx2048m</jvm-arg> <jvm-arg>-Xmn512m</jvm-arg> <jvm-arg>-Xss1m</jvm-arg> <jvm-arg>-XX:PermSize=256M</jvm-arg> <jvm-arg>-XX:MaxPermSize=256M</jvm-arg> <jvm-arg>-XX:SurvivorRatio=8</jvm-arg> <jvm-arg>-XX:MaxTenuringThreshold=15</jvm-arg> <jvm-arg>-XX:+UseParallelOldGC</jvm-arg> <jvm-arg>-XX:GCTimeRatio=19</jvm-arg> <jvm-arg>-XX:+PrintGCDetails</jvm-arg> <jvm-arg>-XX:+PrintGCTimeStamps</jvm-arg> 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 类,堆栈,等等