从一个class文件来理解java虚拟机的种种

Posted 我于杀戮之中绽放,亦如黎明中的花朵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从一个class文件来理解java虚拟机的种种相关的知识,希望对你有一定的参考价值。

一、JDK的体系结构

 

如上图,可以很清楚的了解到JDK和JRE的关系了。JVM+Lib=JRE  我们用java语言调用java API来编写java程序,通过JDK的javac指令将java文件编译为.class字节码文件给jvm执行,JVM解析字节码,映射到CPU或者OS的指令集被最终执行

 

 

 操作系统最终执行的当然还是机器吗了。只不过linux系统和window系统会有不同的jre来做中间件,提供java代码的运行环境。这样一来,java代码就可以实现一次编写,处处执行。

二、.class文件到底是什么样子的

我自己写了一个类的例子Math

package com.lyb.jvm;

public class Math {
    private static Computer computer = new Computer();
    private static int initData = 666;
public int calculate() {
        int a = true;
        int b = true;
        int c = 30;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.calculate();
        (new Thread()).start();
    }
}

编译后找到Math.class文件,然后打开开一眼。就是下面这样的。

读不懂,不过JDK提供了一些指令,比如javap 就可以将字节码文件反编译为比较容易读懂的代码,然后我在IDEA的终端试了一下

Compiled from "Math.java"
public class com.lyb.jvm.Math {
  public com.lyb.jvm.Math();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int calculate();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: bipush        30
       6: istore_3
       7: iload_3
       8: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/lyb/jvm/Math
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method calculate:()I
      12: pop
      13: new           #5                  // class java/lang/Thread
      16: dup
      17: invokespecial #6                  // Method java/lang/Thread."<init>":()V
      20: invokevirtual #7                  // Method java/lang/Thread.start:()V
      23: return

  static {};
    Code:
       0: new           #8                  // class com/lyb/jvm/Computer
       3: dup
       4: invokespecial #9                  // Method com/lyb/jvm/Computer."<init>":()V
       7: putstatic     #10                 // Field computer:Lcom/lyb/jvm/Computer;
      10: sipush        666
      13: putstatic     #11                 // Field initData:I
      16: return
}

 

第二种就简单多了,然后我在网上找了一份对照代码表。https://blog.csdn.net/wangfantastic/article/details/79736860

三、类加载执行的过程

一个java程序是用java Math.class这个指令通过类装载子系统装载到JVM的内存中并被字节码执行引擎执行的的过程如下

四、JVM的内存模型

1.栈和栈帧

栈是用来存放程序运行过程的局部变量的。如Math这个类对象在执行的时候,首先执行main方法,其中有一个局部变量math,并且main方法中调用的calculate这个方法中又有a,b,c这三个局部变量。如果都放在同一个区域当然是分不清的,所以不同的方法会再划分出不同的块来存放各自的局部变量。这个块就是栈帧

main方法一旦执行会启动一个主线程,此时JVM的内存中就会在 栈(线程)部分为主线程划分一个内存区域。同样的,如果有第二个线程,会再从栈中划分出一个内存区域给这个线程。每一个线程的栈模型如下图

 

栈当然是满足FILO原则的,main方法先执行,需要他的局部变量,划分一个区域main先进,calculate后进,当calculate执行完了,局部变量当方法执行完,是要全部销毁的,所以calculate的栈帧先出栈。这也就是为什么java在设计的时候要选择栈作为内存结构中的局部变量存放区域,因为这和java代码中后调用的方法反而先执行完是吻合的。

2.栈帧的内部

栈帧当然不是简单的就存储局部变量,她还要分为:局部变量表,操作数栈,动态连接,方法出口

 

局部变量表可以理解为方法中定义的局部变量,操作数表就是说一些中间数据,比如int a=1;这里的1就是一个操作数而a是局部变量。方法出口相当于一个标记,比如main方法调用caculate方法,当calculate方法执行结束,知道回去main方法中的那个位置继续执行。这就和程序技术器类似,程序计数器是针对每一个线程的,每一个线程都有一个程序计数器,在运行的时候如果线程挂起了,会记录当前线程执行的代码所在的行,这样当线程再次被唤醒的时候可以接着执行,而不是重新执行。程序计数器是由字节码执行引擎来维护的。局部变量当然不仅仅是简单的数据类型,可能还是一个对象,比如math对象,那这个局部变量表中存放的其实是math对象的地址指针,真正new出来的对象是放在堆内存中的。所以栈和堆之间有一个栈指向堆的关系。

五、方法区(java8之前的永久代)

一个字节码文件被类加载器加载后其实是放在JVM内存模型的具体是方法区中的,方法区主要存放的是类相关的信息,比如静态变量,常量(static final),还有就是元数据,类的元数据包含很多,比如这个类的名字,修饰符,方法名字,修饰符,方法里面的指令等等,可以说构成类的信息都会存放在元数据中。方法区在java8之后是直接使用物理内存了。

 

 如果一个静态变量=new User();这样,静态变量放在方法区,而new的对象放在堆中。所以方法区中放的其实就是该对象的指针地址。而对象和数据元之间也并不是单向的关系,对象在创建的时候,会将他的元信息保存在对象头当中。对象并不是简单的属性和方法构成的,对象的数据模型如下图

 

