JVM学习思路整理

       最近学习了JVM的一些知识。作为后端开发人员,这一块还是需要了解的。不过,作为后端开发人员来说,对JVM的了解还是很有限的。这篇文章记录一下我学习JVM的一些思路。不说了,先上目录:

       1.JVM内存结构都有什么?具体分为哪几部分?

       2.一个java类在运行过程中,在JVM中的具体流转过程。

       3.JVM的垃圾回收原理和时机。

       4.JVM的调优案例。

1.内存结构:

话不多说,先上图:

JVM学习思路整理

        主要有三部分组成:类装载子系统,运行时数据区,字节码执行引擎。我们考虑最多的是运行时数据区这块。

        运行时数据区包括:堆,栈,方法区,程序计数器。其中,栈又分为虚拟机栈和本地方法栈。我们经常说的栈就是虚拟机栈。那些用native修饰的方法,将来会入本地方法栈。接下来讨论一下,各个部分存储的内容。

       堆:Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);
幸存区又分为From Survivor空间和 To Survivor空间。上图:

JVM学习思路整理

老年代和年轻代的比例一般是2:1.   年轻代中伊甸园区和survivor区比例是4:1.    from区和To区比例是1:1

堆中主要存放:使用new关键字创建的对象,所有对象实例以及数组都要在堆上分配。是线程共享的区域。

 

栈:线程私有,每个线程拥有独立的栈空间,生命周期与线程生命周期相同。先进后出。里面存存放有栈帧元素。每个方法在执行时都会创建一个栈帧。栈帧分为:局部变量表,操作数栈,动态链接和方法出口。这里了解一下局部变量表和操作数栈。

         局部变量表:

存储基本数据类型的局部变量(包括参数)、和对象的引用(String、数组、对象等),但是不存储对象的内容。

局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),

JVM 会为其分配两个连续的变量槽来存储。以下简称 Slot 。

          SLot复用:

         为了尽可能的节省栈帧空间,局部变量表中的 Slot 是可以复用的。方法中定义的局部变量,其作用域不一定会覆盖整个方法。当方法运行时,如果已经超出了某个变量的作用域,即变量失效了,那这个变量对应的 Slot 就可以交给其他变量使用,也就是所谓的 Slot 复用。通过一个例子来理解变量“失效”。

        public void test(boolean flag)
{
    if(flag)
    {
        int a = 66;
    }
    
    int b = 55;
}

当虚拟机运行 test 方法,就会创建一个栈帧,并压入到当前线程的栈中。当运行到 int a = 66时,在当前栈帧的局部变量中创建一个 Slot 存储变量 a,当运行到 int b = 55时,此时已经超出变量 a 的作用域了(变量 a 的作用域在{}所包含的代码块中),此时 a 就失效了,变量a 占用的 Slot 就可以交给b来使用,这就是 Slot 复用。

凡事有利弊。Slot 复用虽然节省了栈帧空间,但是会伴随一些额外的副作用。比如,Slot 的复用会直接影响到系统的垃圾收集行为。

   操作数栈:可以理解为栈帧中用于计算的临时数据存储区。操作数栈的元素可以是任意的Java数据类型。

   可以运用java反编译工具JAD,结合JVM指令手册,看到操作数栈在虚拟机中的具体操作流程。这里就不不说具体做法了。

栈中可能出现哪些异常?*Error:栈溢出错误  ; OutOfMemoryError:内存不足

如果一个线程在计算时所需要用到栈大小 > 配置允许最大的栈大小,那么Java虚拟机将抛出 *Error

 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

如何设置栈参数?使用 -Xss 设置栈大小

      方法区:同堆一样,线程共享。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。
    常量池是方法区的一部分。上图:

JVM学习思路整理

     程序计数器:

        当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令

        程序计数器是每个线程私有的内存。

        程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。

好了,第一部分JVM内存结构学习完毕。已知晓了各个组成部分存储的内容,接下来我解释下一个类信息在加载过程中的流程。先上一个类:

JVM学习思路整理

1.这个java类会被编译成Math.class文件。

2.类转载子系统尝试记载这个.class文件。首先会进入方法区。加载class文件的所有类具体信息。

3.会为每个方法创建栈帧。对象放入堆,局部变量和对象的引用放入栈帧中的局部变量表。

4.程序执行用程序计数器,字节码执行引擎负责读取指令。程序计数器用来计算和临时存储。

5.return为方法出口,先进后出。

第二部分类信息在JVM中的流转,介绍完毕。接下来讲一下垃圾回收时机和机制。讲这一部分,肯定离不开堆。所以,先去看一下上面堆的具体组成结构。接下来就更好理解了。

首选,new出来的对象会放入对象的伊甸园区,当伊甸园区满了以后,会触发一次MonitorGC,将没有用的对象回收掉,有用的对象进入FORM区。下一次伊甸园区满了后,会再次触发MonitorGC,这次不仅回收伊甸园无用的对象还会回收FORM区无用的对象,再将有用对象存入FROM区。合适的时机,FROM区会移动到To区,每移动一次,To区的对象标记加1,当等于15时,将会把老不死的对象放入老年代。老年代满了以后就会触发FULLGC。

JVM调优就是减少STW(Stop the work)也就是尽量减少FULLGC的触发次数。接下来,讲一下第四部分,真实的调优案例加详细过程分析:

8个G的电脑  3-5个G给操作系统   4-5个G给java虚拟机   堆2-3个G差不多   剩下给元空间,栈。

300个订单/秒    每秒300个订单对象,一个订单对象大小就是成员变量大小的总和,一般不超过1KB。
                肯定还有其他业务对象,最终放大20倍

所以300*20个订单/秒,可能还会有其他操作,如订单查询等等 ,再放大10倍。

所以300KB*20*10个订单/秒,一秒后全部变成垃圾对象。

伊甸园区大概800M,那么13.6秒后会放满。那么前12秒的都变成了垃圾。但第13秒的60M会放入From区(因为minotorGC会暂停程序)。

5-6分钟老年代就会放满。(2G X 1024KB/60M)=5-6分钟。

调优:让其几乎不发生FULLGC

原来:老年代2G   伊甸园区800M    FROM区100M   To区100M
现在:老年代1G   伊甸园区1.6G    FROM区200M   To区200M。

现在是每过25秒伊甸园区会放满。

-XMs:初始堆大小
-Xmx: 最大堆大小
-XMn: 新生代大小
-Xss: 设置每个线程可使用的内存大小,即栈的大小


-XX:MetaspaceSize、-XX:MaxMetaspaceSize:分别设置元空间最小大小与最大大小(Java8以后)


如果servor区大于50%,也会存入老年代