《深入理解 Java 虚拟机》- 笔记一

原文地址:LoveDev

Java 虚拟机(英语:Java Virtual Machine,缩写为JVM)屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行,通过对中央处理器(CPU)所执行的软件实现,实现能执行编译过的Java程序码

运行时数据区
运行时数据区

程序计数器

程序计数器是一块较小内存空间,可以看作是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型中,字节码解释器就是通过改变计数器的值选取下一条需要执行的字节码执行,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖该计数器完成

多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,同一时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令,为了保证线程切换后,能够恢复到正确的执行位置,每条线程都有一个独立的程序计数器,这类内存区域为线程私有的内存。

线程如果执行的是 Java 方法,该计数器记录的是正在执行的虚拟机字节码指令地址,如果是 Native 方法计数器值则为空(Undefined),该内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域

Java 虚拟机栈

Java Virtual Machine Stacks,该栈也是线程私有的,生命周期和线程相同。虚拟机栈描述的是 Java 方法执行的内存模式:每个方法都会在执行的同时创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,方法从调用直至执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程,经常提到的栈内存就是说的虚拟机栈,或者说是虚拟机栈中的局部变量表部分

局部变量表存放了编译期可知的各种基础数据类型reference类型returnAddress类型,其中64位长度的 long 和 double 类型的数据会占用2个局部变量空间,其余只占用1个,局部变量表所需内存空间在编译期间分配完成,进入一个方法时,该方法需要在帧中分配多大局部变量空间已经确定,在方法运行期间不会改变局部变量表的大小

在 Java 虚拟机规范中,对该区域规定了两种异常状况:

  • 线程请求的栈深度大于虚拟机允许的深度,将抛出 StackOverFlowError 异常
  • 如果虚拟机栈支持动态扩展,扩展时无法申请到足够内存,将抛出 OutOfMemoryError 异常

本地方法栈

Native method stack,虚拟机栈为虚拟机执行的 Java 方法服务,本地方法栈为 Native 方法服务,sun hotspot虚拟机就把这两个栈区域合二为一,该栈同样会抛出虚拟机栈的两个异常

Java堆

Java heap,Java虚拟机管理的最大的一个区域,用来存放对象实例,被所有线程共享。

Java虚拟机规范规定:所有对象实例和数组都要在堆上分配。随着栈上分配和标量替换等虚拟机优化技术的出现,堆上分配的说法也不是那么绝对了

Java堆是GC管理的最主要区域之一,堆又分为新生代和老年代,在GC使用Copying算法时,新生代又分为Edan space,From survivor space,To survivor space

Java堆可以处于物理上不连续,但逻辑上连续的内存空间,就像电脑磁盘空间一样

可以通过以下参数扩展虚拟机内存:

  • Xms-初始化堆大小
  • Xmx-java heap最大值
  • Xmn-年轻代堆大小
  • Xss-每个线程的栈大小

如果堆中没有足够的内存空间分配给实例,并且无法扩展时将会抛出 OutOfMemoryError 异常

方法区

Method area,为了和 Java 堆区分,它还可以叫做Non-Heap(非堆)。各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译出的代码等

该区域还有一个常叫的名字:永久代(Permanent Generation),这种叫法不太准确,本质上两者并不等价,在HotSpot虚拟机为了可以像管理 Java 堆一样管理该区域,使用GC分代收集中永久代实现方法区,省去了编写内存管理代码的工作。

-XX:MaxPermSize 可以指定永久代的内存上限,在JDK8之前的HotSpot虚拟机中,32位机器默认的永久代的大小为64M,64位的机器则为85M。永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。但是有一个明显的问题,由于我们可以设置永久代的大小,一旦类的元数据超过了设定的大小,程序就会耗尽内存,并出现内存溢出错误

从 JDK1.7 开始已经逐步移除永久代,在 JDK1.7 中存储在永久代的部分数据就已经转移到了Java Heap 或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap,字面量(interned strings)转移到了java heap,类的静态变量(class statics)转移到了java heap

在 JDK1.8 中,HotSpot 虚拟机已经没有 PermGen space 这个区域了,取而代之的是 Metaspace(元空间)

元空间

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

  • -XX:MetaspaceSize:初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值
  • -XX:MaxMetaspaceSize:最大空间,默认是没有限制的

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

  • -XX:MinMetaspaceFreeRatio:在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  • -XX:MaxMetaspaceFreeRatio:在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分,Class 文件除了有类的版本、字段、方法、接口等描述信息,还有一项信息是常量池,用于存放编译期生成的各种字面量符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

