JVM
虚拟机的位置
JVM
是运行在操作系统之上的,它与硬件没有直接的交互
JVM
虚拟机的体系结构
程序计数器
占用内存空间小,属于线程私有的
字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成
- 如果线程正在执行一个
Java
方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址 - 如果正在执行的是
Native
方法,这个计数器的值则为 (Undefined
)。此内存区域是唯一一个在Java
虚拟机规范中没有规定任何OutOfMemoryError
情况的区域
虚拟机栈
属于线程私有,它的生命周期和线程同步一致,即它是在线程创建时创建,它的生命周期是跟随线程的生命周期的,线程结束虚拟机栈内存也就释放。对于虚拟机栈来说,它不存在垃圾回收问题,只要线程结束了,虚拟机栈也就释放
描述的是 Java
方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame
)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程
虚拟机栈遵循:先进后出,后进先出的原则
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double
)、对象引用(reference
类型)和 returnAddress
类型(指向了一条字节码指令的地址)
StackOverflowError
:线程请求的栈深度大于虚拟机所允许的深度
OutOfMemoryError
:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存
本地方法栈
区别于 Java
虚拟机栈的是,Java
虚拟机栈是为虚拟机执行 Java
方法(也就是字节码)而服务的,而本地方法栈则为虚拟机使用到的 Native
方法服务。也会有 StackOverflowError
和 OutOfMemoryError
异常
堆
属于线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB
)。可以位于物理上不连续的空间,但是逻辑上要连续
对于 Java
应用程序来说,这块区域是 JVM
所管理的内存中最大的一块
OutOfMemoryError
:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常
方法区
属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
运行时常量池
属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String
的 intern()
)都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError
每个区域存储的内容
HotSpot
虚拟机的对象
对象的创建
- 遇到
new
指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载 - 类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)
- 内存空间分配完成后会初始化为
0
(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC
分代年龄等信息存入对象头 - 执行
new
指令后执行init
方法后才算一份真正可用的对象创建完成
对象的访问定位
使用对象时,通过栈上的 reference
(引用类型)数据来操作堆上的具体对象
使用句柄访问
Java
堆中会分配一块内存作为句柄池。reference
存储的是句柄地址
使用直接指针访问( HotSpot
虚拟机所采用)
reference
中直接存储对象地址
二者比较
- 使用句柄的最大好处是
reference
中存储的是稳定的句柄地址,在对象移动(GC
)是只改变实例数据指针地址,reference
自身不需要修改 - 直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销
- 如果是对象频繁
GC
那么句柄方法好,如果是对象频繁访问则直接指针访问好 - 对于虚拟机
HotSpot
而言,它是采用直接指针访问的方式进行对象访问
虚拟机的类加载机制
类加载子系统的作用
- 类加载器子系统负责从文件系统或者网络中加载
Class
文件,class
文件在文件开头有特定的文件标识 ClassLoader
只负责class
文件的加载,至于它是否可以运行,则由Execution Engine
(执行引擎)来决定- 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是
Class
文件中常量池部分的内存映射)
class file
存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM
当中来根据这个文件实例化出n
个一模一样的实例class file
加载到JVM
中,被称为DNA
元数据模板,放在方法区- 在 .
class
文件 ->JVM
-> 最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader
),扮演一个快递员的角色
类加载的过程
类的生命周期( 7 个阶段)
其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段可以在初始化之后再开始(运行时绑定或动态绑定或晚期绑定)
以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):
- 遇到
new、getstatic、putstatic
或invokestatic
这4
条字节码指令时没初始化触发初始化。使用场景:使用new
关键字实例化对象、读取一个类的静态字段(被final
修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法 - 使用
java.lang.reflect
包的方法对类进行反射调用的时候 - 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化
- 当虚拟机启动时,用户需指定一个要加载的主类(包含
main()
方法的那个类),虚拟机会先初始化这个主类
类加载的过程示例
public class HelloLoader {
public static void main(String[] args) {
System.out.println("我已经被加载啦");
}
}
它的加载过程如下
类加载器的体系
- 如果是
JDK
自带的类(Object、String、ArrayList
等),其使用的加载器是Bootstrap
加载器 - 如果自己写的类,使用的是
Application
加载器 Extension
加载器是负责将把Java
更新的程序包的类加载进行Java
加载器个数为3+1
。前三个是系统自带的,用户可以定制类的加载方式,通过继承Java. lang. ClassLoader
类加载器的双亲委派模型
除顶层启动类加载器之外,其他都有自己的父类加载器
工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载
Java
内存模型与线程
Java
内存模型
线程,工作内存,主内存之间的交互关系
- 每个线程都有一个属于自己的工作内存
Java
的内存模型规定,将所有的变量都存放在主内存中,当某一个线程要使用变量时,会把主内存里面的变量复制到自己工作内存,这个线程读写变量时操作的是自己工作内存中的变量- 当这个线程读写变量完成后,会把自己工作内存中的共享变量写回到主内存当中
volatile
关键字
可以确保对一个变量的更新对其他线程马上可见,只是保证可见性,并发情况下依然不安全
当一个变量被声明为 volatile
时,线程在写入变量时会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值
synchronized
关键字
- 属于重量级锁机制,可以保证线程的安全
- 进入
synchronized
块的内存语义是把在synchronized
块内使用到的变量从线程的工作内存中清除,这样在synchronized
块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取 - 退出
synchronized
块的内存语义是把在synchronized
块内对共享变量的修改刷新到主内存
synchronized
关键字原理
通过内部对象 Monitor
(监视器锁)实现,基于进入与退出 Monitor
对象实现方法与代码块的同步。监视器锁的实现是依赖于底层操作系统的 Mutex Lock
(互斥锁)实现,它是一个重量级锁,性能较低
参考:https://blog.csdn.net/weixin_38192427?spm=1001.2101.3001.5343