二 JVM整体结构深度解析

一 JDK体系结构(这里以JDK8进行讲解)

在这里插入图片描述
Java语言的跨平台特性
在这里插入图片描述
JVM整体结构及内存模型
在这里插入图片描述
其中,JVM三大部分:类装载子系统、运行时数据区、字节码执行引擎

  1. “类装载子系统”由C++实现,目的是把Match.class字节码文件放入JVM内存区域(放入JVM的运行时数据区)
  2. 通过字节码执行引擎去执行内存区域里的代码(字节码执行引擎也是C++实现的)
  3. 我们只需要关注JVM里的运行时数据区即可
  • 堆:存放new出来的对象,new出来的对象不一定都在堆里,也可能放在栈里(后边会讲)
  • 栈(虚拟机栈):用来存放局部变量;多个线程时,每个线程都会在虚拟机栈里开辟属于自己的内存空间来存放自己的局部变量;

二 虚拟机栈

在这里插入图片描述

新建Match.java:

public class Math {
    
    
    public static final int initData = 666;
    public static User user = new User();

    public int compute() {
    
      //一个方法对应一块栈帧内存区域
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
    
    
        Math math = new Math();
        math.compute();
    }
}
  • 局部变量表:存放方法里的局部变量值;
  • 操作数栈:程序在运行过程中,数据在运行期间要做运算,也是需要存放的;就是在操作数在程序运行过程中,要做操作的一块临时的中转存放数据的内存空间;
  • 动态链接:在程序运行过程中,把符号引用转变为直接引用;(静态链接指的是在程序加载过程中…)
  • 方法出口:当main方法里的compute方法执行完后,要回到main方法里的哪一行继续执行,方法出口记录的就是这里继续执行的行的信息;

一个方法对应一块栈帧内存区域:
在这里插入图片描述
方法执行完后,方法里的局部变量所占用的内存空间全部都要释放掉,即compute方法被调用完后,对应的栈里的内存要释放掉——出栈(对应数据结构里的栈:FILO),后分配的空间会先被释放掉;

在这里插入图片描述

查看虚拟机栈是怎么操作的

使用javap指令,把Match.class编译为Match.txt:
在这里插入图片描述
打开Match.txt:
在这里插入图片描述
上图展示的是JVM内部的汇编语言(不是我们大学学习的汇编),每条指令都有着属于自己的意义,每条指令对应的意义:JVM指令手册

我们来看compute()方法
在这里插入图片描述
先看第一行代码:iconst-1,去查JVM指令手册如下

在这里插入图片描述
即:
在这里插入图片描述
同理,查询JVM手册可知,第二条指令istore_1的意思是把int类型的值(这里值是1)存入局部变量1(这里的1指的是局部变量表的索引,并不是数字1;其中局部变量0放的是this,即代表调用这个方法的对象)
在这里插入图片描述
把操作数栈里的1放入局部变量表里:
在这里插入图片描述
也就是说第二条JVM指令istore_1执行完后是这样的
在这里插入图片描述
局部变量表不是栈结构(是数组结构),操作数栈是栈结构

同理,其他指令的意义也去查手册即可,

三 程序计数器

和栈一样,程序计数器也是每一个线程独有的,存放的是马上要运行的代码在内存里的位置(行号),每执行完一行代码,字节码执行引擎都会去修改程序计数器里的值
在这里插入图片描述
为什么要设计程序计数器呢?
程序都是多线程的,假设线程1正准备执行第3行代码时,突然被其他优先级高的线程抢占了CPU资源了,此时当前线程1需要挂起,将来是需要恢复的,线程1恢复的时候要告诉CPU从第三行开始执行,这就是程序计数器的作用,负责记录程序执行到第几行了;

这里的main方法里的局部变量表,与上边普通方法的局部变量表有点不一样,这里main方法new了一个对象,而new出来的对象一般都放在堆内存里,可是局部变量里也有match对象信息,那二者是什么关系呢?注意:此时的局部变量里放的不是match对象,而是存放match对象在堆内存里的地址信息;

在这里插入图片描述
栈与堆的关系

栈里存放有很多局部变量与对象类型,而对于对象类型的数据,实际是存放在堆里的,栈里存放的就是对象类型数据在堆里的内存地址

四 方法区

在这里插入图片描述

代码运行时会生成很多常量池(即运行时常量池),这些常量池就放在了方法区里,如compute方法就是放在方法区里的常量池里了;

常量池有很多,包括运行时常量池、八大基本数据类型常量池、字符串常量池等,后期会细讲;

运行时常量池包括:常量、静态变量、类信息
在这里插入图片描述
user对象是静态变量,new出来的对象是放在堆里的,所以方法区里存放的只是user对象在堆里的内存地址;