运行时常量池相对于 Class 文件常量池的一个重要特征就是具备动态性,Java 语言并非只有预置到 Class 文件常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量加到池中,这种特性被利用的比较多是 String 类的 intern() 方法(想要了解该方法的同学请戳这里

运行时常量池是方法区内的一部分,自然也收到了方法区大小的限制,无法申请到内存时同样也会抛出 OutOfMemoryError 异常

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,这块区域也被频繁使用,也可能抛出 OutOfMemoryError 异常

JDK 1.4之后加入了NIO(new Input/Output)类,引入一种基于通道(channel)和缓冲区(Buffer)的 I/O 方式(想要了解 NIO 更加形象的介绍请戳这里),它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作,因为避免了在Java 堆和 Native 堆中来回复制数据,在一些情况下能够显著的提升性能

直接内存不会受到 Java 堆大小限制,但是会受到本机总内存大小以及处理器寻址空间的限制,在配置虚拟机参数时,如果忽略了直接内存,使各个内存区域总和物理内存限制,在动态扩展时就会出现 OutOfMemoryError 异常

<h3 id="base"> 基础数据类型 </h3>

基础数据类型包括booleanbytecharshortintfloatlongdouble

<h3 id="reference"> reference类型 </h3>

A a = new A();

该类型就是常见的引用类型,它并不等同于a对象本身, 可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或与此对象相关的位置

<h3 id="returnAddress"> returnAddress类型 </h3>
该类型指向一条虚拟机指令的操作码地址,与其他类型不同的是,该类型没有对应的 Java 语言类型

<h3 id="stackDepth"> 栈深度 </h3>
可以简单的理解栈为数组,栈的深度,理解为数组的长度

<h3 id="literal"> 字面量 </h3>

字面量是指由文字所表示的取值

final int i = 100;
String s = "hello world";
// 100 和 "hello world" 都可以叫做字面量,i 是常量,s 是变量

<h3 id="SymbolicReferences"> 符号引用 </h3>

在JVM中,类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段

而解析阶段即是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用以一组符号描述所引用的对象,符号可以是任何形式的字面量,只要该组符号使用时具有唯一性并且能够无歧义定位到目标即可,符号引用与虚拟机的内存布局无关,引用的目标并不一定要加载到内存中,当一个 Java 类编译成 Class 文件时,Java 类中并不知道自己类中所引用的那些类或者接口的实际地址,因此只能使用符号引用代替,各种虚拟机内存布局可能不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式已经明确定义在 Java 虚拟机规范的 Class 文件格式中

举个例子,更直观的理解符号引用:

public interface K {
    public static String str = "abc";
    public static int i = new Random().nextInt();
}

public class Test {
    public static String i = K.str;
    public static int i = K.i;
}

使用 javac 命令把上面两个 Java 文件编译成 Class 文件,然后使用 javap 命令反编译和查看编译器编译后的字节码,加上 -verbose 参数可以输出栈大小,方法参数个数

# 进入文件所在目录
$ javac Test.java
$ javap -verbose Test
Classfile /Users/Kevin/IDEAProjects/Test/src/Test.class
  Last modified 2017-6-27; size 348 bytes
  MD5 checksum 740d6b24ac085239bccf8ed09980b232
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#20         // java/lang/Object."<init>":()V
   #2 = Class              #21            // K
   #3 = String             #22            // abc
   #4 = Fieldref           #7.#23         // Test.ss:Ljava/lang/String;
   #5 = Fieldref           #2.#24         // K.i:I
   #6 = Fieldref           #7.#25         // Test.ii:I
   #7 = Class              #26            // Test
   #8 = Class              #27            // java/lang/Object
   #9 = Utf8               ss
  #10 = Utf8               Ljava/lang/String;
  #11 = Utf8               ii
  #12 = Utf8               I
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               <clinit>
  #18 = Utf8               SourceFile
  #19 = Utf8               Test.java
  #20 = NameAndType        #13:#14        // "<init>":()V
  #21 = Utf8               K
  #22 = Utf8               abc
  #23 = NameAndType        #9:#10         // ss:Ljava/lang/String;
  #24 = NameAndType        #28:#12        // i:I
  #25 = NameAndType        #11:#12        // ii:I
  #26 = Utf8               Test
  #27 = Utf8               java/lang/Object
  #28 = Utf8               i
{
  public static java.lang.String ss;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC

  public static int ii;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #3                  // String abc
         2: putstatic     #4                  // Field ss:Ljava/lang/String;
         5: getstatic     #5                  // Field K.i:I
         8: putstatic     #6                  // Field ii:I
        11: return
      LineNumberTable:
        line 7: 0
        line 8: 5
}
SourceFile: "Test.java"

可以看到有很多信息,其中有一个 Constant pool 就是Class 文件常量池,下表列出了所有常量池标志的名字和值:

Entry Type Description
ONSTANT_Utf8 UTF-8 编码的 Unicode 字符串
CONSTANT_Integer int 字面量
CONSTANT_Float float 字面量
CONSTANT_Long long 字面量
CONSTANT_Double double 字面量
CONSTANT_Class class 或者 interface 符号引用
CONSTANT_String String 字面量
CONSTANT_Fieldref field 符号引用
CONSTANT_Methodref method 符号引用
CONSTANT_InterfaceMethodref interface 中 method 的符号引用
CONSTANT_NameAndType 方法和字段的一部分符号引用

每一个标志都有一个相对应的表,表名通过在标志后加上“_info”后缀来产生。例如,对应于CONSTANT_Class标志的表名为CONSTANT_Class_info,表名为CONSTANT_Utf8_info的表中存储着Unicode字符串的压缩形式

对应着此表再来看 javap -verbose Test.class 中的信息,看到在初始化 Test 中的 ii 和 ss 的过程:

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #3                  // String abc
         2: putstatic     #4                  // Field ss:Ljava/lang/String;
         5: getstatic     #5                  // Field K.i:I
         8: putstatic     #6                  // Field ii:I
        11: return
      LineNumberTable:
        line 7: 0
        line 8: 5

因为 K 接口中的 str 字段是在编译期就可以确定的常量值,所有当 Test 引用该常量的时候,虚拟机直接把 str 的 “abc” 直接复制了一份到到 Test 的常量池中,#3就是该值得位置,但是当初始化 Test 的 ii 变量时,从常量池#5项中取值,常量池的第5项是一个符号引用,指向K的i字段。i的值只能在运行的时候才能确定,在运行的时候会将符号引用解析为指向i内存地址的直接引用

<h3 id="AddressingSpace"> 处理器寻址空间 </h3>

数据在存储器(RAM)中存放的数据是有规律的,CPU 取数据时需要知道数据的位置,此时需要一个个数据的挨个寻找,这种行为叫做寻址,但是 CPU 的能力始终有限,超出 CPU 能力范围的数据就取不到了,CPU 寻址最大范围就是寻址空间,单位是字节

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容