JVM 概述

Java Virtual Machine 是Java平台的基石,包含相应的技术规范、实现(JVM的实现就是JRE)、运行实例
为了实现一次编写到处运行 (Write Once Run Anywhere)的理念,JVM是和平台相关的

Java HotSpot Client VM (client VM) :主要用于减少启动时间和内存占用,启动应用时使用-client指定
Java HotSpot Server VM (server VM):用于最大化的执行速度,启动应用时使用-server指定

JVM主要包括三个子系统

  • 类加载子系统 Class Loader
  • 运行时数据区 Runtime Data Area
  • 执行引擎 Execution Engine
JVM构架

运行时数据区

JVM定义了不同的运行时数据区,一些是在JVM启动时就创建的,有些数据区的生命周期和线程有关,主要有5种

程序计数器 Progarm Counter Registers

JVM支持许多线程同时运行,每个JVM线程都有自己的pc寄存器
当线程运行的是Java方法时,存储当前正在执行的JVM指令地址
如果是Native方法,未定义

Java虚拟机栈 Java Virtual Machine Stacks

每个线程都有一个私有的Java虚拟机栈,和线程的生命周期相同。描述了Java方法执行的内存模型
用来存储栈帧(Frame)

Frame
每个方法在调用的时候,都会创建一个栈帧,包含局部变量(Local Variables)、操作数栈(Operand Stacks)、动态链接(Dynamic Linking)、方法的调用者信息(Normal/Abrupt Method Invocation Completion)
局部变量
存储boolen,byte,char,short,int,long,float,double,reference(引用),returnAddress(指向一条操作码)
局部变量的数组长度是在编译时确定的,之后不再改变
同样用于方法调用中的参数传递,0号索引用于存储this
操作数栈
深度由编译时确定
加载常量,局部变量或者字段到操作数栈,出栈计算出结果,压栈
还可以用来准备传递到函数中的参数,接收方法的返回值

和常见的语言(例如C)的栈类似:存储局部变量,局部结果,用于方法调用和返回等

Exception
StackOverflowError:线程请求的栈深度大于JVM的的允许范围(函数调用层级过多导致)
OutOfMemoryError:内存不足,无法动态扩展内存时

-Xss<size>:设定线程栈的大小 (默认单位是byte,支持 K/k M/m G/g 作为单位)

本地方法栈 Native Method Stack

JVM使用到Native方法的时候使用本地方法栈
每一个线程,将创建一个单独的本地方法栈。

和Java虚拟机栈的异常相同

堆Heap

所有的JVM线程共享,所有的类对象和数组都要在堆上分配
堆是虚拟机所管理的内存中最大的一块,也是Garbage Collector管理的主要区域
Java堆的内部可以不连续,堆空间大小可以是固定的,也可以是可扩展收缩的

Exception:
OutOfMemoryError: 请求更多的堆空间无法满足

-Xms<size>:初始堆大小
-Xmx<size>:最大堆大小

方法区 Method Area

被所有Java线程共享,存储被虚拟机加载的类信息,例如运行时常量池,字段和方法数据,方法的代码,构造函数等,JVM规范没有限定方法区的位置,可以是固定大小或者可扩展,内存不要求连续

Hotspot虚拟机中的方法区之前也叫做永久代,PermGen,Java8 后叫做元空间,Matespace,分配在本地内存Native Memory中,它的大小可以自动的增长,可以通过XX:MaxPermSize-XX:MaxMetaspaceSize=<size>设定大小
因为是在本地内存(native memory)分配,所以其最大可利用空间是整个系统内存的可用空间,不容易遇到OutOfMemoryError错误

运行时常量池 Runtime Constant Pool

是方法区的一部分,类似于常规语言的符号表
类或者接口的.class文件中,有一个常量表属性(constant_pool table),存放各种字面常量(在编译时确认)和字段引用(在运行时确认),加载到虚拟机时,对应加载到运行时常量池

