06-底层必备源码-JVM内存模型(Java多线程并发编程)

01-jvm学习路线图

 

02-Java内存模型(Java多线程并发编程)

视频讲解地址:

京东架构师100分钟带你重新认识Java内存模型!让你面试无忧!_哔哩哔哩_bilibili

1.多核并发缓存架构解析

2.jmm模型讲解

3.jmm数据原子操作详解

4.jmm缓存不一致性问题详解

6.volatile可见性底层实现原理

6.volatile可见性、原子性和有序性讲解

7.深入理解Java内存模型jmm与volatile关系

Thread的join()方法:

多个线程同时执行的时候,join()方法会让主线程等待其他的线程执行完

volatile

JVM 整体架构:

Java语言的优势:跨平台,(一处编译,到处运行),java-jre-jvm

为什么可以跨平台:在jvm什么运行的程序都可以跨平台

javap -c + .class文件:反编译java的class文件

jvm调优,

避免发生full gc,需要结果具体核心业务分析,

尽量 minor gc

上线之前给jvm设置参数:

03-java中JVM和JMM之间的区别

一、jvm和jmm之间的关系

二、JMM:java内存模型。

三、JVM:java虚拟机模型

程序在栈中的运行流程:

方法出口:

程序计数器:

本地方法栈

方法区(元空间)

Minor GC :

Full GC:

JVM调优的终极目标:

OOm堆内存溢出

JMM:java内存模型。

JVM:java虚拟机模型。

一、jvm和jmm之间的关系 二、JMM 三、JVM 四、Run Data Area(运行时数据区)

一、jvm和jmm之间的关系

jmm中的主内存、工作内存与jvm中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,

主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。

从更低层次上说,主内存就直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。


二、JMM:java内存模型。

Java的内存模型:分为主内存,和工作内存。

1.主内存:是所有的线程所共享的,

2.工作内存:是每个线程自己有一个,不是共享的。

内存可见性问题:线程工作时将要用到的变量从主内存拷贝到自己的工作内存,然后在工作内存中进行读和写。写完之后,可能没被更新到主内存去。导致其他线程从主内存拷贝数据到自己的工作区时,拷贝的不是最新的数据。这就是内存可见性问题。

Java内存模型(Java Memory Model,JMM)JMM主要是为了规定了线程和内存之间的一些关系。

根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。

每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

  1. Java 虚拟机规范定义了 Java 内存模型来屏蔽掉各种硬件和操作系统的内存差异,达到跨平台的内存访问效果。
  2. 为了获得更好的执行性能,Java 内存模型没有限制执行引擎使用处理器的特定缓存器或缓存来和主内存(可以和 RAM类比,但是是虚拟机内存的一部分)交互,工作内存(可类比高速缓存,也是虚拟机内存的一部分)为线程私有。
  3. 工作内存和主内存的划分和 Java 堆,栈,方法区的划分不同,两者基本没有关系,如果勉强对应,则主内存可理 解为堆中实例数据部分,工作内存则对应栈中部分区域

-----引用>


三、JVM:java虚拟机模型

组成:

  1. 程序计数器:线程私有,表明当前线程执行行数
  2. java虚拟机栈:基本类型,引用类型对应地址
  3. 本地方法栈:保存

  1. java原生方法的信息
  2. 堆:1/3年轻代+2/3老年代
  3. 方法区:持久代。存储类信息、常量、静态变量

1 Class Loader(类加载器)就是将Class文件加载到内存,再说的详细一点就是,把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是类加载器的作用。

2 Run Data Area(运行时数据区) 就是我们常说的JVM管理的内存了,也是我们这里主要讨论的部分。运行数据区是整个JVM的重点。我们所有写的程序都被加载到这里,之后才开始运行。这部分也是我们这里将要讨论的重点。

3 Execution engine(执行引擎) 是Java虚拟机最核心的组成部分之一。执行引擎用于执行指令,不同的java虚拟机内部实现中,执行引擎在执行Java代码的时候可能有解释执行(解释器执行)和编译执行(通过即时编译器产生本地代码执行,例如BEA JRockit),也有可能两者兼备。任何JVM specification实现(JDK)的核心都是Execution engine,不同的JDK例如Sun 的JDK 和IBM的JDK好坏主要就取决于他们各自实现的Execution engine的好坏。

4 Native interface 与native libraries交互,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,所以也很容易出现JVM无法控制的native heap OutOfMemory。