看到这里我好像明白了什么,对象头中有包括元数据的指针也就是类型指针。所以说方法区和堆之间是一个双向关联的关系。

六、本地方法栈

本地方法栈就是说95年在java出现的时候,大家发现了java的优势,很多新的程序使用了java开发,但是与之前的c程序之间并没有很好的对接。当需要调用C语言的.dll文件(类似jar包)的时候就会执行一些native的方法。这些方法的变量就是存放在这个本地方法栈中的。

七、堆

1.堆内存中什么样的对象会被GC清理

就像前面讲到的,一个线程会分配一个栈,一个方法会分配一个栈帧来存放局部变量,如果一个局部变量是引用类型的,就会在堆中有一个对象。当方法执行结束的时候,局部变量被销毁,就不再有什么指向这个局部变量了。其他的程序也基本很难使用这个对象了,此时这个对象就是需要被GC的对象。也可以用可达性算法来判断对象是否可以被销毁。

可达性算法:在上面的分析可以知道,栈中的局部变量和方法区的静态变量,本地方法栈中的局部变量都是可能指向堆的,这些可以指向的出发点就是GC Root,这一系列根出发一直向下找的对象形成一个网络。在这个网络中的对象就是可达对象,否则是不可达对象,不可达对象久要被销毁。

2.垃圾回收的机制

 

 如上图,minor gc是由字节码执行器引擎来触发的,堆内存又分为年轻带和老年代。年轻带中的Eden区放满了之后就会进行一次gc,一次gc之后的对象会被放到From区,且每次gc之后的对象复制会将对象头中的对象分代年龄+1 。当Eden区又满了再gc一次,此时会gcEden区和From区,gc后的对象全部放到To区并年龄+1。再过一段时间Eden区又满了gc一次,清理Eden和To区,gc后存活的对象放到From区。也就是说From区和To区中总又一个是空的,用来存放下一次GC之后的所有对象。达成一个循环。当对象的年龄java默认为15时,判定这些对象是老不死的对象,要一直被拷贝移动。耗费资源和空间,然后就会被移到老年代中。

3.java自带的GC监视器

使用java自己的jvisualvm命令启动GC监视器,来查看垃圾回收的过程,最后这个Tab需要自己在网上下载安装也很简单

我是参考https://blog.csdn.net/u012988901/article/details/102517829进行安装的,然后我写了一个死循环,准备作死运行一波看看效果,代码如下,好了,我要开始运行了

package com.lyb.jvm;

import java.util.ArrayList;
import java.util.List;

public class NeverStop {
    byte[] b=new byte[1024];

    public static void main(String[] args) throws InterruptedException {
        List list=new ArrayList();//list中会引用每次新建的NeverStop类的对象,这样的话就不会轻易被gc,而是会满了才gc,但是都有引用,又gc不了,预测会内存溢出
        while(true){
            list.add(new NeverStop());

            Thread.sleep(10);
        }
    }
}

 

 开始了,下面是刚开始运行我调用到java visualvm的过程监视到的情况,可以看到,已经进行几次gc了,老年代中已经有一些对象了,但是好像程序还很坚挺

 

 

 

a few minutes later,老年代基本也快满了。。。疲软中...

 

 

 果然,最后到IDEA中,发现程序已经OVER了, 这个提示也是非常的OK直接说是堆空间

 

对于以上的情况面对非常高并发,大数据量访问的情况也是要进行JVM调优的,以后收集一些JVM调优的资料,学习后发出来分享。下面只给出一个思路

4.jvm内存调优的思路

可见总有一天老年代也会放满,这时候字节码执行器引擎就会出发full gc,父GC,会堆整个堆内存进行垃圾回收,其实每当gc,不管是minor或者是full的时候都会暂停一些执行中的线程,而执行垃圾回收的线程。这个时候程序可能出现卡顿现象,严重的时候会发生STW,也就是STOP THE WORLD......   所以JVM调优主要就是两个方向:A.减少gc的次数       B.减少每次gc的时间

比如某个web应用再启动的时候可以看到他的gc日志,如下图详细记录了gc的情况,可以看出,才几秒的时间,就进行了两次full gc,原因是元数据空间不足,也就是方法区不足,因为jvm默认的方法区内存才只有几十兆,如果是一下非常大的老牌web项目,比如一些erp,plm等,就会出现这种情况,这个时候就需要去配置,增大方法区的内存空间,来优化gc了。

 

 

以上是关于从一个class文件来理解java虚拟机的种种的主要内容,如果未能解决你的问题,请参考以下文章

深入理解JVM(③)虚拟机的类加载时机

javasecclass文件结构

java虚拟机java虚拟机的类加载机制

深入理解JVM(③)虚拟机的类加载器(双亲委派模型)

理解Java虚拟机体系结构

通俗易懂讲解Java虚拟机的Class文件