JAVA8的JVM虚拟机,看这一篇就够了

有次准备公司分享会,梳理了java8的JVM相关知识,深浅适中。
涉及JVM的结构作用堆的结构组成以及JVM分析方法和性能优化

1.JVM体系结构概述

JVM的位置

在这里插入图片描述
首先JAVA是一门跨平台的语言,这跟他的虚拟机有关。
JVM是运行在操作系统之上的,它与硬件没有直接的交互

JVM体系结构概览

在这里插入图片描述
JVM的结构包含类加载器,运行时数据区,执行引擎和本地方法接口。
1.类加载器:
负责加载class文件,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine决定。
JVM自带的加载器有:
启动类加载器(Bootstrap classLoader)由C++编写;
扩展类加载器(Extension classLoader)由Java编写;
应用程序类加载器(System classLoader)由Java编写;
加载器的双亲委派机制:一个类加载器收到类加载请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。 好处:确保一个类只被加载一次

2.Native Interface本地方法接口:
Java语言本身不能对操作系统底层进行访问和操作,但是可以通过Native Interface接口调用
其他语言来实现对底层的访问,这也是JAVA跨平台的来源。
具体:Native Interface的作用是融合不同的编程语言为Java所用,它的初衷是融合 C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中专门
开辟了一块区域处理标记为Native的代码,它的具体做法是Native Method Stack中登记Native方法,在Execution Engine 执行时加载Native libraries。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者
Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用WebService等等,不多做介绍。

3.执行引擎:
字节码解释器, 将字节码编译成机器语言后执行

4.运行时数据区:
包含方法区,栈,堆,本地方法栈,程序计数器
a.方法区: 方法区是线程共享的,通常用来保存装载的类的元结构信息。
比如:运行时常量池+静态变量+常量+字段+方法字节码+在类/实例/接口初始化用到的特殊方法等。
b.栈: 栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束,栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。用于存放局部变量、实例方法、引用类型变量、方法出口等信息。
c.堆: Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。这个区域是用来存放对象实例的,几乎所有对象实例都会在这里分配内存。
d.程序计数器:线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记
e.本地方法栈:保存被native修饰的方法,即非java方法。

2.堆体系结构概述

在这里插入图片描述
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。Java8删除了堆中的永久代,增加了元空间。

堆的生命周期

新生代
新生代是对象的诞生、成长、消亡的区域,一个对象在这里产生,应用,最后被垃圾回收器收集,销毁。新生代又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的对象都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区.若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到老年代。

老年代
若老年代也满了,那么这个时候将产生MajorGC(FullGC),进行老年代的内存清理。若老年代执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

元空间:
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize:初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize:最大空间,默认是没有限制的。

GC垃圾回收

1)根据发生的区域来分类:
Minor GC:从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
Major GC :是清理老年代。
Full GC(Minor GC+Major GC) :是清理整个堆空间—包括年轻代和老年代。

2)回收算法:
标记-清除算法:先标记可回收的内存,后清除,缺点是:效率比较低;会出现大量不连续的内存碎片。
复制算法:将可用的内存分成两份,每次使用其中一块,当这块回收之后把未回收的复制到另一块内存中,然后把使用的清除。
标记整理算法:是在标记-清除算法基础上,不直接清理,而是使存活对象往一端游走,然后清除一端边界以外的内存,这样既可以避免不连续空间出现,还可以避免对象存活率较高时的持续复制。
分代收集算法:分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代,在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率搞,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。
3)收集器(根据类型可分为串行/并行/并发收集器):
Serial收集器:新生代串行收集器,采用“复制”算法,适合单核处理器,在垃圾回收时,必须暂停其他所有线程。
Serial Old收集器:老年代串行收集器,采用“标记-整理”算法,可与Serial收集器协同工作
ParNew收集器:Serial收集器的多线程版本,并行收集器,除了使用多条线程进行垃圾回收之外,其他与Serial一样,即也需要停止所有用户线程,采用复制算法等。
Parallel Scavenge:新生代垃圾并行收集器,并行的复制算法收集器,特点:可控制吞吐量,运行时暂停所有线程
Parallel Old:Parallel Scavenge的老年代版本并行收集器,只能和Parallel Scavenge配合使用,使用“标记-整理”算法。
Concurrent Mark Sweep(CMS):并发垃圾收集器,且采用标记-清除算法。
Garbage-First收集器(G1):G1既可以作用于新生代又可以作用于老年代。
垃圾收集器默认情况:
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1

3.JVM分析实践及优化理论

JVM分析工具(jps/jmap/jstat/jstack)

  1. jps:查看运行的HotSpot VM进程,使用jps -v 可以显示配置参数。
  2. jmap命令: 用来查看当前系统中jvm进程 heap dump的情况,包括对象的数量,对象所占内存的大小
    推荐:jmap -histo PID:生成java堆中对象的相关信息,包含数量以及占用的空间大小;

在这里插入图片描述
3. 推荐:jmap -heap PID:查看jvm配置和使用情况; jmap -dump PID :生成堆内存快照
在这里插入图片描述
4. jstat命令:主要是用来监控 heap size 和 jvm垃圾回收情况,尤其是gc情况的监控,如果老年代多次发生full gc,那么很可能是内存泄漏导致。 推荐:jstat -gcutil PID 返回百分比的形式显示堆使用情况以及GC情况
在这里插入图片描述

