程序计数器
也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的的字节码指令的地址。
虚拟机栈
栈帧:先进后出
局部变量表
局部变量表的作用是在运行过程中存放所有的局部变量
操作数栈
操作数栈是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域
帧数据
帧数据主要包含动态链接、方法出口、异常表的引用
动态链接
当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。
方法出口
方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址。
异常表
异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。
栈内存
默认大小:2m
设置大小:
- 语法:-Xss栈大小
- 单位:字节(默认,必须是 1024 的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
-Xss1048576
-Xss1024K
-Xss1m
-Xss1g
数据类型
Java中的8大数据类型在虚拟机中的实现:
栈中的数据要保存到堆上或者从堆中加载到栈上时怎么处理
堆中的数据加载到栈上
由于栈上的空间大于或者等于堆上的空间,所以直接处理但是需要注意下符号位。
boolean、char为无符号,低位复制,高位补0
byte、short为有符号,低位复制,高位非负则补0,负则补1
栈中的数据要保存到堆上
byte、char、short由于堆上存储空间较小,需要将高位去掉。boolean比较特殊,只取低位的最后一位保存。
本地方法栈
功能与服务对象:
- 虚拟机栈(Virtual Machine Stack):主要是为执行Java方法服务,是JVM执行Java方法时的工作区域,每个线程都拥有自己独立的虚拟机栈。每当一个Java方法被调用时,都会为该方法创建一个栈帧(Stack Frame),用于存储局部变量、操作数栈、动态链接和方法出口等信息。
- 本地方法栈(Native Method Stack):与虚拟机栈相似,但其服务的是Native方法(非Java编写的方法,如C/C++编写的方法)。当线程调用Native方法时,会进入本地方法栈。需要注意的是,本地方法栈并不是Java虚拟机规范的一部分,而是由具体实现来决定的。
内存分配与共享:
- 虚拟机栈:每个线程在创建时都会创建一个虚拟机栈,它是线程私有的,即不能实现线程间的共享。每个线程都只能访问自己的虚拟机栈中的栈帧。
- 本地方法栈:同样地,本地方法栈也是线程私有的,与虚拟机栈类似,不同的线程在本地方法栈中不会有数据的共享。
异常处理:
- 当栈空间不足以存储新创建的栈帧时,或者尝试访问栈以外的内存时,两种栈都会抛出
StackOverflowError
异常。 - 如果虚拟机栈或本地方法栈的扩展无法满足内存分配需求时,将会抛出
OutOfMemoryError
异常。
实现与合并:
- 在某些JVM实现中(如HotSpot VM),本地方法栈与虚拟机栈是合并实现的,这意味着Java方法和Native方法都使用同一个栈。但需要注意的是,这种合并实现并不改变两者在逻辑上的区别。
性能与特点:
- 虚拟机栈和本地方法栈都是由系统自动分配和释放的,它们的存储速度都相对较快,因为栈是机器系统提供的数据结构,计算机会在底层对栈提供分配和释放的支持。
综上所述,虚拟机栈和本地方法栈的主要区别在于它们服务的方法和内存管理的细节上。虚拟机栈服务于Java方法,而本地方法栈服务于Native方法;两者都是线程私有的,不能共享;在异常处理和内存分配上,两者也有各自的特点。在某些JVM实现中,两者可能合并实现,但逻辑上仍然保持独立。
堆
一般Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上。栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。
堆内存的溢出
通过new关键字不停创建对象,放入集合中,模拟堆内存的溢出,观察堆溢出之后的异常信息。
三个重要的值
堆空间有三个需要关注的值,used、total、max。used指的是当前已使用的堆内存,total是java虚拟机已经分配的可用堆内存,max是java虚拟机可以分配的最大堆内存。
要修改堆的大小,可以使用虚拟机参数 –Xmx(max最大值)和-Xms (初始的total)。
语法:-Xmx值 -Xms值
单位:字节(默认,必须是 1024 的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
限制:Xmx必须大于 2 MB,Xms必须大于1MB
-Xms6291456
-Xms6144k
-Xms6m
-Xmx83886080
-Xmx81920k
-Xmx80m
建议:
Java服务端程序开发时,建议将-Xmx和-Xms设置为相同的值,这样在程序启动之后可使用的总内存就是最大内存,而无需向java虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况。-Xmx具体设置的值与实际的应用程序运行环境有关,在《实战篇》中会给出设置方案。
对象
对象头(Header):
- Mark Word:
- 锁信息:包括锁状态(如未锁定状态、偏向锁状态、轻量级锁状态、重量级锁状态)和锁标志(如是否启用偏向锁、是否启用自旋等)。
- 对象的哈希码:在某些JVM实现中,哈希码存储在Mark Word中。
- GC标志:用于标记对象的垃圾回收状态,如对象是否被标记为垃圾,是否经过了写屏障等。
- 类型指针(Klass Pointer):指向对象的类元数据的地址,即对象的类型信息。
实例数据(Instance Data):
- 实例数据存放一个对象中的其他信息,即类中声明的成员变量(包括从父类继承的和本类定义的)。
对齐填充(Padding):
- 用于保证对象占用内存空间的整数倍,以达到空间存储的优化。对齐填充并不是JVM对象内容必须的部分,而是为了保证JVM空间管理上的性能。
锁状态
- 无锁状态(Unlocked)
- 对象的初始状态,没有任何线程持有该对象的锁。
- 在Mark Word中,偏向锁位与锁标志位合起来表示为“001”。
- 偏向锁状态(Biased Locking)
- 当一个线程首次访问某个同步代码块并获取锁时,JVM会尝试将锁偏向给这个线程,以减少CAS(Compare-And-Swap)操作,从而提高性能。
- 在Mark Word中,偏向锁位与锁标志位合起来表示为“101”。
- 如果后续没有其他线程竞争该锁,持有偏向锁的线程将一直执行,无需再次进行同步判断。
- 轻量级锁状态(Lightweight Locking)
- 当有另外一个线程参与锁竞争时,偏向锁会升级为轻量级锁。
- 轻量级锁通过自旋(Spinning)的方式尝试获取锁,避免线程挂起和恢复的开销。
- 如果自旋成功,则获得锁;如果自旋失败,则升级为重量级锁。
- 重量级锁状态(Heavyweight Locking)
- 当多个线程同时竞争锁时,轻量级锁会升级为重量级锁。
- 重量级锁通过让未获得锁的线程阻塞(Blocking)来等待锁的释放,从而保证线程安全。
- 重量级锁的开销较大,因为它涉及线程的挂起和恢复操作。
锁标志
- 是否启用偏向锁(Biased Locking Enabled)
- JVM的一个配置选项,用于决定是否启用偏向锁。
- 如果启用,则当线程首次访问某个同步代码块时,会尝试将锁偏向给该线程。
- 是否启用自旋(Spinning Enabled)
- 与轻量级锁相关的一个配置选项,用于决定是否启用自旋机制。
- 如果启用,则当线程尝试获取轻量级锁失败时,会进行自旋操作,等待锁的释放。
- 自旋的次数和持续时间可以通过JVM参数进行配置。
元数据的指针
Klass pointer元数据的指针指向方法区中保存的InstanceKlass对象:
指针压缩
JVM(Java Virtual Machine)的指针压缩(CompressedOops)技术主要用于64位JVM环境中,以优化内存使用和垃圾收集(GC)性能。在64位JVM中,每个native指针通常占用8个字节,这会导致内存占用增加和GC停顿时间变长。为了解决这个问题,JVM引入了指针压缩技术。
指针压缩的原理是利用Java对象通常是对齐的这一特点。由于对象对齐,大多数对象的偏移量是可以预测的。因此,JVM可以使用对象的偏移量来计算对象的地址,而不必使用完整的指针。在64位JVM中,压缩指针可以将64位指针压缩为32位,这意味着每个指针只需要占用4字节的内存空间。这样,JVM可以使用较小的指针来定位对象,从而节省了堆内存的使用量。
具体来说,当堆内存小于32GB时,压缩指针是有效的。在压缩过程中,JVM将堆的基地址和对象的偏移量分开存储。偏移量被除以8后保存到32位地址中,而基地址则保存在JVM的内部数据结构中。当需要访问对象时,JVM使用基地址和32位偏移量来重新计算完整的64位地址。
启用指针压缩后,JVM会压缩以下类型的指针:
- 每个Class的属性指针(静态成员变量)
- 每个对象的属性指针
- 普通对象数组的每个元素指针
然而,压缩指针并不是万能的。对于某些特殊类型的指针,如指向PermGen的Class对象指针、本地变量、堆栈元素、入参、返回值和NULL指针,JVM不会进行优化。
需要注意的是,当堆内存大于32GB时,压缩指针会失效,JVM将强制使用64位指针来寻址Java对象。此外,在64位平台的HotSpot JVM中使用32位指针(实际存储用64位)时,内存使用会多出1.5倍左右。这是因为较大的指针在主内存和缓存之间移动数据时需要占用更多的带宽,同时GC也会承受更大的压力。因此,在配置JVM堆内存时,应谨慎选择以避免过大的堆内存导致性能下降。
32G原因
- 寻址空间:当堆内存达到32GB(即2的35次方字节,因为JVM的堆内存通常是以8字节对齐的)时,32位的压缩指针就无法再有效地寻址整个堆内存空间了。32位指针的最大寻址空间是2的32次方字节,即4GB。因此,当堆内存超过这个范围时,JVM就需要使用完整的64位指针来确保能够访问堆中的每一个对象。
- 内存对齐:Java对象在内存中通常是按照8字节对齐的。这意味着对象在内存中的起始地址是8的倍数。在32位指针压缩的情况下,JVM通过一定的算法和技巧来管理和使用这些地址。但是,当堆内存超过32GB时,这些算法和技巧就不再适用,因为32位指针无法直接表示所有的对齐地址。
总结
Java中的锁状态和锁标志是JVM为了提高并发性能和降低锁竞争开销而引入的机制。通过合理地配置和使用这些机制,可以优化多线程应用程序的性能。需要注意的是,这些机制的实现细节可能会因JVM版本和配置的不同而有所差异。
方法区
类的元信息,保存了所有类的基本信息
运行时常量池,保存了字节码文件中的常量池内容
字符串常量池,保存了字符串常量
直接内存
在 JDK 1.4 中引入了 NIO 机制,使用了直接内存,主要为了解决以下两个问题:
1、Java堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用。
2、IO操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到Java堆中。
现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少了数据复制的开销。写文件也是类似的思路。
使用堆创建对象的过程:
使用直接内存创建对象的过程,不需要进行复制对象,数据直接存放在直接内存中:
使用方法:
要创建直接内存上的数据,可以使用ByteBuffer
。
语法: ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);
注意事项: arthas的memory命令可以查看直接内存大小,属性名direct。
设置内存
将 直接内存大小设置为 1024 KB:
-XX:MaxDirectMemorySize=1m
-XX:MaxDirectMemorySize=1024k
-XX:MaxDirectMemorySize=1048576
不同版本区别
JDK8之后
静态变量移到元空间
字符串常量池仍在堆区