Java虚拟机的架构(二)

时间:2022-12-27 12:47:44

Java虚拟机的基本结构

Java虚拟机的架构(二)
类加载子系统:负责从文件系统或者网络加载Class信息,加载的类信息存放于一块称为方法区的内存空间;
  1. 方法区:除了类的信息外,可能还会存放运行时常量池信息,包括字符串,字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射);
  2. Java堆:它会在虚拟机启动的时候建立,它是Java程序最主要的内存工作区域。几乎所有的Java对象实例都存放于Java堆中。堆空间是所有线程共享的,这是一块与Java应用密切相关的内存区间;
  3. 直接内存:JavaNIO库允许Java程序使用直接内存。直接内存是在Java堆外的,直接向系统申请的内存区间。通常,访问直接内存的速度会优于Java堆。读写频繁的场合使用此内存为优。直接内存在Java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存;
垃圾回收系统:该系统可以对方法区,堆和直接内存进行回收;堆是回收器的工作重点;Java中,所有的对象空间释放都是隐式的。对于不再使用的垃圾对象,垃圾回收系统会在后台默默工作,默默查找,标识并释放垃圾对象,完成包括Java堆,方法区和直接内存中的全自动化管理;

Java栈:每个Java虚拟机线程都有一个私有的Java栈。一个线程的Java栈在线程创建的时候被创建。栈中保存着帧信息,局部变量,方法参数,同时和Java方法的调用,返回密切相关;

本地方法栈:该栈与Java栈类似,不同在于Java栈用于Java方法的调用,而本地方法栈用于本地方法的调用。Java虚拟机允许Java直接调用本地方法(通常使用C编写);

PC寄存器:寄存器也是每个线程私有的空间,Java虚拟机会为每一个Java线程创建PC寄存器。在任意时刻,一个Java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined;
执行引擎:它是Java虚拟机的最核心组件之一,它负责执行虚拟机的字节码。现代虚拟机为了提高执行效率,会使用及时编译技术将方法编译成机器码后再执行;


虚拟机参数

Java虚拟机可使用Java_HOME/bin/java程序启动(Java_HOMEJDK的安装目录),Java进程的命令行使用方式如下:

java [-options] class [args....]

-optionsb表示Java虚拟机的启动参数,class为带有main()函数的Java类,args表示传递给主函数main()的参数。设定特定的Java虚拟机参数,在options处指定即可


举个栗子:
public class SimpleArgs {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("参数"+(i+1)+":"+args[i]);
}
System.out.println("-Xmx"+Runtime.getRuntime().maxMemory()/1000/1000+"M");
}
}
这段代码打印了传递给main()函数的参数,同时打印了系统的最大可用堆内存,可以使用如下命令行运行这段代码:
Java虚拟机的架构(二)
从结果可以看出,第一个参数 -Xmx传递给Java虚拟机,生效后,使得系统最大可用堆空间为32MB,参数a则传递给主函数main(),作为应用程序的参数

使用的Eclipse的话,在运行对话框的参数选项卡上,也可以设置这两个参数。上图显示了“程序参数”和“虚拟机参数”两个文本框,将所需的参数填入即可;
Java虚拟机的架构(二)

辨清Java堆