S0/S1:幸存者区
E:伊甸园区,初生区
0:老年代
M:元空间
YGC:年轻带回收次数
YGCT:年轻带回收花费时间(秒)
FGC:full GC
FGC:full GC花费时间(秒)
GCT:GC总时间(秒)
  1. jstack PID :打印所有线程的堆栈信息

在这里插入图片描述

优化

GC 优化的两个目标:

  1. 将进入老年代的对象数量降到最低(减少Full GC的频率)
    对象在 Eden 区被创建,随后被转移到 Survivor 区,在此之后剩余的对象会被转入老年代。也有一些对象由于占用内存过大,在 Eden 区被创建后会直接被传入老年代。老年代 GC 相对来说会比新生代 GC 更耗时,因此,减少进入老年代的对象数量可以显著降低 Full GC 的频率。
  2. 减少 Full GC 的执行时间
    Full GC 的执行时间比 Minor GC 要长很多,因此,如果在 Full GC 上花费过多的时间(超过 1s),将可能出现超时错误。可以通过减小老年代内存大小使Full GC的时间降低,但是减小老年代的内存大小又会增加Full GC的频率,两者需要摸索出合适的平衡值。

优化方向建议:
JVM调优没有固定的参数参考,需要根据不同的服务器和系统需求来调整。

  1. GC优化是到最后不得已才采用的手段。
  2. 一般来说堆越大越好,能够降低GC的频率,但增加堆内存,会造成单次GC需要遍历处理的对象更多,耗时增加;也会受服务器硬件的限制无法无限大,所以需要根据实际找到平衡值。
  3. 通常堆参数-Xms和-Xmx可以设置相等,防止垃圾收集器在最小、最大之间收缩堆而产生额外的消耗,耗费性能。
  4. 新生代/老年代大小比例设置合适:新生代过小,发生Minor GC频繁,且大对象容易直接进入老年代;新生代过大,老年代变小,容易Full GC频繁,Minor GC耗时大幅度增加。具体设置多大合适没有值,需要根据实际优化,SUN官方给出的建议是新生代占堆的3/8比较合适。

常见的JVM异常

A. OutOfMemory(OOM)

OutOfMemory ,即内存溢出,是一个常见的 JVM 问题。什么情况下会抛出OOM呢?满足以下2个条件:
1、JVM98%的时间都花费在内存回收
2、每次回收的内存小于2%

那么分析 OOM 的思路是什么呢?首先,要知道有三种 OutOfMemoryError:

  • OutOfMemoryError:Java heap space - 堆空间溢出
  • OutOfMemoryError:PermGen space - 方法区和运行时常量池溢出
  • OutOfMemoryError:unable to create new native thread - 线程无法创建

B. OutOfMemoryError:PermGen space 表示方法区和运行时常量池溢出。

原因:
Perm 区主要用于存放 Class 和 Meta 信息的,Class 在被 Loader 时就会被放到 PermGen space,这个区域称为年老代。GC 在主程序运行期间不会对年老区进行清理,默认是 64M 。
当程序程序中使用了大量的 jar 或 class,使 java 虚拟机装载类的空间不够,超过 64M 就会报这部分内存溢出了,需要加大内存分配,一般 128m 足够。

解决方案:
(1)扩大永久代空间
JDK7 以前使用 -XX:PermSize 和 -XX:MaxPermSize 来控制永久代大小。
JDK8 以后把原本放在永久代的字符串常量池移出, 放在 Java 堆中(元空间 Metaspace)中,元数据并不在虚拟机中,使用的是本地的内存。
使用 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 控制元空间大小。
(2)清理应用程序中 WEB-INF/lib 下的 jar,用不上的 jar 删除掉,多个应用公共的 jar 移动到 Tomcat 的 lib 目录,减少重复加载

C. OutOfMemoryError:Java heap space 表示堆空间溢出。
原因:JVM 分配给堆内存的空间已经用满了。
问题定位
(1)使用 jmap 或 -XX:+HeapDumpOnOutOfMemoryError 获取堆快照。 (2)使用内存分析工具(visualvm、mat、jProfile 等)对堆快照文件进行分析。 (3)根据分析图,重点是确认内存中的对象是否是必要的,分清究竟是是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

内存泄漏:没用了的内存没用及时释放导致最后占满内存。
内存泄漏常见几个情况:
1)静态集合类:声明为静态(static)的 HashMap、Vector 等集合,通俗来讲 A 中有 B,当前只把 B 设置为空,A 没有设置为空,回收时 B 无法回收-因被 A 引用。
2)监听器:监听器被注册后释放对象时没有删除监听器
3)物理连接:DataSource.getConnection()建立链接,必须通过 close()关闭链接。
4)有死循环或不必要地重复创建大量对象

查找:FGC 次数越多,FGCT 所需时间越多-可非常有可能存在内存泄漏。

优化参数

堆配置:
-Xms:初始堆大小
-Xms:最大堆大小
-Xss: 每个线程的堆栈大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。
-XX:MaxMetaSpaceSize=n:最大元空间大小
-XX:+CollectGen0First : FullGC时是否先YGC
收集器设置:
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
-XX:ParallelGCThreads 并行收集器的线程数
打印GC:
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值