【JVM】详细分析JVM内存区域

前面已经分析过JVM内存区域的各个功能,可是过于抽象和散乱,接下来就根据Math.java、Math.class、JVM结构图和Java VisualVM工具来完整详细的理解一个代码在JVM中是如何执行的,以及JVM是如何对内存区域进行垃圾收集的。java

(1)准备好Math.java、Math.class和JVM结构图

(1.1)认清*.java、*.class和JVM之间的关系

首先,Math.java文件是咱们用编译器编写的源代码文件,可是,这种文件人看得懂机器看不懂,因此Math.java文件要转换成字节码文件Math.class,也就是二进制文件;而后把字节码文件Math.class传进JVM让机器进行编译处理,最后把代码编译成机器码,实现对机器的控制。
Math.java、Math.class和JVM结构图
如图所示,不一样的文件对应不一样的编译器,最终都会转换成字节码文件,而后才能传入JVM:
在这里插入图片描述
以下图所示:字节码文件翻译成机器码还有三个步骤
在这里插入图片描述程序员

(1.2)编写一个简单的Math.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();
        System.out.println("test");
    }
}

(1.3)对Math.java文件进行反编译,获得Math.class文件

(1)先执行main方法,而后找到target目录下已经编译好的Math.class文件,在控制器中打开
在这里插入图片描述
(2)此时虽然已经获得class文件,可是文件里都是字节码,不方便阅读,因而使用命令进行反编译,获得一个咱们容易阅读的代码内容
在这里插入图片描述(3)在控制器中获得咱们想要的字节码文件内容web

public class com.itheima.Math {
  public static final int initData;

  public static com.itheima.User user;

  public com.itheima.Math();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int compute();
    Code:
       0: iconst_1 //把int类的常量1压入操做数栈---1;
       1: istore_1 //把int类的值存入局部变量1---int a=1;
       2: iconst_2 //把int类的常量2压入操做数栈---2;
       3: istore_2 //把int类的值存入局部变量2---int b=2;
       4: iload_1 //
       5: iload_2
       6: iadd
       7: bipush        10
       9: imul
      10: istore_3
      11: iload_3
      12: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/itheima/Math
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method compute:()I
      12: pop
      13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      16: ldc           #6                  // String test
      18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      21: return

  static {};
    Code:
       0: new           #8                  // class com/itheima/User
       3: dup
       4: invokespecial #9                  // Method com/itheima/User."<init>":()V
       7: putstatic     #10                 // Field user:Lcom/itheima/User;
      10: return
}

(1.4)准备一个JVM运行时数据区域图

在这里插入图片描述
完成上面的材料准备工做,就能够跟着代码来分析执行的流程了!!!算法

(2)从Math.class开始分析流程

(2.1)Math.class进入JVM

Math.class文件最开始放在磁盘中,而后通过【类装载子系统】装入【方法区】,字节码执行引擎读取方法区的字节码自适应解析,边解析边运行,而后PC寄存器指向了main函数所在的位置,虚拟机开始在栈中为main函数预留一个栈帧,而后开始运行main函数,main函数里的代码被字节码执行引擎映射成本地操做系统里相应的实现,而后调用本地方法接口,本地方法运行的时候,操纵系统会为本地方法分配本地方法栈,用来储存一些临时变量,而后运行本地方法,调用操做系统APIi等等。spring

在这里插入图片描述

在这里插入图片描述
上图代表:jvm 虚拟机位于操做系统的堆中,而且,程序员写好的类加载到虚拟机执行的过程是:当一个 classLoder 启动的时候,classLoader 的生存地点在 jvm 中的堆,而后它会去主机硬盘上将A.class装载到jvm的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),而后在堆内存生成了一个A字节码的对象,而后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载本身的classLoader。数据库

(2.2)认识方法区

类加载器把加载到的全部信息放入方法区,也就是说方法区里存放了类的信息,有类的static静态变量、final类型变量、field自动信息、方法信息,处理逻辑的指令集,咱们仔细想一想一个类里面也就这些东西。数组

而堆中存放是对象和数组,
1-这里的对应关系就是 “方法区–类” “堆–对象”,以“人”为例就是,堆里面放的是你这个“实实在在的人,有血有肉的”,而方法区中存放的是描述你的文字信息,如“你的名字,身高,体重,还有你的行为,如吃饭,走路等”。
2-再者咱们从另外一个角度理解,就是从前咱们得知方法区中的类是惟一的,同步的。可是咱们在代码中每每同一个类会new几回,也就是有多个实例,既然有多个实例,那么在堆中就会分配多个实例空间内存。tomcat

方法区的内容是边加载边执行,例如咱们使用tomcat启动一个spring工程,一般启动过程当中会加载数据库信息,配置文件中的拦截器信息,service的注解信息,一些验证信息等,其中的类信息就会率先加载到方法区。但若是咱们想让程序启动的快一点就会设置懒加载,把一些验证去掉,如一些类信息的加载等真正使用的时候再去加载,这样说明了方法区的内容能够先加载进去,也能够在使用到的时候加载。多线程

