一、运行时数据区
Java虚拟机在运行Java程序的时候,会把内存区域划分为多个区域,称为运行时数据区。
jdk1.8以前

jdk1.8后

线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存 (非运行时数据区的一部分)
程序计数器
程序计数器是用来记录当前线程的执行位置,以便于从别的线程切换回来时,能回到正确的位置,继续完成剩余的操作。同时,字节码编译器会通过改变程序计数器来依次执行指令,从而实现代码的流程控制。
程序计数器不会产生StackOverflowError和OutOfMemonyError
虚拟机栈
虚拟机栈是描述Java方法执行的内存模型,每次方法执行的数据都是通过栈传递的,每有一个方法执行,就会有一个栈帧压入栈中,方法执行完毕后就会弹出,栈帧存储着方法执行的各种数据(局部变量表、操作数栈、动态链接等)。
方法执行return语句或者抛出异常都相当于方法执行结束,栈帧会弹出。
Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。
StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。OutOfMemoryError: Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
本地方法栈
本地方法栈与虚拟机栈基本相同,本地方法栈执行的是本地(Native)方法,每执行一个本地方法都会压入一个栈帧,执行结束后弹出,也有可能会抛出StackOverflowError和OutOfMemonyError。
堆
内存中堆和栈的区别:
堆是存储的单位,栈是运行时的单位,栈是解决程序运行时的问题的,如何处理数据,堆是解决数据的存储问题,如何存放数据,为数据分配内存。
Java堆是垃圾回收的主要区域,所以为了方便垃圾回收,更好地回收和分配内存,将堆分成了新生代和老年代,即分代垃圾收集算法.
jdk1.7中堆分为新生代、老年代和永久代

jdk1.8去除了永久代,取而代之的是元空间(元空间用的是本地内存)

对象在堆中的分配过程
- 一个新的对象先在Eden区创建,当Eden放满时,此时又要添加新的对象,就会进行垃圾回收,清除不再需要的对象,剩余对象转到S0,然后Eden中再添加新的对象。
- 在S0中,当再次触发垃圾回收时,S0中未被回收的对象就会转到S1中。
- 在S1中,又触发垃圾回收的话,S1中未被回收的对象就会转到S0中。
- 但一个对象重复3、4步骤15次,就可转入老年代(默认是15次,可通过
-XX:MaxTenuringThreshold来设置。)。 - 当老年代满了之后,就会对老年代进行垃圾回收,若对老年区进行垃圾回收后仍不能保存对象,就会抛出OOM异常OutOfMemonyError。
方法区
方法区是用来存储虚拟机加载的类信息、常量、静态变量等数据。
方法区可以看作是独立于Java堆的内存空间。
在jdk1.8之前,方法区主要是由永久代实现,jdk1.8之后,用元空间来取代永久代,二者的区别就在于,永久代使用虚拟机分配的内存,容易抛出OOM异常,而元空间用的是本地内存,内存溢出的几率很小。
运行时常量池
运行时常量池是方法区的一部分,存储常量池,用于存放编译器生成的各种字面量与符号引用。
常量池可以看作是一张表,虚拟机指令会根据这张表找到要执行的类名,方法名、参数类型,和字面量等。
常量池是class文件的一部分,在类加载后存放到方法区的运行时常量池中。
直接内存
直接内存也称为堆外内存,是一块分配在JVM堆外的内存,所以这部分内存不是用虚拟机来管理的,而是用操作系统来管理;Java通过DriectByteBuffer对其进行操作,避免了在Java堆和Native堆之间来回复制数据,提高性能。
Java创建对象的过程
- 类加载检查:当虚拟机遇到一条new指令时,会根据该指令的参数检查是否能在常量池中定位到一个类的符号引用,并检查引用代表的类是否有被加载过、解析和初始化,如果没有,就先执行类加载。
- 分配内存:类加载完成后,接下来就会对象分配内存,
分配方式有:
指针碰撞:适用于堆内存规整的情况,在内存中用一个指针作为分界指示器,使用过的内存放在一边,没使用过的放在另一边,分配内存时通过移动指针来分配。
空闲列表:适用于堆内存不规整的情况,虚拟机必须维护一个列表来记录哪些内存可用,哪些内存不可用,分配内存时在列表中进行划分并更新列表。
分配内存时保证线程安全的方式:
CAS+失败重试:分配内存时采用CAS机制,加上失败重试来保证内存更新的原子性。但该方式效率不高,一般不优先用这种。
TLAB:每个线程预先在Java堆中分配一小块内存,当有对象要分配内存时,先用这一小块内存,这一小块用完时,再用上面的方法。
- 初始化零值:分配完内存后,将对象初始化为零值。
- 设置对象头:设置对象头,类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。
- 执行init()方法:此时虚拟机创建对象已完成,转到Java程序的层面上来,继续执行init()。
对象的内存布局
对象的内存布局可分为3部分:对象头、实例数据和对齐填充。
对象头分为两部分:一部分是对象运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。
实例数据就是对象真正存储的有效信息,也就是程序中所定义的各种类型的字段内容。
对齐填充起占位作用,因为HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,当不是8的倍数时,就要进行对齐填充。
本文详细解读Java虚拟机的运行时数据区,包括线程私有区(程序计数器、虚拟机栈、本地方法栈)、共享区(堆、方法区、直接内存),以及对象的内存布局和创建过程。重点讲解了内存分配策略和可能出现的错误类型。
1100

被折叠的 条评论
为什么被折叠?



