Back

JVM笔记之JVM结构

JVM运行时数据区

运行时数据区(分布在操作系统堆中,由程序员管理)

  • 方法区
  • 虚拟机栈(Java栈)
  • 堆(Java堆)
  • 本地方法栈/区
  • 程序计数器

JVM结构
JVM结构

虽然这几个区域都是属于运行时数据区,但是这几个区域的创建时机是不一致的,有的是随虚拟机启动而创建的,随虚拟机销毁而销毁;有的是随线程创建而创建,随线程销毁而销毁。

线程独享的区域

程序计数器

程序计数器是来指示当前线程正在执行的JVM指令,因此程序计数器是线程独有的。一个JVM支持多个线程,每一个线程都要自己的程序计数器。
如果线程正在执行的方法是Java方法,则程序计数器保存的是当前线程正在执行的JVM指令,如果正在执行的方法是Native方法,则保存为空(undefined)。

虚拟机栈

每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用。每个栈中的数据(原始类型和对象引用)都是私有的。数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会自动消失。
栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

本地方法区

本地方法区存储着native方法的调用状态,一般会随着线程创建而针对每一个线程分配。

全局共享的区域

方法区

方法区是可供各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量等。当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域。

由于早期的 Hotspot JVM 实现,很多人习惯于将方法区称为永久代(Permanent Generation)。

运行时常量池 :方法区的一部分(Java jdk1.7中的常量池是移到了堆中,同时在jdk1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域) ,存放着类中固定的常量信息、方法、和field的引用信息。JVM在加载类的时候会为每一个Class分配一个独立的常量池。

JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,存储着所有类实例即类对象和数组对象。对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定

理所当然,堆也是垃圾收集器重点照顾的区域,所以堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。

JVM
JVM

JVM 本身是个本地程序,还需要其他的内存去完成各种基本任务,比如,JIT Compiler 在运行时对热点方法进行编译,就会将编译后的方法储存在 Code Cache 里面;GC 等功能需要运行在本地线程之中,类似部分都需要占用内存空间。

JDK 已经发生了很大变化,Intern 字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,Intern 字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。

堆内部结构

堆内部结构
堆内部结构

  1. 新生代

    新生代是大部分对象创建和销毁的区域,在通常的 Java 应用中,绝大部分对象生命周期都是很短暂的。其内部又分为 Eden 区域,作为对象初始分配的区域;两个 Survivor,有时候也叫from、to 区域,被用来放置从 Minor GC 中保留下来的对象。

    JVM 会随意选取一个 Survivor 区域作为“to”,然后会在 GC 过程中进行区域间拷贝,也就是将 Eden 中存活下来的对象和 from 区域的对象,拷贝到这个“to”区域。这种设计主要是为了防止内存的碎片化,并进一步清理无用对象。

  2. 老年代

    放置长生命周期的对象,通常都是从 Survivor 区域拷贝过来的对象。当然,也有特殊情况,我们知道普通的对象会被分配在 TLAB 上;如果对象较大,JVM 会试图直接分配在 Eden 其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM 就会直接分配到老年代。

  3. 永久代

    这部分就是早期 Hotspot JVM 的方法区实现方式了,储存 Java 类元数据、常量池、Intern 字符串缓存,在 JDK 8 之后就不存在永久代这块儿了。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus