JVM的基本结构及其各部分详解(二)

3.2 栈帧组成之操做数栈java

操做数栈是栈帧的主要内容之一,它主要用于保存计算过程当中的中间结果,同时做为计算过程当中变量临时的存储空间。数据结构

操做数栈也是一个先进后出的数据结构,只支持入栈和出栈两种操做,许多java字节码指令都须要经过操做数栈进行参数传递。好比add指令,它就会在操做数栈中弹出两个整数并进行加法计算,计算结果会被入栈,如图:显示了iadd先后操做数栈的变化。jvm

3.3 帧数据区函数

除了局部变量表和操做数栈,java栈帧还须要一些数据来支持常量池的解析、正常方法返回和异常处理等。大部分java字节码指令须要进行常量池访问,在帧数据区中保留着访问常量池的指针,方便程序访问常量池。性能

此外,当函数返回或者出现异常时,虚拟机必须恢复调用者函数的栈帧,并让调用者函数继续执行下去。对于异常处理,虚拟机必须有一个异常处理表,方便在发生异常时找处处理异常的代码,所以异常处理表也是帧数据区中重要的一部分,一个典型的异常处理表以下所示:测试

Exception table:优化

from   to    target   typespa

4        16      19      any线程

19       21      19      any代理

它表示在字节码偏移量4--16字节可能抛出任意异常,若是抛出异常,则跳转到字节码偏移量19处执行。当方法抛出异常时,虚拟机就会查找相似的异常表来处理,若是没法在异常表中找到合适的处理方法,则会结束当前函数调用,返回调用函数,并在调用函数中抛出相同的异常,并查找调用函数的异常表来进行处理。

3.4 栈上分配

栈上分配是java虚拟机提供的一项优化技术,它的基本思想是,对于那些线程私有的对象(这里指不可能被其余线程访问的对象),能够将他们打散分配到栈上,而不是分配到堆上。分配到栈上的好处是能够在函数调用结束后自行销毁,而不须要垃圾回收器的介入,从而提升系统的性能。

栈上分配的一个技术基础是进行逃逸分析,逃逸分析的目的是判断对象的做用域是否有可能逃逸出函数体。以下代码所示显示了一个逃逸对象:

private static User u;

public static void alloc(){

  u = new User();

  u.id = 5;

  u.name = "jim";

}

对象u是类的成员变量,该字段有可能被任何线程访问,所以属于逃逸对象,而如下对象显示了一个非逃逸对象:

public static void alloc(){

  User u = new User();

  u.id =  5;

  u.name = "jim";

}

在上述代码中,对象User u 以局部变量的形式存在,而且该对象并无被alloc()函数返回或者出现任何形式的公开,所以它未发生逃逸,因此对于这种状况,虚拟机就有可能将User u 分配在栈上,而不是在堆上。

对于大量的零散小对象,栈上分配提供了一种良好的对象分配优化策略,栈上分配速度快,而且能够有效避免垃圾回收带来的负面影响。但因为栈和堆空间相比,栈空间较小,所以对于大对象没法也不适合在栈上分配。

实例1:测试非逃逸对象的分配空间位置

package com.jvm;

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 = "jim";
  }

  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);
  }
}

使用-Xmx10M -XX:+PrintGC 虚拟机参数运行代码:

[GC (Allocation Failure) 2048K->544K(9728K), 0.0015011 secs]
10

上述代码在主函数中进行了1亿次alloc()调用进行对象的建立,因为User对象实例须要占用约16byte的空间,所以累计分配空间将达到1.5G,若是堆空间小于这个值,就必然发生GC。而此时咱们只分配了最大的堆内存为10M,若是这些对象在堆上建立,必然会引发大量的垃圾回收现象,查看垃圾回收日志,并无。因此,说明其对象分配在栈上。

实例2:对比测试逃逸对象的分配空间位置:

package com.jvm;

public class OnStackTest {
  public static class User{
    public int id = 0;
    public String name = "";
  }
  public static User u;
  public static void alloc(){
    u = new User();
    u.id = 5;
    u.name = "jim";
  }

  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);
  }
}

一样使用虚拟机参数-Xmx10M -XX:+PrintGC设置最大堆空间和打印垃圾回收日志,运行此代码:

可见,发生大量的垃圾回收现象,说明此时堆内存远远不够,须要不断的进行垃圾回收。

 

4 方法区

和堆同样,方法区是一块全部线程共享的内存区域,它用于保存系统的类信息,好比类的字段、方法、常量池等。方法区的大小决定了系统能够保存多少个类,若是系统定义了太多的类,致使方法区的溢出,虚拟机一样会抛出内存溢出错误。

在JDK1.六、JDK1.7中,方法区能够理解为永久区(Perm)。永久区可使用参数-XX:PermSize和-XX:MaxPermSize指定,默认状况下,-XX:MaxPermSize为64M。一个大的永久区能够保存更多的类信息。若是系统使用了一些动态代理,那么有可能会在运行时生成大量的类,若是这样,就须要设置一个合理的永久区大小,确保不发生永久区内存溢出。

 

在JDK1.8中,永久区已经被完全移除,取而代之的是元数据区,元数据区大小可使用参数-XX:MaxMetaspaceSize指定(一个大的元数据区可使系统支持更多的类),这是一块堆外的直接内存。与永久区不一样,若是不指定大小,默认状况下,虚拟机会耗尽全部的可用系统内存。

若是元数据区发生异常,虚拟机同样会抛出异常。

相关文章
相关标签/搜索