JVM之内存结构
从整体上看JVM的内存分为两大类:线程私有的和线程共享的。
线程私有:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享:
- 堆区
- 方法区
程序计数器
主要作用就是记住下一条JVM指令的执行地址。因为在多线程的情况下,同一个时间单核CPU只会执行一个线程中的方法,也就是说CPU会不断切换执行的线程,那么线程会不断的中断和恢复,这就要求线程在中断前就要记住当前的运行状态,从而下一次恢复时能继续向下执行。
虚拟机栈
其实就是我们通常理解的栈,而栈内又存放这一个一个栈帧,每一次的方法调用都会有一个新的栈帧入栈。
每一个栈帧主要包含四个方面:
- 局部变量表
- 主要用于存储方法参数和方法内的局部变量,局部变量又包含:Java基本的数据类型、对象的引用
- 局部变量表所需要的大小是在编译期内确定的
- 由于局部变量只在当前变量中有效,所以不存在线程安全的问题
- 局部变量表最基本的存储单元是Slot( [1] 为什么很多的地方都有最基本的存储单元,他的好处是什么?),32位以内的类型只占用一个Slot,64位的类型(long和double)占用两个连续的Slot,byte、short、char在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。
为什么byte、short、char在存储前被转换为int?
有一种说法是:因为JVM指令中的操作码只占一个字节,也就是说最多只有256个不同的操作码,如果给每一个数据类型都定义一个不同的操作码的话会不够用。比如:iadd,将栈中两个int类型相加,但是并没有针对short类型的“sadd”。【2】 - 阿萨德
- 操作数栈
- 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
-
int a = 5; int b = 10; int result = a + b; // 对应字节码 0: iconst_5 // Push the value 5 onto the stack 1: istore_1 // Store the value into local variable 'a' 2: bipush 10 // Push the value 10 onto the stack 4: istore_2 // Store the value into local variable 'b' 5: iload_1 // Push the value of 'a' onto the stack 6: iload_2 // Push the value of 'b' onto the stack 7: iadd // Pop the top two values, add them, and push the result 8: istore_3 // Store the result into local variable 'result'
- 动态链接
- 动态链接是指向运行时常量池的方法引用
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在Class文件的常量池( [2] Class文件中都包含什么?)中。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
- 静态链接 or 动态链接,早期绑定 or 晚期绑定
- 虚方法和非虚方法:如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法,比如静态方法、私有方法、final 方法、实例构造器、父类方法都是非虚方法,其他方法称为虚方法。
- 虚方法表:在面向对象编程中,会频繁的使用到动态分派,如果每次动态分派都要重新在类的方法元数据中搜索合适的目标有可能会影响到执行效率。为了提高性能,JVM 采用在类的方法区建立一个虚方法表,使用索引表来代替查找。非虚方法不会出现在表中。每个类中都有一个虚方法表,表中存放着各个方法的实际入口。虚方法表会在类加载的连接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕。
- 方法返回地址
- 用来存放调用该方法的PC寄存器的值。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息。
本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用
堆区
为了加快GC的速度,JVM将堆区逻辑上分为了三类:
- 新生代:伊甸园区+两个幸存区,HotSpot虚拟机默认为:8:1:1
- 老年代
- 元空间:像一些方法中的操作临时对象等,JDK1.8之前是占用JVM内存,JDK1.8 后直接使用物理内存。不管是JDK8之前的永久代,还是JDK8及以后的元空间,都可以看作是Java虚拟机规范中方法区的实现。
暂时不管元空间,为什么分成新生代和老年代就可以加快GC的速度?为什么分为两代,而不是三代、四代?
这是因为,宏观的视角来看可以将对象根据存活时间分为两大类,一类是“招生夕灭”的,一类是“难以消亡”的。因此如果将对象根据存活时间放到内存中不同的区域,这样新生代可以以较高的频率来进行GC,而老年代可以以较低的频率来进行GC,如果不分的话每一次GC就需要遍历所有的对象,效率肯定是不如分代的。至于为什么是分两代,我觉得是因为他只通过一个维度来进行区分,如果需要分三代,应该再有一个类似于“存活时间”的区分条件。
对象的内存分配
- 创建一个对象时,对象优先分配到Eden区(先不考虑Eden一开始就放不进去)
- 当Eden空间不足时,会发生一次Minor GC
- JVM会把存活的对象转移到Survivor from中,并且对象年龄+1(先不考虑Survivor放不进去)( [3] 对象头是怎么样的?)
- 当Eden再次空间不足时,有会发生一次Minor GC
- Eden、Survivor from转移到Survivor to中,并且对象年龄+1,from和to的引用互换
- 一直这样下去,转移时发现某个对象的年龄超过:PetenureSizeThreshold(默认15),对象会直接被分配到老年代
- 并不是只有年龄到了PetenureSizeThreshold才会分配到老年代中。如果在Survivor空间中相同年龄所有对象大小的总和大于
Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
- 并不是只有年龄到了PetenureSizeThreshold才会分配到老年代中。如果在Survivor空间中相同年龄所有对象大小的总和大于
- 当老年代空间不足时,会发生Major GC(只有CMS GC会有单独收集老年代的行为)
- 还有full GC(收集整个Java堆和方法区的垃圾),当一个系统频繁发生full GC就需要排查问题了
JDK1.8默认垃圾回收器:Parallel Scavenge + serial Old(PS MarkSweep)
- 大对象会直接进入老年代,目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。一次上面的两处先不考虑的地方是通过老年代兜底的。
方法区
方法区中都有哪些内容?
- [heap] 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放。运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的是 String.intern()方法。受方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
- [本地内存] 存储类信息
- [heap] 静态变量
- JIT编译后的代码等数据
永久代 vs 元空间
方法区(method area)只是JVM规范中定义的一个概念,不同的厂商有不同的实现。而永久代(PermGen)是 Hotspot 虚拟机特有的概念, JDK8 的时候又被元空间取代了,永久代和元空间都可以理解为方法区的落地实现。
- 永久代物理是堆的一部分,和新生代,老年代地址是连续的(受垃圾回收器管理),而元空间存在于本地内存(我们常说的堆外内存,不受垃圾回收器管理),这样就不受JVM限制了,也比较难发生OOM(都会有溢出异常)
- 存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中
上面的问题
- 为什么很多的地方都有最基本的存储单元,他的好处是什么?
- String字符问题...【4】
- DDIA 第三章 散列索引
- Class文件中都包含什么?【5】
- 类信息:描述类的信息,包括类的名字、修饰符(public或private)、父类、接口以及注解(annotation)
- 变量信息:描述类变量的信息,包括每个变量的名字、修饰符、类型和注解(annotation)
- 方法信息:描述类方法的信息,包括每个方法的名字、修饰符、参数的类型和返回值的类型、方法的注解(annotation),还包括编译后的、字节码形式的方法代码
- 常量池:常量池是一个数组,包括数字、字符和类型的常量。这些信息通过java提供的Class类能够获取,并通过反射类使用。也是动态编译和java逆向工程的基础(也就是文中的常量池)
- 对象头是怎么样的?
JVM中对象头的方式有以下两种(以32位JVM为例):【6】-
普通对象
|--------------------------------------------------------------| | Object Header (64 bits) | |------------------------------------|-------------------------| | Mark Word (32 bits) | Klass Word (32 bits) | |------------------------------------|-------------------------|
-
数组对象
|---------------------------------------------------------------------------------| | Object Header (96 bits) | |--------------------------------|-----------------------|------------------------| | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | |--------------------------------|-----------------------|------------------------|
-
mark word
这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。
为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:|-------------------------------------------------------|--------------------| | Mark Word (32 bits) | State | |-------------------------------------------------------|--------------------| | identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal | |-------------------------------------------------------|--------------------| | thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased | |-------------------------------------------------------|--------------------| | ptr_to_lock_record:30 | lock:2 | Lightweight Locked | |-------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked | |-------------------------------------------------------|--------------------| | | lock:2 | Marked for GC | |-------------------------------------------------------|--------------------|
-
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
biased_lock lock 状态 0 01 无锁 1 01 偏向锁 0 00 轻量级锁 0 10 重量级锁 0 11 GC标记 -
biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
-
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
-
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
-
thread:持有偏向锁的线程ID。
-
epoch:偏向时间戳。
-
ptr_to_lock_record:指向栈中锁记录的指针。
-
ptr_to_heavyweight_monitor:指向管程Monitor的指针。
64位:
|------------------------------------------------------------------------------|--------------------| | Mark Word (64 bits) | State | |------------------------------------------------------------------------------|--------------------| | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal | |------------------------------------------------------------------------------|--------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased | |------------------------------------------------------------------------------|--------------------| | ptr_to_lock_record:62 | lock:2 | Lightweight Locked | |------------------------------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked | |------------------------------------------------------------------------------|--------------------| | | lock:2 | Marked for GC | |------------------------------------------------------------------------------|--------------------|
-
-
class pointer
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:- 每个Class的属性指针(即静态变量)
- 每个对象的属性指针(即对象变量)
- 普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
-
array length
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。
-
===============================
仅作为校招时的《个人笔记》,详细内容请看【参考】部分
===============================
参考
- https://www.pdai.tech/ JVM内存结构部分
- https://cloud.tencent.com/developer/news/735870
- 《深入理解Java虚拟机》
- https://www.zhihu.com/question/279539793
- https://blog.csdn.net/weixin_31090403/article/details/114125322
- https://www.jianshu.com/p/3d38cba67f8b