学习地址:

https://www.processon.com/view/link/5ff6bff11e08531de8231a57#map

线程共享数据:

堆 和 方法区(元空间)

线程私有的(线程安全的):

栈,本地方法栈,程序计数器

所以我们可以知道,每一个线程都会有属于自己的区域(程序计数器,栈,本地方法栈)

不同的区域都会给线程分配专属的空间

java源文件被编译为字节码文件(.class全是机器指令)

javap - c HelloWorld.class : 反编译 字节码文件之后

反编译后的字节码文件为jvm指令 (Oracle官网上有jvm指令手册)

Oracle官网上有jvm指令手册

程序在栈中的运行流程:

当我们启动程序的时候,main()方法会入栈

cacl()方法 的执行过程:

calc() 栈帧 的组成:(同样其他的方法也有类似的结构,比如main()方法)

局部变量表

操作数栈(它本身也是栈结结构)

动态链接

方法出口

calc() 入栈,执行calc() 方法 
1. 先将1压如操作数栈 
2. 1 出栈,然后赋值为局部变量a 
3. 将2压如操作数栈 
4. 2 出栈,然后赋值为局部变量b 
5. 操作数栈里面 装载1 2 ,然后相加 为3 6. 将10压栈,
然后进行 相乘 操作,计算出结构30,然后将30压入操作数栈, 
最后返回30,赋值为c calc()执行完之后,就出栈,
然后继续执行下面的代码,

栈内存溢出的场景解析:

方法的递归调用

循环调用方法(栈溢出) 流程:

main() 方法会在栈里面开辟一块空间 调用repeat() 方法,然后会给该方法开辟一块空间 循环调用repeat()方法,然后会在栈中,又开辟一快空间, 循环调用,一直开辟空间,知道这个栈的空间不够了,然后就栈内存溢出了 开辟栈, 空间的默认大小是 1M(这个参数可以设置的) 默认给该线程分配1M的空间,然后你循环调用该方法,然后一直开辟栈空间,查过1M之后, 空间就不够了,这个时候就会栈内存溢出了

动态链接

方法出口:

当执行完方法之后,方法需要出栈,但是有返回值, 如何知道这个返回值是给谁的?接下来该走哪一步程序呢? 方法出口就起到这个的作用,会有标记,然后就继续往下面执行程序

程序计数器:

程序执行到什么地方是 由程序计数器 标记的,这样程序才能依次向下执行 程序计数器的值的更改是谁修改的? 程序计数器的值的修改是由 字节码执行引擎更改的,程序的执行也都是由字节码执行 引擎执行的,所以字节码执行引擎修改程序计数器的值也是很方便的

本地方法栈

java程序中的native方法 native()方法都是执行c或者c++语言的具体逻辑 这里类似 一个jar包,直接调用c或者c++的方法就行 c或者c++的方法存在什么地方的呢:就是存在本地方法栈里面的

方法区(元空间)

1.8之后换成了名字 元空间 组成: 1.常量 2.静态变量 3.类元信息(类信息) java执行。class字节码文件的时候,类装在子信息会将类名,方法名吖,常量吖 等信息都加载到方法区里面去

堆的组成:年轻代 (1/3)+ 老年代(2/3)

年轻代的组成:Eden元区(8/10) + 幸存区Survivor(s0(1/10) + s1(1/10))

产生的对象存在于Eden元区,程序是24小时不断执行的,一直创建对象的话,那Eden元区且不是会爆满呢,空间不够了,这个时候就需要回收对象(回收没有被引用到的对象)

垃圾对象被回收,回收到什么地方呢?

对象被引用,不是垃圾对象,不被回收,会去到什么地方呢?

Minor GC :

1.存活的对象: 会被存放到幸存区Survivor,(对象obj被从Eden元区 丢到 S0 区域, obj的分代年龄为1 ) 分代年龄:对象被存到幸存区Survicor之后,对象的分代年龄会 + 1 , 程序还是继续 运行一直创建对象,Eden元区会触发Minor GC(字节码执行引擎会开启1个垃圾回收的线程, 执行Minor GC) Eden元区的对象被 Minor GC,同时也会去判断s0区域的对象是否需要gc,obj2从Eden 元区被分配到s1区域(它的分代年龄为1),去判断s0区的对象是否需要gc的时候, 当obj也一直被引用的时候,obj就不需要gc就同样的被移到s1区(obj的分戴年龄+1 变成2 )

