jvm内存结构

83 阅读7分钟

一、jvm组成结构

JVM = 类加载器(classloader) + 执行引擎(execution engine) + 运行时数据区域(runtime data area)。

二、jvm运行时区域

Java虚拟机在执行Java程序的过程中,会把它所管理的内存区域划分为若干个不同的数据区域。

这些区域有着各自的用途,以及创建和销毁的时间,

  • 有的区域随着虚拟机进程的启动而一直存在(方法区、堆),即主内存,共享数据区域
  • 有些区域则是随着用户线程的启动和销毁而建立和销毁(虚拟机栈、本地方法栈、程序计数器),即工作内存,线程私有数据区域

开线程影响哪块内存?

  • 每当有线程被创建的时候,JVM就需要为其在内存中分配虚拟机栈本地方法栈来记录调用方法的内容,分配程序计数器记录指令执行的位置,这样的内存消耗就是创建线程的内存代价。

2.1、堆

  • 在虚拟机启动时创建,用来存放对象实例,几乎所有的对象实例都在这里分配内存;

    存储数据类型:

    • 基本数据类型:成员变量,其变量名及其数据值存放在堆内存中。
    • 引用数据类型:成员变量和局部变量,对象存放在堆内存中,其变量名和地址值存放在栈中,该地址值指向所引用的对象。
  • 被所有线程共享;

  • Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC堆。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代(比例为1:2);新生代又有Eden空间、From Survivor空间、To Survivor空间三部分(比例为8:1:1)

    新生代要负责存储以下情况的对象:

    • 小对象先分配到eden区和from区,gc一次,就将存活对象移到to区,清理eden区和from区。

    老年代要负责存储以下情况的对象:

    • 老年代要存储gc大于15的对象;

      from区to区来回倒腾15次对象还没被回收就进入老年代

    • 为新生代进行空间担保,新生代发生gc,且放不下时,这个对象直接进入老年代;

      例:对象占20兆,gc后,没被回收,from区只有5兆,此时直接进老年代

    • 大对象,对象超过eden区的1/2直接进老年代。

  • Java 堆不需要连续内存,并且可以通过动态增加其内存,增加失败会抛出OutOfMemoryError异常

内存中的堆栈和数据结构中堆栈的区别:

  • 内存中的堆栈和数据结构中的堆栈没有任何关系,是两个完全不同的概念。它们除了名字一样,没有必然的联系。

  • 内存中的堆栈我们可以理解为是内存卡中的一块空间,由操作系统内核来决定哪一块是堆内存,哪一块是栈内存,大小又是多少。是物理上的概念。

  • 数据结构里的堆栈指的是为了实现某种应用而抽离出来的操作,比如世界富豪排行榜。是组织数据的一种手段或工具。是抽象上的概念。

2.2、方法区

  • 用于存放 常量、静态变量、已被加载的类信息、即时编译器编译后的代码 等数据;

  • 被所有线程共享;

  • 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机把它当成永久代(Permanent Generation)来进行垃圾回收

    • 在 Java6 版本中,永久代在方法区;到了 Java7 版本,永久代的运行时常量池和静态变量被合并到了堆中;而到了 Java8,永久代被元空间(处于本地内存)取代了。

    • 为什么要用元空间替换永久代呢?

      • 为了融合 HotSpot JVM 与 JRockit VM,因为 JRockit 没有永久代;
      • 永久代内存经常不够用或发生内存溢出。
  • Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出OutOfMemoryError 异常

2.2.1、运行时常量池
  • 运行时常量池是方法区的一部分;
  • Class文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载时被放入这个区域;
  • 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池;
2.2.2、字符串常量池

String对象创建有两种方式:

  • 字面量形式创建

    String str1 = "aaa";
    解释:字符串常量池中不存在aaa这个字符串对象的引用,
    	所以新创建一个字符串对象,然后将这个引用放入字符串常量池。
    	
    String str2 = "aaa";
    解释:字符串常量池中已经存在aaa这个字符串对象的引用,
    	于是将已经存在的字符串对象的引用返回给变量str2,这里不会再重新创建字符串对象。
    	
    结论:System.out.println(str1 == str2); 结果为true
  • 使用new创建

    不论字符串常量池有没有相同内容对象的引用,都重新创建字符串对象,然后将这个引用存储到栈内存中。

    String str3 = new String("aaa");
    String str4 = new String("aaa");
    
    结论:System.out.println(str3 == str4); 结果为false。
    
    如果想让new出来的String对象的引用加入到字符串常量池中,可以使用intern方法。
    即String str4 = str3.intern();这时候System.out.println(str3 == str4); 结果为true

string、stringbuilder、stringbuffer的区别:

  • string是不可变字符序列,stringbuilder和stringbuffer是可变字符序列。
  • stringbuffer是线程安全的,执行速度慢;stringbuilder是线程不安全的,执行速度快。

2.3、虚拟机栈

  • 虚拟机栈记录的是Java方法执行的内存模型。每个方法执行时,JVM都会同步创建一个栈帧,并将栈帧压入虚拟机栈中。方法执行完毕后,对应的栈帧会出栈。每个栈帧一般包括以下四个部分:

    • 局部变量表:用于存放方法参数和方法内部定义的局部变量。
    • 操作数栈:字节码指令的操作数存储栈
    • 动态连接:用于存放指向运行时常量池的引用
    • 返回地址:记录上层调用方法调用位置
  • 存储数据类型

    基本数据类型:局部变量,其变量名及其数据值存放在栈内存中。

    局部变量中的基本数据类型都是存储在栈内存中吗?

    答案:不是。

    int[] array = new int[]{1,2};
    由于new了一个对象,所以new int[]{1,2}这个对象是存储在堆中的,
    也就是说1,2这两个基本数据类型是存储在堆中。
    
  • 虚拟机栈是每个线程独有的,生命周期与线程相同;

  • 虚拟机栈存在两类异常情况:如果线程请求的栈深度大于虚拟机允许的最大深度,会抛出StackOverflowError异常;当栈扩展时无法申请到足够的内存则会抛出OutOfMemoryError异常。

2.3、本地方法栈

  • 为虚拟机使用到的 Native 方法服务;
  • 线程私有,生命周期与线程相同;
  • 与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowErrorOutOfMemoryError异常;

2.5、程序计数器

  • 当前线程所执行的字节码的行号指示器;
  • 线程私有,生命周期与线程相同;
  • 在虚拟机的概念模型里,字节码解释器工作时就是 通过改变这个计数器的值来选取下一条需要执行的字节码指令,如:分支、循环、跳转、异常处理、线程恢复(多线程切换)等基础功能;
  • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(undefined);
  • 程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,所以此区域不会出现OutOfMemoryError的情况。

三、总结

jvm内存结构是解决java中数据如何存放的问题。