Java堆是和Java应用程序关系非常密切的内存空间,几乎所有的对象都存放在堆中。并且Java堆是完全自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理,而不需要显示地释放;
Java堆的构成(最为常见的)分为:
  1. 新生代:存放新生对象或年龄不大的对象;分为eden区,s0区,s1区。s0s1也被称为fromto区域,它们是两个大小相等,可以互换角色的内存空间
  2. 老年代:存放老年对象;
  3. 图示:
  4. Java虚拟机的架构(二)


    绝大多数情况下,对象首先分配在eden区,在一次新生代回收后,如果对象还存活,则会进入s0或者s1,之后,每经过一次新生代回收,对象如果存活,它的年龄就会加1.当对象的年龄达到一定条件后,就会被认为是老年对象,从而进入老年代
    示例1通过下面这段代码展示Java堆,方法区和Java栈之间的关系
    public class SimpleHeap {
    private int id;
    public SimpleHeap(int id){
    this.id=id;
    }
    public void show(){
    System.out.println("My ID is "+id);
    }
    public static void main(String[] args) {
    SimpleHeap s1 =new SimpleHeap(1);
    SimpleHeap s2 =new SimpleHeap(2);
    s1.show();
    s2.show();
    }
    }

    分析:SimpleHeap实例本身分配在堆中,描述SimpleHeap类的信息存放在方法区,main()函数中s1s2局部变量存放在栈中,并指向堆中的两个实例
    Java虚拟机的架构(二)

    函数调用:出入Java栈

    Java栈是一块线程私有的内存空间。和线程关系紧密。线程执行的基本行为是函数调用,每次函数调用的数据都是通过Java栈传递的。
    Java栈是一块先进后出的数据结构,只支持出栈和入栈两种操作;栈中保存的主要内容为栈帧。
    1. 每次函数调用,都会有一个对应的栈帧被压入Java栈,每一个函数调用结束,都会有一个栈帧被弹出Java栈;函数1对应栈帧1,函数2对应栈帧2,依此类推;
    2. 函数1调用函数2,函数2调用函数3,......但函数1被调用时,栈帧1入栈;但函数2被调用时,栈帧2入栈,.....。当前正在执行的函数所对应的帧就是当前帧(位于栈顶),它保存着当前函数的局部变量,中间运算结果等数据;
    3. 当函数返回时,栈帧从Java栈中被弹出。返回方式有两种,正常的函数返回return,另一种是抛出异常;
    4. 在一个栈帧中,至少包含局部变量表,操作数栈和帧数据区几个部分;
    5. 图示:
    6. Java虚拟机的架构(二)
    7. 由于每次函数调用都会生成对应的栈帧,从而占用一定的栈空间。
      如果栈空间不足,那么函数调用自然无法进行。当请求的栈深度大于最大可用栈深度时,系统就会抛出
      *Error栈溢出错误


    举个栗子:使用无限循环的递归调用,来测试栈深度
    public class TestStackDeep {
    private static int count = 0;
    public static void recursion(){
    count++;
    recursion();
    }
    public static void main(String[] args) {
    try{
    recursion();
    }catch(Throwable e){
    System.out.println("deep of calling = " + count);
    e.printStackTrace();
    }
    }
    }

    使用参数-Xss128K执行此段代码,部分输出结果为:


    deep of calling = 2741

    java.lang.*Error

    athey.up2.TestStackDeep.recursion(TestStackDeep.java:19)

    athey.up2.TestStackDeep.recursion(TestStackDeep.java:20)

     


    如果将参数增大,那么调用的层数也会增加;

    函数嵌套调用的层次在很大程度上由栈的大小决定,栈越大,函数可以支持嵌套调用次数越多;


    局部变量表:

    它用于保存函数的参数以及局部变量。局部变量表中的变量只在当前函数调用中有效,函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之销毁;
    局部变量表在栈帧之中,如果函数的参数和局部变量较多,会使得局部变量表膨胀,从而每一次函数调用就会占用更多的栈空间,最终导致函数的嵌套调用次数减少。再来个栗子:两个函数分别包含不等的变量和参数,测试它们谁拥有更深的调用层次;
    public class TestStackDeep {
    private static int count = 0;
    public static void recursion(long a,long b,long c){
    long e=1,f=2,g=3,h=4,i=5,k=6,q=7,x=8,y=9,z=10;
    count++;
    recursion(a,b,c);
    }
    public static void recursion(){
    count++;
    recursion();
    }
    public static void main(String[] args) {
    try{
    // recursion(0L,0L,0L);
    recursion();
    }catch(Throwable e){
    System.out.println("deep of calling = " + count);
    e.printStackTrace();
    }
    }
    }
    //两个函数都使用-Xss128K参数测试。第2个,不包含局部变量的函数的调用层次更深
    
       
       
      栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的;

      栗子:槽位复用示例。localvar1()函数中,局部变量ab都作用到了函数末尾,故b无法复用a所在的位置。而在localvar2()函数中,局部变量a在方法块外不再有效,故局部变量b可以复用a的槽位(1个字)
      public void localvar1(){
      int a = 0;
      System.out.println(a);
      int b = 0;
      }
      public void localvar2(){
      {
      int a = 0;
      System.out.println(a);
      }
      int b = 0;
      }


      通过jclasslib查看:
      localvar1()方法局部变量槽位:Java虚拟机的架构(二)

      localvar2()方法局部变量槽位:Java虚拟机的架构(二)


      局部变量表中的变量也是重要的垃圾回收根节点,被局部变量表中直接或间接引用的对象都是不会被回收的;
      又是栗子:演示局部变量对垃圾回收的影响

      public class LocalVarGC {
      public void localvarGc1(){
      byte[] a = new byte[6*1024*1024];
      System.gc();
      }
      public void localvarGc2(){
      byte[] a = new byte[6*1024*1024];
      a = null;
      System.gc();
      }
      public void localvarGc3(){
      {
      byte[] a = new byte[6*1024*1024];
      }
      System.gc();
      }
      public void localvarGc4(){
      {
      byte[] a = new byte[6*1024*1024];
      }
      int c =10;
      System.gc();
      }
      public void localvarGc5(){
      localvarGc1();
      System.gc();
      }
      public static void main(String[] args) {
      LocalVarGC ins = new LocalVarGC();
      ins.localvarGc1();
      }
      }

      分析:
      localvarGc1中,申请空间后,立即进行垃圾回收,由于byte数组被变量a引用,因此无法回收这块空间。
      localvarGc2中,在垃圾回收前,先将变量a置为null,使byte数组失去强引用,故垃圾回收可以顺利回收byte数组。
      localvarGc3中,在进行垃圾回收前,先使局部变量a失效,虽然a已经离开作用域,但是a依然存在于局部变量中,并且也指向这块byte数组,故byte数组依然无法被回收。
      localvarGc4中,在垃圾回收之前,不仅使变量a失效,更是申明了变量c,使变量c复用了变量a的字,由于变量a此时被销毁,故垃圾回收器可以顺利回收byte数组。
      localvarGc5中,它首先调用localvarGc1,在localvarGc1中并没有释放byte数组,但在localvarGc1返回后,它的栈帧被销毁,自然也包含了栈帧中的所有局部变量,故byte数组失去引用,在localvarGc5的垃圾回收中被回收。


      打印效果:
      可使用参数“
      -XX:+PrintGC”执行这些函数,下面的结果是localvarGc4()的:
       

      [GC6819K->536K(125952K), 0.0078666 secs]

      [Full GC 536K->461K(125952K), 0.0302894 secs]//堆空间从536K,变为461K


      操作数栈:

      主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间;
      该栈也是先进后出的数据结构。许多Java字节码指令都需要通过此栈进行参数传递;

      说明:iadd指令,它会在操作数栈中弹出两个整数并进行加法计算,计算结果会被入栈

      Java虚拟机的架构(二)



        帧数据区

        在帧数据区保存着访问常量池的指针,帮助Java字节码指令进行常量池访问;此外,当函数返回或者出现异常时,虚拟机必须恢复调用者函数的栈帧,并让调用者函数继续执行下去。对于异常处理,虚拟机要有一个异常处理表,方便在发生异常的时候找到处理异常的代码,因此异常处理表也是帧数据区中重要的一部分。一个典型的


        异常处理表:

        Java虚拟机的架构(二)

        它表示字节码偏移量4~16字节可能排除任意异常,如果遇到异常,则跳转到字节码偏移19处执行;

        当方法排除异常时,虚拟机就会查找类似的异常表来进行处理,如果无法在异常表中找到合适的处理方法,则会结束当前函数调用,返回调用函数,并在调用函数中抛出相同的异常,并查找调用函数的异常表进行处理;


        栈上分配

        说明:它是Java虚拟机提供的一项优化技术;


        基本思想:对于线程私有的对象(指不可能被其他线程访问的对象),可以将它们打散分配在栈上,而不是分配到堆;


        好处:函数调用完毕自动销毁,不需要垃圾回收器介入,提高系统性能;


          技术支持:逃逸分析。

          目的:判断对象的作用域是否有可能逃逸出函数体

          举个栗子:下面的代码显示了一个逃逸对象:

          private static User u;
          public static void alloc(){
          u = new User();
          u.id = 5;
          u.name = "geym";
          }
          //对象User u是类的成员变量,该字段有可能被任何线程访问,因此属于逃逸对象

          //再次举个栗子:以下代码片段显示了一个非逃逸对象:

          public static void alloc(){
          User y= new User();
          u.id=5;
          u.name = "geym";
          }
          //对象User以局部变量的形式存在,并在该对象并没有alloc()函数返回,或者任何形式的公开,因此,它并未发生逃逸。虚拟机对于这种情况可能将User分配到栈上,而不在堆上


          //示例:通过此示例显示对非逃逸对象的栈上分配

          public class OnStackTest {
          public static class User{
          public int id = 0;
          public String name ="";
          }
          public static void alloc(){
          User u = new User();
          u.id =5;
          u.name = "geym";
          }
          public static void main(String[] args) {
          long b = System.currentTimeMillis();
          for (int i = 0; i < 100000000; i++) {
          alloc();
          }
          long e = System.currentTimeMillis();
          System.out.println(e-b);
          }
          }

          //这段代码在主函数进行了一亿次alloc()调用进行对象创建,由于User对象实例需要占据约16字节的空间,因此累计分配空间达到将近1.5GB。如果堆空间小于这个值,就会发生GC