Java内存模型与生命周期以及JVM

本文探讨了为什么需要内存模型,阐述了CPU和缓存一致性以及并发编程中遇到的问题。接着,介绍了JVM的内存结构,并详细讲解了Java内存模型(JMM)如何解决并发问题,包括限制处理器优化和使用内存屏障。最后,简述了JAVA类的完整生命周期,包括加载、连接、初始化、使用和卸载五个阶段,以及各个阶段的具体细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么要有内存模型

CPU 和缓存一致性

我们应该知道,计算机在执行程序的时候,每条指令都是在 CPU 中执行的,而执行的时候,又免不了和数据打交道。

而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存。

刚开始,还相安无事,但是随着 CPU 技术的发展,CPU 的执行速度越来越快。

而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和 CPU 的执行速度比起来差距就会越来越大,这就导致 CPU 每次操作内存都要耗费很多等待时间。

这就像一家创业公司,刚开始,创始人和员工之间工作关系其乐融融,但是随着创始人的能力和野心越来越大,逐渐和员工之间出现了差距,普通员工越来越跟不上 CEO 的脚步。

老板的每一个命令,传达到基层员工之后,由于基层员工的理解能力、执行能力的欠缺,就会耗费很多时间。这也就无形中拖慢了整家公司的工作效率。

可是,不能因为内存的读写速度慢,就不发展 CPU 技术了吧?总不能让内存成为计算机处理的瓶颈吧?

所以,人们想出来了一个好的办法,就是在 CPU 和内存之间增加高速缓存。

缓存的概念大家都知道,就是保存一份数据拷贝。它的特点是速度快,内存小,并且价格昂贵。

那么,程序的执行过程就变成了:程序在运行过程中,会将运算需要的数据从主存复制一份到 CPU 的高速缓存当中。

那么 CPU 进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

之后,这家公司开始设立中层管理人员,管理人员直接归 CEO 领导,领导有什么指示,直接告诉管理人员,然后就可以去做自己的事情了。管理人员负责去协调底层员工的工作。

因为管理人员是了解手下的人员以及自己负责的事情的。所以大多数时候,公司的各种决策,通知等,CEO 只要和管理人员之间沟通就够了。

而随着 CPU 能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。

按照数据读取顺序和与 CPU 结合的紧密程度,CPU 缓存可以分为一级缓存(L1),二级缓存(L2),部分高端 CPU 还具有三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分。

这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。

那么,在有了多级缓存之后,程序的执行就变成了:当 CPU 要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。

随着公司越来越大,老板要管的事情越来越多,公司的管理部门开始改革,开始出现高层,中层,底层等管理者。一级一级之间逐层管理。

单核 CPU 只含有一套 L1,L2,L3 缓存;如果 CPU 含有多个核心,即多核 CPU,则每个核心都含有一套 L1(甚至和 L2)缓存,而共享 L3(或者和 L2)缓存。

公司也分很多种,有些公司只有一个大 Boss,他一个人说了算。但是有些公司有比如联席总经理、合伙人等机制。

单核 CPU 就像一家公司只有一个老板,所有命令都来自于他,那么就只需要一套管理班底就够了。

多核 CPU 就像一家公司是由多个合伙人共同创办的,那么,就需要给每个合伙人都设立一套供自己直接领导的高层管理人员,多个合伙人共享使用的是公司的底层员工。

还有的公司,不断壮大,开始拆分出各个子公司。各个子公司就是多个 CPU 了,互相之前没有共用的资源。互不影响。

随着计算机能力不断提升,开始支持多线程。那么问题就来了,我们分别来分析下单线程、多线程在单核 CPU、多核 CPU 中的影响。

单线程:CPU 核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。

单核 CPU,多线程:进程中的多个线程会同时访问进程中的共享数据,CPU 将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。

但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。

多核 CPU,多线程:每个核都至少有一个 L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的 Cache 中保留一份共享内存的缓冲。

由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的 Cache 之间的数据就有可能不同。

在 CPU 和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核 CPU 中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。

如果这家公司的命令都是串行下发的话,那么就没有任何问题。

如果这家公司的命令都是并行下发的话,并且这些命令都是由同一个 CEO 下发的,这种机制是也没有什么问题。因为他的命令执行者只有一套管理体系。

如果这家公司的命令都是并行下发的话,并且这些命令是由多个合伙人下发的,这就有问题了。

因为每个合伙人只会把命令下达给自己直属的管理人员,而多个管理人员管理的底层员工可能是公用的。

比如,合伙人 1 要辞退员工 a,合伙人 2 要给员工 a 升职,升职后的话他再被辞退需要多个合伙人开会决议。两个合伙人分别把命令下发给了自己的管理人员。

合伙人 1 命令下达后,管理人员 a 在辞退了员工后,他就知道这个员工被开除了。