方法区是被字节码执行引擎直接执行的,因此静态信息都是在类加载的时候就建立了的,非静态的信息是在堆内存实例化对象的时候才建立,因此静态方法中不能调用非静态的方法和变量
在这里插入图片描述jvm

(2.3)认识栈内存

(1)在以前学习Static关键字的时候了解到,Math math=new Math();,在这个代码执行时分为三块:

  1. Math math:负责在栈内存开辟内存空间,用来存放类对象的名称,其实就是对象的地址,此时对象尚未完成初始化,还不能使用
  2. new Math():负责在堆内存开辟内存空间,用来存放类对象,此时完成对象的初始化,对象可使用了
  3. Math math=new Math():负责将栈内存里的地址指向堆内存里的对象,此时类对象的名字和类对象的实例合为一体,咱们就能够经过类对象名称来调用对象中的信息了

(2)咱们把栈放大,能够看到在栈里面,每个方法都有一个本身的栈帧,main方法有一个栈帧,compute方法有一个栈帧,在代码中main方法还调用了compute方法,那么它们是怎么存放信息、处理信息并相互传递信息的呢?

由于先运行main方法,因此main方法先入栈,再调用compute方法,再入栈,由于栈的特色是先入后出,因此compute先出栈,接下分析compute的结构和信息处理过程。

方法栈帧的内存又能够分为4个部分:局部变量表、操做数栈、动态连接、方法出口。其中局部变量表用来存放变量的名称a、b、c等,操做数栈存放要进行赋值的操做数一、2。操做流程以下:
在这里插入图片描述
在这里插入图片描述
把操做数栈里的值赋值给局部变量表中的变量,还有其余一系列操做,操做结果就获得了 c=30,而且使用return语句把结果返回了,可是这个值是怎么传递给main方法的呢?那就是经过方法出口,把结果传递给栈中的下一个方法(方法栈帧是按照调用顺序入栈的,因此结果都是顺着往下传递),此时compute方法栈帧完成出栈。
在这里插入图片描述
在这里插入图片描述

再来分析main方法栈帧,它一样也包含局部变量表
在这里插入图片描述
在这里插入图片描述
从上面的分析能够看出,经过栈和命令使得方法完成了数据的赋值和处理,还有方法的调用和处理等操做,最后把方法的处理的结果经过方法出口传递给调用者。

(2.4)认识程序计数器

在Math.class文件中能够看到,每一行指令都有一个行数,若是线程在执行指令的时候忽然被挂起了,等线程被唤醒后要接着上次被挂起的位置执行怎么办?

那就要给每个方法线程配备一个程序计数器,用来标记线程执行的位置,每执行一行指令,字节码执行引擎都会命令程序计数器更新到最新的行数。

因此程序计数器的做用为:

  1. 读取指令:字节码解释器经过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 标记位置:在多线程的状况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候可以知道该线程上次运行到哪儿了。

在这里插入图片描述

(2.5)认识堆

在main方法中对Math进行实例化,栈内存中的math地址指向堆内存中的math对象,可是math对象对应的那些属性和方法不会也都放在堆内存中,这就要讲到下一个区域方法区(元空间)了

在这里插入图片描述

(2.6)方法区,栈、堆之间的过程

类加载器加载的类信息放到方法区---->执行程序后,方法区的方法压入栈的栈顶---->栈执行压入栈顶的方法---->遇到new对象的状况就在堆中开辟这个类的实例空间。(这里栈是有此对象在堆中的地址的)

(3)详细分析堆内存里对象的一辈子

堆分为:新生代、老年代
新生代分为:Eden区、From Survivor区、To Survivor区

全部新建立的对象都会放在堆内存的Eden区里,这个区最大,当Eden的空间用完了,程序又须要建立对象,就会使用minor GC算法对新生代进行垃圾收集,Eden区中死亡的对象被垃圾收集器处理掉,存活的对象进入From Survivor区;若是From Survivor区也满了,再进行minor GC算法,还活着的对象从From Survivor区进入To Survivor区,而且对象年龄+1,接下来每次有存活的对象就在From Survivor区、To Survivor区两个区里来回跑。

当对象的年龄达到15时,说明这个对象真的很难死亡,那么就能够把这个对象传入老年代,在老年代中若是内存也被放满了就会使用full GC进行垃圾收集
在这里插入图片描述

(4)堆内存中垃圾收集的直观监控

(4.1)打开监测工具而且安装插件

JDK为咱们提供了监测工具VisualVM,咱们直接在 cmd 中输入 jvisualvm 命令就能够进入

在这里插入图片描述
可是工具VisualVM是没有安装Visual GC的插件的,安装Visual GC的插件参考博客:安装Visual GC的插件

(4.2)编写一个不断往堆里放对象的代码

public class HeapTest {
    byte[] a=new byte[1024*100];

    public static void main(String[] args) throws InterruptedException {
        ArrayList<HeapTest> heapTests=new ArrayList<>();
        while (true){
            heapTests.add(new HeapTest());
            Thread.sleep(10);
        }
    }
}

(4.3)执行测试代码看效果

在这里插入图片描述

  1. Eden区的内存先是上升,而后新生代被minor GC一次,它就清零了,死亡对象被回收了,存活的对象放进from Survivor里了
  2. Old里的对象在不断增长