当对象被Minor GC 15次之后(分代年龄为15),该对象就会被移到老年代去

Minor GC 15次都未被回收的对象,就认为是老不死的对象,就移动到老年代

stw

Full GC:

当老年代的对象过多的时候,就会触发Full GC,Full GC的时候不仅仅是回收老年代的对象,年轻代的对象也会回收,所以Full GC的时间比 Minor GC的时间要长很多

影响:在full gc的时候会停止用户线程,对于用户来讲,就是会卡顿,很影响用户体验

Full GC的话,所有的老年代的对象都会被回收?

Full GC 垃圾回收,肯定回收的都是那些没有被引用到的对象 一般项目程序里面的什么对象会放在老年代呢: 比如spring 容器对象,线程池对象,缓存的对象

JVM调优的终极目标:

调优的时候就是为了减少Full GC

OOm堆内存溢出

对象一直被引用,不回收,堆里面的老年代一直存对象,直到内存不足, 出现oom堆内存溢出

cmd 进入命令框

输入:jvisualvm命令打开java提供的jvm可视化页面工具

安装插件才能看到这个可视化界面

思考为什么:

https://blog.csdn.net/qq_41701956/article/details/100074023

1)为什么要分为Eden和Survivor?

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。

Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

2)为什么要设置两个Survivor区?

设置两个Survivor区最大的好处就是:解决了碎片化,

刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)

3)JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代?

CPU-CPU缓存-主内存(Volatile)

        计算机的运转是,将磁盘上面的文件,读取到主内存,然后再到cpu中就行运算。在最初的时候cpu 和主内存的运算速度差不多,但是随着时间的推移,CPU的运算速度增长特别快,然而主内存的计算速度和CPU的计算速度差距就很大。导致主内存会拖慢CPU的计算速度,所以这个时候就出现了CPU缓存,数据线从主内存读到cpu缓存,CPU缓存的计算速度,相对于比较接近CPU。这样就可以提高计算速度。

JMM内存模型

         JMM内存模型中,线程之间会存在内存可见行问题:就是线程1 2 3读取主内存中的共享变量作为自己线程的工作内存中的共享变量副本,但是当别的线程修改了共享变量副本的时候,传递到主内存中修改了共享变量。其余线程中的共享变量副本是旧的,这就是线程的可见性问题。

Volatile 共享变量:

         1.当静态变量没有加volatile修饰的时候,当第二个线程修改了initFlag的值为true,第一个线程读取到的initflag的值会一直是旧的false,所以一直无法结束while循环。

        2.当使用volatile修饰变量initFlag的时候,线程2修改了initFlag的值之后,线程1也会立刻知道initFlag的值被修改为true。被volatie修饰的变量,可以解决线程可见性问题,当共享变量被某一个线程修改的时候,会立刻通知其余使用到该共享变量的线程。

JMM数据原子操作:

 

线程从主内存中读取共享变量,修改共享变量的操作:

        线程1 :read(先从主内存中读取共享变量) -> load(将主内存读取到的数据写入到工作内存中) -> use(从工作内存读取数据来计算)

        线程2 :read(先从主内存中读取共享变量) -> load(将主内存读取到的数据写入到工作内存中) -> use(从工作内存读取数据来计算) -> assign(将计算好的 值重新赋值到工作内存中) -> store(将工作内存中的数据写入主内存)-> write(将store过去的变量赋值给主内存中的变量)

        线程1一直死循环,工作内存中的变量一直都是旧的值。

        线程2已经将工作内存中的修改后的副本值同步更新到了主内存中。

 JMM缓存不一致问题:

1.CPU缓存一致性协议(MESI):

2.缓存加锁:

 

 Volatile缓存可见性实现原理:

 

 CPU的指令重排序:

 

 重排序会遵循as-if-serial 与 happens-before原则

需要结合Java内存模型与线程规范

 规范里面存在2个规范as-if-serial, happens-before

happens-before(可理解为JAVA也是一个产品,产品经理设计了happens-before这个功能,功能就是这个规范):

 happens-before原则

 

 

 阿里面试题:双重检查锁(double-check locking)

阿里巴巴开发规范最新文档:

 

 对象创建时候的流程:

 

 

解决方法:阿里巴巴开发规范里描述的,在属性上加volatile修饰,可以保证程序的可见性和有序性(这样就不会发生cup指令重排序)

JVM规范定义的内存屏障:

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值