01.说说对JVM和JMM的理解

vvEcho 2024-01-20 14:08:36
Categories: Tags:

JVM

JVM 是 Java 虚拟机,是Java程序运行的执行引擎和内存管理平台。
它定义了Java程序如何与操作系统交互,负责字节码的加载、解释/编译、内存分配、垃圾回收等核心功能。JVM 的具体实现包括内存区域划分(如堆、栈、方法区)和运行机制(如类加载、即时编译).

jvm由类装载器、运行时数据区、执行引擎、本地方法接口还有垃圾回收器构成;其中垃圾回收器作用在整个jvm内存中(主要作用在堆和方法区)

其中运行时数据区分为:线程共享区和线程独占区

线程共享区包含:方法区和堆

线程独占区分为:虚拟机栈,本地方法栈,程序计数器(也叫PC寄存器)

方法区存的存储的是已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,此区域可能会有OOM;方法区与堆有很多共性:线程共享、内存不连续、可扩展、可垃圾回收,同样当无法再扩展时会抛出OutOfMemoryError异常。正因为如此相像,Java虚拟机规范把方法区描述为堆的一个逻辑部分,但目前实际上是与Java堆分开的(Non-Heap)

堆存放对象实例,堆的GC采用分代收集算法;堆内存分为新生代和老年代还有永久代(java8改为元空间),新生代又细分为伊甸园区,S0区,S1区

其中新生代采用的是复制算法(-1.当eden满时会触发YoungGC,把Eden和S0区域中存活的对象复制到S1区域(如果有对象的年龄以及达到了老年的标准,则复制到老年代区),同时把这些对象的年龄+1,如果S1空间不够了也会放到老年区;-2.清空eden、servicorFrom,然后清空Eden和ServicorFrom中的对象;-3.S1和S0互换:清理完之后,S1中的对象的放入到S0中,这个时候S1又变成空了,
然后这样循环,eden满了又一次一次这样的对调,当年龄达到一定标准(比如15次)就直接进入老年代了)

Eden区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发 YoungGC,对新生代区进行一次垃圾回收。

S0: 上一次YoungGc的幸存者,作为这一次GC的被扫描者。

S1: 保留了一次YoungGc过程中的幸存者

老年代:采用的是标记清除算法首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常

永久代:指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。

在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory, 而字符串池和类的静态变量放入java堆中,所以元空间就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制

JMM

MM(Java Memory Model)是Java语言内存模型,是一种抽象的概念,并不真实存在,它描述的是一组规则或者规范。通过这些规则、规范定义了程序中各个变量的访问方式。
jvm运行程序的实体是线程,而每个线程运行时,都会创建一个工作内存(也叫栈空间),来保存线程所有的私有变量。

而JMM内存模型规范中规定所有的变量都存储在主内存中,而主内存中的变量是所有线程都可以共享的,对主内存中的变量进行操作时,必须在线程的工作内存中操作,首先将主内存的变量copy到工作内存,进行写操作后,再将变量刷回到主内存中,所有线程只有通过主内存来进行通信

所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量,还是局部变量,类信息、常量、静态变量都是放在主内存中,属于所有线程共享区域,所以存在线程之间安全问题。

主要是存储局部变量(方法内部的变量,存储着主内存中变量的副本),每个线程只能在自己的工作内存中操作变量副本,对其他线程是不可见的。就算两个线程同时执行同一段代码,也是都在自己的工作内存中对变量进行操作。由于线程的工作内存是私有,所以线程之间是不可见的,同时也是线程安全

JMM三大特性:原子性、可见性、有序性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程给打断。

在java中,对基本的数据类型的操作都是原子性的操作,但是要注意的是对于32位系统的操作对于long、double类型的并不是原子性操作(对于基本数据类型,byte,short,int,float,boolean,char读写是原子操作)。因为对于32位的操作系统来说,每次读写都是32位,而doubel、long则是64位存储单位。就会导致一个线程操作完前面32位后,另一个线程刚好读到后面的32位,这样一来一个64位被两个线程分别读取。

可见性指的是当一个共享变量被一个线程修改后,其他线程是否能够立即感知到。对于串行执行的程序是不存在可见性,当一个线程修改了共享变量后,后续的线程都能感知到共享变量的变化,也能读取到最新的值,所以对于串行程序来讲是不存在可见性问题。

对于多线程程序,就不一定了,前面分析过对于共享变量的操作,线程都是将主内存的变量copy到工作内存进行操作后,在赋值到主内存中。这样就会导致,一个线程改了之后还未回写到主内存,其余线程就无法感知到变量的更新,线程之间的工作内存是不可见的。另外指令重排序以及编译器优化也会导致可见性的问题

有序性是指对于单线程的代码,我们总是认为程序是按照代码的顺序进行执行,对于单线程的场景这样理解是没有问题,但是在多线程情况下, 程序就会可能发生乱序的情况,编译器编译成机器码指令后,指令可能会被重排序,重排序的指令并不能保证与没有排序前的保持一致