JVM知识点梳理

文章目录


前言

学习java,了解java的运行环境、运行机制是极为必要的,本篇对学习JVM内存模型相关知识作一个梳理。

一、关于JVM

1、关于JDK、JRE、JVM

  • JRE(Java Runtime Environment),也就是java平台。所有的java程序都要在JRE环境下才能运行。
  • JDK(Java Development Kit),是开发者用来编译、调试程序用的开发包。JDK也是JAVA程序需要在JRE上运行。
  • JVM(Java Virtual
    Machine),是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。

2、JVM原理

  • jvm是java的核心和基础,在java编译器和os平台之间的虚拟处理器,可在上面执行字节码程序。
  • java编译器只要面向jvm,生成jvm能理解的字节码文件。java源文件经编译成字节码程序,通过jvm将每条指令翻译成不同的机器码 ,通过特定平台运行。

JVM知识点梳理

3、 JVM执行程序的过程

  1. 加载.class文件
  2. 管理并分配内存
  3. 执行垃圾收集

4、 JVM的生命周期

4.1、JVM实例对应了一个独立运行的java程序,它是进程级别。

a) 启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void
main(String[] args)函数的class都可以作为JVM实例运行的起点
b) 运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以表明自己创建的线程是守护线程
c) 消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出

4.2、JVM执行引擎实例则对应了属于用户运行程序的线程,它是线程级别的

二、JVM内存模型

1.java代码具体执行过程

JVM知识点梳理

2、jvm内存结构图(运行时数据区)

JVM知识点梳理

1)、程序计数器

由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令。为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。
由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

2)、虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型,是线程私有的。每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,而且 每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。其中,局部变量表主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和 对象句柄,它们可以是方法参数,也可以是方法的局部变量。

虚拟机栈有两种异常情况:*Error 和 OutOfMemoryError。我们知道,一个线程拥有一个自己的栈,这个栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss 参数可以设置虚拟机栈大小),若线程请求的栈深度大于虚拟机允许的深度,则抛出 *Error 异常。此外,栈的大小可以是固定的,也可以是动态扩展的,若虚拟机栈可以动态扩展(大多数虚拟机都可以),但扩展时无法申请到足够的内存(比如没有足够的内存为一个新创建的线程分配栈空间时),则抛出 OutofMemoryError 异常。

3)、本地方法栈

本地方法栈与Java虚拟机栈非常相似,也是线程私有的,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机执行 Native 方法服务。与虚拟机栈一样,本地方法栈区域也会抛出 *Error 和 OutOfMemoryError 异常。

4)、Java 堆

Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存。Java堆是线程共享的,类的对象从中分配空间,这些对象通过new、newarray、 anewarray 和 multianewarray 等指令建立,它们不需要程序代码来显式的释放。

由于Java堆唯一目的就是用来存放对象实例,因此其也是垃圾收集器管理的主要区域,故也称为称为 GC堆。从内存回收的角度看,由于现在的垃圾收集器基本都采用分代收集算法,所以为了方便垃圾回收Java堆还可以分为 新生代 和 老年代 。新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,对象就会被移入老年代。

5)、方法区

方法区与Java堆一样,也是线程共享的并且不需要连续的内存,其用于存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区通常和永久区(Perm)关联在一起,但永久代与方法区不是一个概念,只是有的虚拟机用永久代来实现方法区,这样就可以用永久代GC来管理方法区,省去专门内存管理的工作。根据Java虚拟机规范的规定,当方法区无法满足内存分配的需求时,将抛出 OutOfMemoryError 异常。

(1)、运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的各种 字面量 和 符号引用。其中,字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等;而符号引用则属于编译原理方面的概念,包括以下三类常量:类和接口的全限定名、字段的名称和描述符 和 方法的名称和描述符。因为运行时常量池(Runtime Constant Pool)是方法区的一部分,那么当常量池无法再申请到内存时也会抛出 OutOfMemoryError 异常。

运行时常量池相对于Class文件常量池的一个重要特征是具备动态性。Java语言并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,比如字符串的手动入池方法intern()。

三、JVM内存溢出的情况

JVM知识点梳理

1、Java堆溢出 (OOM)

Java堆用于存储对象的实例,只要不断地创建对象,并且保证GC roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
  要解决这个异常,一般先通过内存映像分析工具对堆转储快照分析,确定内存的对象是否是必要的,即判断是 内存泄露 还是 内存溢出。如果是内存泄露,可以进一步通过工具查看泄露对象到GC Roots的引用链,比较准确地定位出泄露代码的位置。如果是内存溢出,可以调大虚拟机堆参数,或者从代码上检查是否存在某些对象生命周期过长的情况。