而合伙人 2 的管理人员 2 这时候在没得到消息之前,还认为员工 a 是在职的,他就欣然的接收了合伙人给他的升职 a 的命令。
处理器优化和指令重排

上面提到在 CPU 和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。

除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是处理器优化。

除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如 Java 虚拟机的即时编译器(JIT)也会做指令重排。

可想而知,如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。

关于员工组织调整的情况,如果允许人事部在接到多个命令后进行随意拆分乱序执行或者重排的话,那么对于这个员工以及这家公司的影响是非常大的。

并发编程的问题

前面说的和硬件有关的概念你可能听得有点蒙,还不知道他到底和软件有啥关系。

但是关于并发编程的问题你应该有所了解了,比如原子性问题,可见性问题和有序性问题。

其实,原子性问题,可见性问题和有序性问题是人们抽象定义出来的。而这个抽象的底层问题就是前面提到的缓存一致性问题、处理器优化问题和指令重排问题等。

这里简单回顾下这三个问题,我们说,并发编程,为了保证数据的安全,需要满足以下三个特性:

原子性,是指在一个操作中,CPU 不可以在中途暂停然后再调度,即不被中断操作,要不执行完成,要不就不执行。
可见性,是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性,即程序执行的顺序按照代码的先后顺序执行。
有没有发现,缓存一致性问题其实就是可见性问题。而处理器优化是可以导致原子性问题的。指令重排即会导致有序性问题。

所以,后文将不再提起硬件层面的那些概念,而是直接使用大家熟悉的原子性、可见性和有序性。

JVM内存结构

Java程序执行过程中,内存会被划分为不同的数据区域,各个区域有各自的用途。

有些区域随虚拟机的启动而存在
有些区域随线程的启动而启动,随线程的结束而销毁JVM运行时的内存结构

什么是内存模型?

前面提到的,缓存一致性问题、处理器优化的指令重排问题是硬件的不断升级导致的。那么,有没有什么机制可以很好的解决上面的这些问题呢?

最简单直接的做法就是废除处理器和处理器的优化技术、废除 CPU 缓存,让 CPU 直接和主存交互。

但是,这么做虽然可以保证多线程下的并发问题。但是,这就有点因噎废食了。

所以,为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是——内存模型。

为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。

通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。

它解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

内存模型解决并发问题主要采用两种方式:

限制处理器优化
使用内存屏障
Java 内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存。

线程的工作内存中保存了该线程中用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。

不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

而 JMM 就作用于工作内存和主存之间数据同步过程。它规定了如何做数据同步以及什么时候做数据同步。
这里面提到的主内存和工作内存,读者可以简单的类比成计算机内存模型中的主存和缓存的概念。

特别需要注意的是,主内存和工作内存与 JVM 内存结构中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,无法直接类比。

《深入理解Java虚拟机》中认为:如果一定要勉强对应起来的话,从变量、主内存、工作内存的定义来看,主内存主要对应于 Java 堆中的对象实例数据部分。而工作内存则对应于虚拟机栈中的部分区域。

简述JAVA类的生命周期

一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段

加载

主要是:把类的信息加载到方法区中,并在堆中实例化一个Class对象。

加载方式

根据类的全路径加载class文件
从jar的包中读取class文件
根据一定的规则实时生成,比如设计模式中的动态代理模式,就是根据相应的类自动生成它的代理类。

加载的时期

不是jvm启动就加载,而是在真是使用的时候才会触发加载。

new 一个类的时候
调用类的静态方法,以及读取或者修改一个类的静态字段的时候(不是常量)
这个类是程序的入口类
对这个类进行反射的时候(执行了上面的行为)
连接
一般会跟加载阶段和初始化阶段交叉进行。

验证

验证一下这个类是否合法,

字节码格式是否合法
变量和方法是否有重复
继承和实现是否符合标准
。。。

准备

给类的静态变量分配并初始化存储空间;
也就是给静态变量赋默认的初始值(不包括非静态变量)

解析

把符合引用转换为直接引用。
比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,
jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。
这里show就是符号引用,而c17164就是直接引用。
在解析阶段jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。

初始化

执行静态变量的初始化和静态Java代码块,并初始化程序员设置的变量值!

时机

和加载的时机一样,更准确的说初始化之前必须先经过加载,所以他们基本一样

new 一个类的时候
调用类的静态方法,以及读取或者修改一个类的静态字段的时候(不是常量)
对这个类进行反射的时候(执行了上面的行为)
初始化一个类的子类,该子类所有的父类都会被初始化。
作为程序的入口类(如:main方法所在的类,java 命令跟着的类)
过程
按照顺序自上而下运行类中的【变量赋值语句】和【静态语句】,
如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句
参考:
https://developer.51cto.com/art/201807/579744.html
https://www.cnblogs.com/wangsen/p/10838733.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值