Exception:
OutOfMemoryError: 方法区无法满足内存分配需求

堆外内存

Native Memory或者Off-Heap内存空间,例如NIO中的DirectBuffer就是使用native函数直接分配的堆外内存,可以通过-XX:MaxDirectMemorySize来设置NIO直接缓冲区的最大值。

使用堆外内存的好处:

  • 可以扩展更大的内存空间
  • 能减少GC时间
  • 可以在进程间共享数据

Java堆中对象的分配、布局、访问

对象的创建

通过new指令创建对象时,虚拟机的执行流程

  1. 类加载
  2. 为对象分配内存(大小是确定的,在加载后便已经确定),两种分配策略
  • 指针碰撞(Bump the Pointer)内存是规整的,一边是用过的,一边是空闲的,中间一个指针
  • 空闲列表(Free List),记录哪些内存可用,查找到一个足够大的可用的内存空间划分给对象,并更新列表上的记录
    具体的策略取决于GC算法,具有整理功能(Compact)使用碰撞指针,否则使用空闲列表,默认在新生代使用-XX:+UseTLAB
    并发分配内存时,对分配内存空间的动作进行同步处理(基于CAS,Compare and Swap),或者采用本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)策略,每个线程在Java堆中预先分配一块内存,在该线程的内存块内进一步分配,只有TLAB用完并分配新的TLAB时才需要同步锁定
  1. 内存空间初始化为零值
  2. 对象头(Object Header)信息设置
  3. 执行init方法,即构造方法

对象的内存布局

对象头(Header)

HotSpot虚拟机的对象头(Object Header)包括两部分信息

  • 存储对象自身的运行时数据, 如哈希码(identity_hashcode,对象的标识)、GC分代年龄、偏向锁标志、锁状态标志、偏向线程ID、偏向时间戳、指向栈中锁记录的指针、指向互斥量(重量级锁)的指针等,这部分数据的长度在32位和64位的虚拟机中分别为32个和64个Bits,官方称它为“Mark Word”,hotspot内部对应markOop.hpp,其中oop全拼是ordinary object pointer,表示指向一个对象的指针,这里的mark不是一个指针,而是一个word
  • 类型指针(指向方法区的对象类型数据),用于判定该对象是哪个类的实例

如果是数组类型,还会有4bytes(32bits)用于记录数组的长度

64位JVM,未进行指针压缩的对象头结构:

|------------------------------------------------------------------------------------------------------------|--------------------|
|                                            Object Header (128 bits)                                        |        State       |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
|                                  Mark Word (64 bits)                         |    Klass Word (64 bits)     |                    |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |    OOP to metadata object   |       Normal       |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |    OOP to metadata object   |       Biased       |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 |    OOP to metadata object   | Lightweight Locked |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 |    OOP to metadata object   | Heavyweight Locked |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
|                                                                     | lock:2 |    OOP to metadata object   |    Marked for GC   |
|------------------------------------------------------------------------------|-----------------------------|--------------------|

32位

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

实例数据(Instance Data)

存储父类子类的数据,存储顺序与虚拟机字段分配策略和源码中的定义顺序有关,相同宽度的字段会被分配到一起,HotSpot默认策略是从长到短排列,引用排最后: long/double --> int/float --> short/char --> byte/boolean --> Reference

对齐填充(Padding)

占位符,默认情况下HotSpot VM对象的起始地址是8字节的整数倍

查看内存布局

可以使用openJDK中的JOL (Java Object Layout) 查看内存布局

$ java -version
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)

$ java -jar jol-cli-0.9-full.jar  internals java.lang.Object
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

Instantiated the sample instance via default constructor.

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,mark work为前两行,占8字节,kclass pointer占4字节(这里为64位虚拟机,使用了指针压缩),对齐消耗4字节

指针压缩

