谈一谈Java内存区域和Java内存模型的理解? / Java内存区域和Java内存模型是一个东西吗?
Java内存区域和Java内存模型不是一个东西!!!!!
Java内存区域,也就是Java运行时数据区域。是指Java虚拟机在运行时创建的一个内存区域,用于存储Java程序运行时所需要的数据结构和对象实例。Java运行时数据区包括堆、方法区、虚拟机栈、本地方法栈和程序计数器等部分。
Java内存模型,也就是JMM,定义了程序中各个变量的访问规则,在虚拟机中将变量存储到内存和从内存中取出变量这种底层细节。
JMM
JMM(Java Memory Model)是Java虚拟机规范中定义的一种内存模型,它描述了Java程序如何在多线程环境下访问共享内存。JMM主要是为了屏蔽各种硬件和操作系统对内存访问的差异而定义出来的内存模型。JMM定义了一个抽象的计算机内存模型,包括主内存和工作内存两部分。
-
主内存
主内存是所有线程共享的内存区域,也是Java内存模型中的核心部分。主内存中保存着Java对象的实例数据、类信息、方法等。
在多线程环境下,当一个线程修改了主内存中的共享变量时,其他线程并不会立即看到这个变量的修改。这是因为每个线程都有自己的工作内存,与主内存之间存在缓存数据不一致的问题。 -
工作内存
每个线程都有自己的工作内存(Thread Local Memory),它是线程私有的内存区域。工作内存中保存着该线程使用到的共享变量的副本拷贝。
当一个线程需要使用某个共享变量时,它会首先从主内存中读取该变量的值到自己的工作内存中,并对它进行操作。操作完成后,该线程再将变量的值写回主内存中。在这个过程中,其他线程并不能直接访问到该线程的工作内存。 -
内存交互操作
Java内存模型还定义了一些内存交互操作,包括lock、unlock、read和write等。这些操作可以保证多线程环境下共享变量的可见性和一致性。- lock和unlock:用于对共享变量进行加锁和解锁,确保同一时刻只有一个线程可以访问该变量。
- read:用于将工作内存中的值传递到主内存中。
- write:用于将主内存中的值传递到工作内存中。
-
内存屏障
Java内存模型还定义了内存屏障(Memory Barrier),用于控制内存交互操作的顺序和可见性。
内存屏障分为四种类型:- LoadLoad屏障:保证load指令之前的所有load指令已经执行完毕。
- StoreStore屏障:保证store指令之前的所有store指令已经执行完毕。
- LoadStore屏障:保证load指令之前的所有指令都已经执行完毕,并且能够读取到最新的变量值。
- StoreLoad屏障:保证store指令之前的所有指令都已经执行完毕,并且该指令所写入的变量值对于其他线程可见。
JMM的作用
保证多线程环境下的数据可见性、原子性和有序性。通过JMM规定的规范,我们可以确保多线程的正确性和可靠性。
synchronized、volatile、Lock等关键字和API有什么作用?
这些关键字和API是Java多线程编程中用来实现同步的机制,用于保证多线程环境下共享变量的可见性、有序性和原子性。其中synchronized关键字用于实现悲观锁机制,volatile关键字用于实现轻量级同步机制,Lock接口用于实现更加灵活的锁机制。
Java内存区域
1.8之前
1.8及之后
字符串常量池?
字符串常量池 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cpp ,StringTable 本质上就是一个HashSet ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置)。
StringTable 中保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象。
JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
什么是直接内存?
直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
JDK1.4 中新加入的 NIO(Non-Blocking I/O,也被称为New I/O),引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
说一说HotSpot 虚拟机在 Java 堆中对象分配过程?
-
类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 -
分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 -
初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 -
设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 -
执行init方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
虚拟机内存分配的两种方式?
-
指针碰撞
- 适用场合:堆内存规整(没有内存碎片)的情况下
- 原理:用过的内存全部整合到一边,没有用过的内存放到另一边,中间一个分解指针,只需要想着没用过的内存方向将该指针移动对象内存大小即可。
- 使用该分配方式的GC收集器:Serial,ParNew
-
空闲列表
- 适用场合:堆内存不规整的情况下
- 原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录
- 使用该分配方式的 GC 收集器:CMS
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。
说一说对象创建中不安全的现象? / 如何解决线程不安全的问题?
对象创建在虚拟机中是非常频繁的行为, 即使仅仅修改一个指针所指向的位置, 在并发情况下也并不是线程安全的, 可能出现正在给对象A分配内存, 指针还没来得及修改, 对象B又同时使用了原来的指针来分配内存的情况。
一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性; 另外一种是把内存分配的动作按照线程划分在不同的空间之中进行, 即每个线程在Java堆中预先分配一小块内存, 称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB) , 哪个线程要分配内存, 就在哪个线程的本地缓冲区中分配, 只有本地缓冲区用完了, 分配新的缓存区时才需要同步锁定。 虚拟机是否使用TLAB, 可以通过-XX: +/-UseTLAB参数来设定。
对象的内存布局?
在HotSpot虚拟机里, 对象在堆内存中的存储布局可以划分为三个部分: 对象头(Header) 、 实例数据(Instance Data) 和对齐填充(Padding) 。
HotSpot虚拟机对象的对象头部分包括两类信息。 第一类是用于存储对象自身的运行时数据, 如哈希码(HashCode) 、 GC分代年龄、 锁状态标志、 线程持有的锁、 偏向线程ID、 偏向时间戳等, 这部分数据的长度在32位和64位的虚拟机(未开启压缩指针) 中分别为32个比特和64个比特, 官方称它为“Mark Word”。
对象头的另外一部分是类型指针, 即对象指向它的类型元数据的指针, Java虚拟机通过这个指针来确定该对象是哪个类的实例。
接下来实例数据部分是对象真正存储的有效信息, 即我们在程序代码里面所定义的各种类型的字段内容, 无论是从父类继承下来的, 还是在子类中定义的字段都必须记录起来。文章来源:https://www.toymoban.com/news/detail-414858.html
对象的第三部分是对齐填充, 这并不是必然存在的, 也没有特别的含义, 它仅仅起着占位符的作用。 由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍, 换句话说就是任何对象的大小都必须是8字节的整数倍。 对象头部分已经被精心设计成正好是8字节的倍数(1倍或者
2倍) , 因此, 如果对象实例数据部分没有对齐的话, 就需要通过对齐填充来补全。文章来源地址https://www.toymoban.com/news/detail-414858.html
到了这里,关于我的面试八股(JVM篇)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!