JVM(Java Virtual Machine)是Java程序的运行环境,它是一个虚拟的计算机,通过解释Java字节码执行Java程序。JVM是跨平台的,可以在不同的操作系统上运行Java程序,这也是Java语言的重要特性之一。
JVM包括三个主要的组件:类加载器、执行引擎和内存管理系统。
- 类加载器用于将Java源代码编译后生成的字节码载入到JVM中;
- 执行引擎则负责解释和执行字节码,将其转化为机器码并执行;
- 内存管理系统则管理着JVM中的内存分配、垃圾回收等任务。
JVM的优势在于其高度可移植性,使得Java程序能够在各种操作系统和硬件平台上运行,并且JVM的自动垃圾回收机制也极大地降低了程序员的工作量,帮助开发者避免了许多内存泄漏和空指针异常的问题。
JVM创建对象
1.类加载器检查
虚拟机遇到一条new指令时首先检查这个指令的参数是否能在运行时常量池中定位到这个类的符号引用,检查这个类的符号引用代表的类是否已被加载、解析和初始化过。若没有则需先执行相应的类加载过程。
2.分配内存
在类加载器检查通过后,接下来虚拟机将为新对象分配内存。对象所需的内存大小在类加载完成后便可确定。分配方式有指针碰撞、空闲列表两种。
指针碰撞 :
- 适用场合 :堆内存规整(即没有内存碎片)的情况下。
- 原理 :用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
- 使用该分配方式的 GC (garbage collect)收集器:Serial, ParNew
空闲列表 :
- 适用场合 : 堆内存不规整的情况下。
- 原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
- 使用该分配方式的 GC 收集器:CMS
内存分配并发问题
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
- CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
3.初始化零值
内存分配之后虚拟机需要将分配的内存空间都初始化为对应数据类型的零值。
4.设置对象头
初始化零值之后,虚拟机需要对对象进行必要的配置。对象的所属类、类的元数据信息、对象哈希码、GC分代年龄(GC分代是指垃圾回收器将对内存分为不同的区域,GC分代年龄是指对象在堆中存活的时间,通常以垃圾回收的次数进行计算,当年龄达到某一阈值之后就需要移动到下一代区域中)等信息会存放在对象头中。
5.执行init方法
从虚拟机的角度看新的对象已经产生,但所有字段还是零值,一般之后还需要执行
对象内存信息
在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。
Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象访问
Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针。
句柄
如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
直接指针
如果使用直接指针访问,reference 中存储的直接就是对象的地址。
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
著作权归所有 原文链接:https://javaguide.cn/java/jvm/memory-area.html
Java内存区域
定义了JVM在运行时如何分区存储程序数据。Java虚拟机在执行Java程序时会将管理的内存划分为不同的数据区域。
jdk1.7之前还有一个方法区(包含运行时常量池)也在运行时数据区域中。jdk1.8后在本地内存中开辟一个元空间包含运行时常量池。
其中虚拟机栈、程序计数器、本地方法栈都是线程独有的。
堆、运行时常量池、直接内存(非运行时数据区的一部分)是线程共享的。
堆
虚拟机管理内存中最大的一块区域,可以动态扩展或者缩小。由所有线程共享,虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在堆中分配内存。
堆内存可以分为三个部分:
新生代(young generation)、老年代(old generation)、永久代(permanent generation,jdk1.7)/元空间(MetaSpace,jdk1.8)
新生代又可以细分为一个Eden区和两个Survivor区。当Java程序创建新的对象时,这些对象会被放入Eden区中。如果Eden区没有足够的空间存放这些对象,一部分对象将被转移到Survivor区中。Survivor区主要用来存放从Eden区中已经存活下来的对象,而不是被回收的对象。Survivor区有两个,一个是From区,一个是To区。当一个Survivor区被填满时,其中还活着的对象将被移动到另外一个Survivor区中,同时这个Survivor区也被清空。不断重复这个过程,直到对象被移到老年代。
老年代用于存放长期存活的对象,因为老年代的对象存活时间较长,所以回收频率比较低。大多数情况下,对象只有在持续存在很长时间,才会被移到老年代。
Tenured即老年代
程序计数器
当前线程所执行字节码的行号指示器,字节码解释器工作时通过改变计数器的值选取下一条需要执行的字节码指令。
1.实现代码的流程控制(顺序、循环、选择、异常等)。
2.多线程的情况下用于记录当前线程执行的位置,从而当线程切换时保留和恢复现场。
程序计数器是唯一一个不会出现 OutOfMemoryError
的内存区域,生命周期随着线程创建而创建、随着线程结束而结束。
虚拟机栈
线程私有,生命周期与线程相同。除了native之外的所有方法调用都需要通过虚拟机栈来实现。方法调用的数据需要通过栈传递,每次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后都会有一个栈帧被弹出。
栈帧中存储了局部变量表、操作数栈、动态链接、方法返回地址等信息。
![]()
局部变量表
存放编译器可知的各类型数据(byte、boolean、char、short、int、float、long、double)、对象引用(reference类型,对象实例本身存储在堆内存之中,对象的引用变量存储在局部变量表中)操作数栈
方法运行过程中产生的中间计算结果以及计算过程中产生的临时变量。动态链接
当执行一个方法时,如果该方法需要调用其他方法,则会将需要调用的方法的引用添加到栈帧中的动态链接中。如果函数调用陷入无限循环的话就会导致stackOverFlowError。
简单总结一下程序运行中栈可能会出现两种错误:
StackOverFlowError
: 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出StackOverFlowError
错误。OutOfMemoryError
: 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
异常。
本地方法栈
与虚拟机栈相似,区别是虚拟机栈为执行java方法(字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,用于存放本地方法的局部变量表、动态链接、操作数栈、出口信息。
方法区
方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。不同虚拟机上方法区的实现有所不同。
方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。
在Java 8及之前的版本中,方法区(永久代)是Java虚拟机规范中定义的一块内存区域,用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据。而在Java 8及之后的版本中,由于方法区容易造成内存泄漏和溢出等问题,因此被元空间所取代。
元空间是Java 8及之后版本中取代方法区的新概念,它同样也是一块内存区域,用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据。与方法区不同的是,元空间并不位于Java虚拟机堆内存之内,而是使用本地内存(Native Memory)来实现。
另外,元空间相对于方法区来说,有以下几个优点:
- 元空间采用本地内存实现,而方法区则是Java虚拟机堆内存的一部分。这意味着元空间的访问速度更快,因为它不需要通过Java虚拟机堆内存来进行访问。
- 元空间可以使用更大的物理内存来存储类信息等数据,而方法区则受到Java虚拟机堆内存大小的限制。这使得元空间能够更好地支持大型应用程序,元空间能更好地应对内存溢出和内存泄露的问题。
总之,元空间是Java 8及之后版本中取代方法区的新概念,采用本地内存实现,并且具有动态调整大小、访问速度快和支持大型应用程序等优点。
运行时常量池
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table) 。
字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。常见的符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。
常量池表会在类加载后存放到方法区的运行时常量池中。
字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。通常情况下
直接内存
特殊的内存缓冲区,并不在Java堆或者方法区中分配,通过JNI的方式在本地内存中分配。直接内存被用于处理大量的数据,比如图像和视频数据。
JVM调优
JVM 调优是指对 Java 虚拟机(JVM)的配置进行优化,以提高应用程序的性能和稳定性。JVM调优主要设计三个指标,分别是:内存占用量、系统延迟、系统吞吐量。
在进行 JVM 调优时,需要考虑以下几个方面:
Java内存模型
Java内存模型(Java Memory Model,简称JMM)是一套规范,用于定义Java虚拟机(JVM)中多线程程序访问共享内存时的行为和规则。
原因
一般的编程语言会直接使用操作系统层面的内存模型,Java是最早提供内存模型的编程语言,因为Java语言是跨平台的,它需要提供自己的内存模型以屏蔽系统的差异。
同时JMM也是一套Java并发编程规范,抽象了线程和主内存之间关系,并且规定了从Java源代码到CPU可执行指令这个转化过程需要遵守的并发规则,简化了多线程编程,增强了程序的可移植性。
内存
主存
所有线程创建的实例对象都存放在主内存中【包括成员变量、方法中的局部变量等】
本地内存
JMM抽象出的一个概念,每个线程私有的存储空间,用来存储共享变量的副本,每个线程只能访问自己的本地内存。
线程之间进行通信需要经过主内存同步【如:线程1将本地内存中修改的变量值同步到主存中,线程2再从主存中读取对应的值】,JMM给变量提供了可见性的保障。
happens-before 原则
Java 规范定义了一组 happens-before 规则,用于描述并发系统中事件之间的顺序关系,用于确保程序中的多线程操作能够正确地执行。该原则指出,如果事件 A 发生在事件 B 之前,那么 A 就 “happens-before” B,这确保了A的执行结果对B是可见的。例如,如果一个写操作 “happens-before” 一个读操作,那么在读操作中就可以看到写操作所做的修改。