1、JVM运行时数据区
image-20230301154314997.png-
堆(共享)
- 堆用来存放对象和数组,只要是堆中的对象,就可以被所有线程共享(静态变量、静态常量、字符串存储在堆中的老年代里)
- Java7 版本中将永久代的静态变量和运行时常量池转移到堆中存放的
- 堆是 JVM 上最大的内存区域。垃圾回收操作的对象就是堆
- 堆空间一般是程序启动时就申请了,一般设置成可伸缩的。 随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收,这就是GC
- 对于基本数据类型对象(如byte、short、int、long、float、double、char),在方法体内声明时,会直接分配在栈中,其它情况都会分配在堆中
- 对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用它的引用
- 比如,把这个引用保存在虚拟机栈的局部变量表中。但是在开启了逃逸分析时,如果发现某个对象只会在方法内部使用,则可能会将该对象经过标量替换后也存在栈中
- 堆的几个重要参数
- -Xms:堆的最小值(初始值,默认单位是:字节,要求是1024的整数倍)
- -Xmx:堆的最大值
- -Xmn:新生代的大小
- -XX:NewSize;新生代最小值(初始值)
- -XX:MaxNewSize:新生代最大值
- 堆用来存放对象和数组,只要是堆中的对象,就可以被所有线程共享(静态变量、静态常量、字符串存储在堆中的老年代里)
-
方法区(元空间)(共享)
- 方法区存储的内容
- 类型信息(比如类全称、父类全称)
- 域信息(域名称、域修饰符private等)
- 方法信息(方法名称、方法修饰符、返回类型等)
- 字面量(字面量包括文本字符串、八种基本类型的值 、被声明为final的常量等)
- 假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有加载进 JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待
- 方法区是 JVM 对内存的逻辑划分
- 在 JDK1.7 及之前将方法区称为永久代
- 在 JDK1.8 及以后使用了元空间来实现方法区
- 元空间大小参数设置:
- 如果不设置参数,则只受本机总内存的限制
- jdk1.7 及以前
- -XX:PermSize
- -XX:MaxPermSize
- jdk1.8 以后
- -XX:MetaspaceSize
- -XX:MaxMetaspaceSize
- 方法区存储的内容
-
本地方法栈
- 如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行
-
虚拟机栈(线程私有)
- Java虚拟机栈是当前线程在执行方法时存储所需的数据、指令、返回地址的一种栈结构(先进后出)。它的生命周期与线程保持一致
- 每调用一个方法就会在栈里加入一个栈帧。调用的方法执行完了,对应的栈帧就会出栈
- 栈帧里分为4个区域,这4个区域就包含了执行Java方法时的全部内容
- 局部变量表
- 存方法参数和局部变量。如果是引用类型则存的是对象在堆中的地址。第0行,放的就是this
- 操作数栈
- 操作数栈也是一个先进后出的栈结构,它用来临时存放即将要操作的数据
- 动态连接
- Java 语言特性多态(需要类加载、运行时才能确定具体的方法),如子类重写父类方法后,具体调用哪个要等到执行时才能确定。这时候的符号引用转化为直接引用称为动态链接
- 方法出口
- 方法出口其实就是记录的返回地址。例如:方法1调用了方法2之后,那么方法2的出口地址就是方法1调用方法2的代码位置,因为在方法2执行完了之后,要回到方法1继续执行。
- 正常返回(遇到方法返回的字节码指令)
- 异常返回(通过异常处理器表<非栈帧中的>来确定),并且这个异常没有在方法体内得到处理
- 局部变量表
-
程序计数器(线程私有)
- 由于现在都是多线程运行,而一个CPU在同一时刻只能运行一个线程,多个线程只能交替运行。程序计数器的作用就是记录当前线程下一条要运行的指令,这样保证了线程在切换回来时能回到正确的位置继续开始执行
- 如果正在执行的是Native 方法,由于不是JVM执行,则这个计数器值为空(Undefined)
1.1、堆空间划分
image-20230301160529675.png-
一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区
- 大对象直接进入老年代
- JVM参数
-XX:PretenureSizeThreshold
来设置对象大小阈值(字节数)- 比如“1048576”,就是1M
- JVM参数
- 经过15次GC仍然存活的对象
- JVM参数
-XX:MaxTenuringThreshold
来设置存活次数,默认15
- JVM参数
- 动态对象年龄判断
- Survivor区域里一批对象的 总大小大于了这块Survivor区域的内存大小的50% ,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了
- Survivor区空间不够
- GC后Survivor区空间不够存放对象了,这时会吧对象直接转移到老年代
- 老年代空间分配担保规则
- 大对象直接进入老年代
-
对象入堆流程
image-20230301160753966.png
1.2、Java对象内存布局
image-20230301160428185.png-
对象头
- Mark Word
- 包含一系列的标识,例如锁的标记、对象年龄等。在32位系统占4字节,在64位系统中占8字节
- Class Pointer
- 指向对象所属的 Class 在方法区的内存指针,通常在32位系统占4字节,在64位系统中占8字节,64位 JVM 在 1.6 版本后默认开启了压缩指针,那就占用4个字节
- Length
- 如果对象是数组,还需要一个保存数组长度的空间,占 4 个字节
- Mark Word
-
Mark Word
image-20230302140421111.png -
Monitor
- Java虚拟机给每个对象和class字节码都设置了一个监听器Monitor,用于检测并发代码的重入
- 当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程
- Contention List:所有请求锁的线程将被首先放置到该竞争队列
- Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
- Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
- OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
- Owner:获得锁的线程称为Owner
- !Owner:释放锁的线程
2、垃圾回收(GC)
-
如何判断垃圾对象
- 可达性分析算法
- 是以根对象(GCRoots)为起始点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达
- GC Roots包括
- 本地方法栈内JNI,引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用
- GC Roots包括
- 使用可达性分析算法后,内存中存活的对象都被被根对象集合直接或间接连接着,搜索所走过的路径称为引用链
- 如果目标对象没有任何引用链相连,则是不可达的,意味着该对象已经死亡,可以标记为垃圾对象
- 是以根对象(GCRoots)为起始点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达
- 可达性分析算法
-
GC算法
- 标记-清除算法
- 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象Header中记录为可达对象
- 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
- 复制算法
- 将或者的内存空间分为两块,每次使用其中一块。在垃圾回收时,将正在使用的内存中的存活的对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有的对象,交换两个内存的角色,最后完成垃圾回收
- 标记-压缩(标记-整理)算法
- 第一个阶段和标记清除算法一样,从根节点开始标记所有被引用的对象
- 第二阶段将所有的存活对象压缩在内存的一端,按照顺序排放,之后清理边界外所有的空间
- 最终效果等同于标记清除算法执行完成后,再进行一次内存碎片整理
- 与标记清除算法本质区别,标记清除算法是非移动式的算法,标记压缩是移动式的
- 分代收集算法
- 不同生命周期的对象可以采取不同的收集方式,以便提高回收效率,几乎所有的GC都采用分代收集算法执行垃圾回收的
- 年轻代
- 生命周期短,存活率低,回收频繁
- 采用Serial、ParNew、Parallel Scavenge回收算法
- 老年代
- 区域较大,生命周期长,存活率高,回收不及年轻代频繁
- 采用Serial Old、Parallel Old GC、CMS 回收算法
- 增量收集算法
- 每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成
- 通过对线程间冲突的妥善管理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
- 分区算法
- 为了控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一次GC所需时间
- 分代算法是将对象按照生命周期长短划分为两个部分,分区算法是将整个堆划分为连续的不同的小区间
- 每一个小区间都独立使用,独立回收,这种算法的好处是可以控制一次回收多少个小区间
- 标记-清除算法
-
Serial回收器(串行回收)
- Serial收集器采用复制算法,串行回收和STW机制的方式执行内存回收
-
ParNew回收器(并行回收)
- 除了采用并行回收,其他方面和Serial之间几乎没有任何区别
- 年轻代使用,不影响老年代
-
CMS回收器(低延迟)
-
CMS(Concurrent Mark Sweep)收集器是一种以获取
最短回收停顿时间
为目标的收集器 -
采用的是
标记-清除算法
整个过程分为4步- 初始标记
- 标记GC Roots能关联到的对象,需要Stop The World
- 并发标记
- 进行GC Roots Tracing
- 重新标记
- 修改并发标记因用户程序变动的内容,需要Stop The World
- 并发清除
- 初始标记
-
由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来
说,CMS收集器的内存回收过程是与用户线程一起并发地执行的
-
由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此CMS收集器不能像其他收集器那样等到老年代几乎填满再进行回收,而是当堆内存使用率达到某一阈值时,便开始进行回收
-
CMS采取标记清除算法,会产生内存碎片,只能够选择空闲列表执行内存分配
-
-
G1回收器
- Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合
- 使用不同的region表示Eden,s0,s1,老年代等,G1跟踪各个region里面垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
- 优势
- 并行与并发
- 同时兼顾年轻代与老年代
- 空间整合
- region之间用复制算法,整体可以看做是标记压缩算法。
- 两种算法都避免内存碎片,有利于程序长时间运行,分配大对象不会因为无法找到连续空间提前触发下一次GC,尤其当Java堆非常大的时候,G1优势更加明显
- 可预测的停顿时间模型
- 能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不能超过N毫秒
- Region
- region可以充当多个角色,所有region大小相同,且在JVM生命周期内不会改变
- 一个region可能属于Eden、survivor或Old内存区域。但是一个region只能属于一个角色
- G1还增加了一个新的内存区域:humongous,主要用于存储大对象,如果超过1.5个region,就放到humongous中
- 工作过程可以分为如下几步
- 初始标记
- 标记一下GC Roots能够关联的对象,并且修改TAMS的值,需要Stop The World
- 并发标记
- 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
- 最终标记
- 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需要Stop The World
- 筛选回收
- 对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划
- 初始标记
3、什么是JMM
-
JMM就是Java内存模型(java memory model)。JMM 是建立在硬件内存模型基础上的抽象模型,因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以JMM屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果
-
JMM抽象结构划分为线程本地缓存与主存,每个线程均有自己的本地缓存,本地缓存是线程私有的,主存则是计算机内存,它是共享的
932dc2d271410523a126591b8268cb74.png
- JMM内存模型规则
- 我们所有定义的变量(除局部变量)都存放在主内存(Main Memory)
- 每个线程都有自己的工作内存,工作内存中存放需要使用的变量的主内存副本
- 线程对所有变量的操作都必须在工作内存中进行。不能直接在主内存中操作
- 不同线程之间无法直接访问对方工作内存中的变量。线程间变量值的传递需要通过主内存进行传递
- 保证并发的三大特性
- 可见性
- 当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,这就是可见性,如果无法保证,就会出现缓存一致性的问题
-
JMM
规定,所有的变量都放在主存中,当线程使用变量时,先从缓存中获取,缓存未命中,再从主存复制到缓存,最终导致线程操作的都是自己缓存中的变量 - Java是利用
volatile
关键字来提供可见性的。 当变量被volatile修饰时,这个变量被修改后会立刻刷新到主内存,并失效其他线程的缓存,当其它线程需要读取该变量时,会去主内存中读取新值
- 原子性
-
原子性是指一个或者多个操作在
CPU
执行的过程中不被中断的特性,一个线程在执行时不会被其他线程干扰 -
Java
中提供了synchronized
(同时满足有序性、原子性、可见性)可以保证结果的原子性
-
原子性是指一个或者多个操作在
- 有序性
- 编译器和处理器为了优化性能,会对代码做重排,所以语句实际执行的先后顺序与输入的代码顺序可能一致,这就是指令重排序
- 为解决重排序,使用Java提供的
volatile
修饰变量同时保证可见性、有序性,被volatile
修饰的变量会加上内存屏障禁止排序
- 可见性
特性 | volatile | synchronized | Lock | Atomic |
---|---|---|---|---|
可见性 | 可以保证 | 可以保证 | 可以保证 | 可以保证 |
原子性 | 无法保证 | 可以保证 | 可以保证 | 可以保证 |
有序性 | 一定程度保证 | 可以保证 | 可以保证 | 无法保证 |
网友评论