方法区与堆的关系:
当new出来的是静态变量的对象类型时(如上边的user对象),方法区里存放的是这个对象在堆里的地址信息;

方法区在JDK1.8之前有一块区域叫永久代,jdk1.8之后叫元空间,用的是物理内存(即内存条,不是JVM里的内存区域),不做设置的话,默认21M,但随着系统不断运行,会把剩余的物理内存全部用完

五 本地方法栈

本地方法:比如我们new一个线程的时候,底层会调一个native修饰的方法,这个方法就是本地方法,它是由C++实现的
在这里插入图片描述
本地方法由来:JAVA是在1995年面世的,之前的系统都是C++实现的,在JAVA出来后,C++系统免不了需要与JAVA系统进行交互,JAVA里的本地方法就会调用C++里的dll文件(C++库)

本地方法栈:提供本地方法的内存空间;

每一个线程都有自己的虚拟机栈、程序计数器、本地方法栈
在这里插入图片描述

六 堆

由两部分组成,年轻代与老年代;一般老年代占2/3,年轻代占1/3,这个比例可以调节;

年轻代分为Eden区和Survivor区,Survivor又分两部分s0与s1,总的比例为8:1:1;
在这里插入图片描述
new出来的对象一般放在Eden区(也不一定,大部分放在这里了),Edent被放满后,JVM会做GC垃圾回收(这里GC是minor GC),把里边没有用的对象进行回收(GC垃圾回收,后期博客里会细讲);如果一个对象被GC后还存活,那么分代年龄S0会加1,此时当Edent又被放满后,再次触发GC,此时的GC不仅回收Edent区域的对象,还会回收S0区域,GC后把Edent和S0里任然存活的对象放在S1里,S0到S1的对象的分代年龄又会继续加1;此时触发GC时,又把Edent和S1里存活的对象放S0里了,如果是S1里放S0里的对象,则该对象的分代年龄再加1,如果分代年龄加到15(可以设置,最大是15,不同的垃圾收集器,这个值默认不一样)的时候还没有被回收,则该对象会被移动到老年代里;

思考:一个web系统,什么对象会被放在老年代里?
答案:静态变量引用的对象,包括对象值、缓存、spring容器里的对象;

6.1 jvisualvm工具查看堆信息

在死循环里向集合存放数据
在这里插入图片描述
其中,heapTests是GCroot根节点,里边加进去的HeapTest对象,一直都被heapTests引用,直到放到老年代里后也不会被回收掉,最后会出现OOM;

jvisualvm是JDK自带的工具,会识别本地所有的JVM进程
在这里插入图片描述
由于程序一直在运行,所有下边这个图是动态的
在这里插入图片描述
当下边的老年代满了之后,会先触发full GC(回收整个堆与方法区的内存),如果没有回收掉的话,就没有多余内存了,就会出现OOM异常(内存溢出)

6.2 STW

在整个GC过程中会发生STW(stop the word),这种情况对于用户和程序都是不友好的,后期要讲的JVM调优目的之一就是减少GC次数(主要是减少full GC,因为耗时长)或者减少GC执行时间;

七 JVM内存参数设置

主要设置的就是这三块区域
在这里插入图片描述
Spring Boot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件里):

java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar

方法区在JDK1.8之前有一块区域叫永久代,jdk1.8之后叫元空间,用的是物理内存(即内存条,不是JVM里的内存区域),不做设置的话,默认21M;

方法区自动扩容机制

但随着系统不断运行,达到21M满了后,触发full GC,假设GC完使用的内存只有1M了,下次就会把21M调小,比如调到15M,;如果GC后使用内存20M(即回收了很少),则下次可能就会把内存调大,比如30M;

推荐方法区内存设置具体的值,不要不设置:

-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M

-Xss:每个线程的栈大小
-Xms:初始堆大小,默认物理内存的1/64
-Xmx:最大堆大小,默认物理内存的1/4
-Xmn:新生代大小
-XX:NewSize:设置新生代初始大小
-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N
-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。
由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。

StackOverflowError示例:

// JVM设置  -Xss128k(默认1M)
public class StackOverflowTest {
    
    
    
    static int count = 0;
    
    static void redo() {
    
    
        count++;
        redo();
    }

    public static void main(String[] args) {
    
    
        try {
    
    
            redo();
        } catch (Throwable t) {
    
    
            t.printStackTrace();
            System.out.println(count);
        }
    }
}

运行结果:
java.lang.StackOverflowError
	at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:12)
	at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)
	at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)
   ......

结论:
-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多

JVM内存参数大小该如何设置?
JVM参数大小设置并没有固定标准,需要根据实际项目情况分析

猜你喜欢

转载自blog.csdn.net/qq_33417321/article/details/117387023