2、虚拟机栈和本地方法栈溢出 (SOF/OOM)

(1). SOF

如果线程请求的栈深度大于虚拟机栈允许的最大深度,将抛出*Error异常。我们知道,每当Java程序启动一个新的线程时,Java虚拟机会为它分配一个栈,并且Java虚拟机栈以栈帧为单位保持线程运行状态。每当线程调用一个方法时,JVM就压入一个新的栈帧到这个线程的栈中,只要这个方法还没返回,这个栈帧就存在。 那么可以想象,如果方法的嵌套调用层次太多,比如递归调用,随着Java虚拟机栈中的栈帧的不断增多,最终很可能会导致这个线程的栈中的所有栈帧的大小的总和大于-Xss设置的值,从而产生*Error溢出异常。

(2). OOM

如果虚拟机在拓展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。在虚拟机栈和本地方法栈发生OOM异常场景如下:当Java 程序启动一个新线程时,若没有足够的空间为该线程分配Java栈(一个线程Java栈的大小由-Xss设置决定),JVM将抛出OutOfMemoryError异常。

3、方法区和运行时常量池溢出 (OOM)

运行时常量池溢出的情况: String.intern()是一个native方法,在JDK1.6及之前的版本中,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象,否则将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在永久代中,如果不断地使用intern方法手动入池字符串,则会抛出OutOfMemoryError异常。但在JDK1.7及其以后的版本中,对intern()方法的实现作了进一步改进,其不会再复制实例到常量池中,而仅仅是在常量池中记录首次出现的实例的引用。

四、JVM优化

1、调优前原则

1、多数的Java应用不需要在服务器上进行GC优化;
2、多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;
3、在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);
4、减少创建对象的数量;
5、减少使用全局变量和大对象;
6、GC优化是到最后不得已才采用的手段;
7、在实际使用中,分析GC情况优化代码比优化GC参数要多得多;

2、GC优化目的

1、将转移到老年代的对象数量降低到最小;
2、减少full GC的执行时间;

3、优化策略

1、减少使用全局变量和大对象;
2、调整新生代的大小到最合适;
3、设置老年代的大小为最合适;
4、选择合适的GC收集器;

常规GC指标:

1.Minor GC执行时间不到50ms;
2.Minor GC执行不频繁,约10秒一次;
3.Full GC执行时间不到1s;
4.Full GC执行频率不算频繁,不低于10分钟1次;

4、常见实例记录

实例1:java.lang.OutOfMemoryError: GC overhead limit exceeded错误

出现这种错误的情况有三个:
1.堆太小。
2.有不能回收大对象的存在。
3.有死循环

解决思路:
1.使用ps -ef |grep "java"查看jvm参数;
如果堆区的大小设置和机器的内存比例较大,建议加大参数。
2.检查代码中可能出现的大对象,使用完成后记得取消引用;
3.是否有不可退出的循环,或者递归的方法存在。

实例2:服务器经常出现卡顿现象

解决思路:
使用jstat -gcutil查看gc参数
1.看Minor GC执行是否频繁,执行时间是否长。如果时间久则说明Eden空间小,可以适当调节堆的大小
2.看Full GC执行是否频繁,如果平凡发生Full GC,如果堆大小设置已经足够大,先查看NewRatio设置的比例值,如果值过大,将会导致:新生代太小,导致对象提前进入老年代,触发老年代发生Full GC;老年代较大,进行Full GC时耗时较大;

实例3:发现内存占用率很高,Full GC频繁

解决思路:

1.查看java程序的pid

ps aux|grep xxx

假如获取到的pid是8888

2.生成dump文件

sudo -u root -H  jmap -dump:format=b,file=dumpJvmPidIs8888.hprof 8888

如果找不到jmap命令,可以使用以下方法

sudo -u root -H  $JAVA_HOME/bin/jmap -dump:format=b,file=dumpJvmPidIs630.hprof 13518

3.检查dump下来的文件,看是否有大对象

总结

学习JVM,其一,通过理解java的运行机制,JVM在运行环境中起到的作用,了解可能导致高频GC的原因,利于写出高质量代码。其二、对必要条件下的JVM调优。

上一篇:Java 应用线上问题排查思路、常用工具小结


下一篇:【大厂必考之JVM】02,作为Java开发程序员