对应的JVM选项是-XX:+UseCompressedOops默认开启,使用压缩指针时,对象引用实际表示的是32位偏移而不是64位指针,可以提高性能(通常64位JVM消耗的内存会比32位的大1.5倍)

压缩oops表示指向64位Java堆基地址的32位对象偏移量。因为它们是对象偏移而不是字节偏移,所以它们可以用于处理4G个对象(不是字节),HotSpot VM默认是进行8字节对齐,地址低3位始终为0,此时可以处理32GB大小的堆, 使用过程中,JVM将它们放大8倍并加上Java堆基址以查找它们引用的对象。

当Java堆大小大于32G时,也可以使用压缩指针,通过-XX:ObjectAlignmentInBytes=alignment选项设定对齐字节数目,必须是2的幂次,范围[8,256],默认为8,可以管理的堆空间大小为:

4GB * ObjectAlignmentInBytes

开启指针压缩时,堆中的以下oop会被压缩:

  • 每个对象的klass字段
  • 每个oop实例字段
  • oop数组的每个元素(objArray)

对象的访问定位

通过栈上的reference数据指向堆上的具体数据,有两种实现方式:

  • 直接指针访问:reference中存储的直接就是对象地址,访问速度更快,HotSpot使用该方式,它需要虚拟机明确知道某个位置是什么类型的数据,这样才能在对象移动后改变Reference类型的内容,HotSpot的JIT编译器会生成OopMap符号信息来记录栈上和寄存器上的引用对象位置,用来在安全点进行GC
  • 句柄访问:reference中存储的是稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针,reference本身不需要修改

锁状态

根据Java对象头,锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级,但不能降级


偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,所以当一个线程访问一个同步块并获取锁时,会通过CAS操作在对象头里存储偏向的线程ID,后续使用时只需要测试对象头的threadID,当其他线程试图获得偏向锁CAS失败时,持有偏向锁的线程会先暂停,恢复为无锁状态,或者转换为轻量级锁

这里threadID 会覆盖MarkWord中原有的identity hashcode,如果一个已经偏向的对象调用object.identityHashCode()将会触发偏向锁的撤销

epoch字段占2位,起到时间戳的作用

轻量级锁

轻量级是相对使用操作系统互斥量来实现的重量级锁而言的,在执行同步代码块之前,JVM会先在当前线程的栈帧中创建一个锁记录(Lock Record)的空间,用于存储锁对象目前的MarkWord拷贝,该拷贝叫做Displaced Mark Word

然后使用CAS操作将对象头中的MarkWord替换为指向锁记录的指针,如果失败了,表示其他线程已经获得了锁,当前线程通过自旋等待,如果自旋失败,锁膨胀为重量级锁,修改Mark Word,当前线程阻塞,等待持有锁的线程释放锁并唤醒该线程

重量级锁

锁的最终形态,标识位为10,其中指针指向的是monitor对象(也称为管程监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,当一个 monitor 被某个线程持有后,它便处于锁定状态。在JVM中,monitor是由ObjectMonitor实现的,其主要功能如下

优化

  • 自旋锁:当线程申请锁时,锁被占用,则让当前线程执行一个忙循环(自旋),看看持有锁的线程是否会很快释放锁。如果自旋后还没获得锁,才进入同步阻塞状态;
  • 自适应自旋:时间根据之前的情况进行调整
  • 锁消除:根据逃逸分析,消除锁
  • 锁膨胀: 如果虚拟机探测到一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(膨胀)到整个操作序列的外部
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容

  • JVM内存模型Java虚拟机(Java Virtual Machine=JVM)的内存空间分为五个部分,分别是: ...
    光剑书架上的书阅读 2,493评论 2 26
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,221评论 11 349
  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,697评论 0 11
  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,575评论 3 83
  • 其实写诗这事儿,是跟总在蓝酒吧喝酒的阿恒和小吹学的,阿恒其实比我大三十岁,两年前老伴走了,他戴着棕色的近视眼镜,略...
    关馨仁阅读 237评论 0 0