JVM-内存管理01-(运行时数据区域)

本文深入探讨Java内存区域,包括程序计数器、虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。阐述对象创建过程,涉及对象头、实例数据和对齐填充的内存布局,以及对象访问定位。同时,讨论了Java如何通过自动内存管理和垃圾收集避免内存泄漏和溢出问题。
摘要由CSDN通过智能技术生成

目录

动态内存分配

1. 程序计数器     

2. Java虚拟机栈

3. 本地方法栈

4. Java堆

5. 方法区

6. 运行时常量池

对象的创建与内存布局

1. 对象的创建

2. 对象的内存布局

3. 对象的访问定位

        Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

        ——《深入理解Java虚拟机》

        与C++不同的是,Java虚拟机提供了内存自动管理机制,使得Java程序员不必过于担心内存泄漏内存溢出问题,也不需要像C++一样手动管理内存,JVM替程序员完成了这些工作。

动态内存分配

        Java虚拟机JVM在执行Java程序时会将其所管理的内存空间划分为几个不同的区域,每个区域有各自的用途、生命周期、与线程依赖关系。主要包括了程序计数器、本地方法栈、虚拟机栈、堆、方法区5个部分。

1. 程序计数器     

        程序计数器是一块较小的内存空间(线程私有的,不允许其他线程访问),可以看作当前线程所执行的字节码的行号指示器。字节码解释器工作时,通过改变程序计数器的值选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖程序计数器完成。

       在Java虚拟机中,多线程是通过线程轮流切换、分配处理器执行时间的方式实现的。在任何一个确定的时刻,一个处理器(对于多核处理器来说就是一个内核)都只会执行一条线程中的指令。

        为了线程切换后能恢复到正确的执行位置,每个线程都需要有独立的程序计数器。由于每个线程的程序计数器是独立存储的,因此各线程之间的程序计数器互不影响,这类内存区域被称为线程私有的内存区域

        注意:程序计数器只对Java方法生效(只有Java方法才会入栈执行,而程序计数器与虚拟机栈搭配使用),当处理器正在执行本地(Native)方法,这个计数器的值为空程序计数器是唯一一个不会出现OutOfMemoryError情况的区域。

2. Java虚拟机栈

        Java虚拟机栈是线程私有的,是除非Native方法外的其他方法执行的区域,其生命周期与线程相同。因此,开辟Java虚拟机栈的方法有三种(本质上都是通过开辟线程让JVM分配虚拟机栈):执行main()方法、执行实现Runnable接口的run()方法、执行实现Callable接口的call()方法。

          虚拟机栈描述的是Java方法执行的线程内存模型:每开辟一个线程,JVM就会有分配一个虚拟机栈(栈底方法为main()、run()、call()方法),每个方法被调用时,JVM都会同步创建一个描述该方法的栈帧(包括局部变量表、操作数栈、动态链接等)。每一个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

       JVM中对栈内存区域规定了两类异常情况:当线程请求的栈深度大于虚拟机所允许的深度(在用一个线程中调用了太多方法),将抛出StackOverflowError异常;如果JVM允许Java虚拟机栈内存动态扩展,当栈扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。此外,如果开辟了太多的线程以至于无法申请到足够的内存时也会抛出OutOfMemoryError异常。

3. 本地方法栈

        本地方法栈与Java虚拟机栈类似,也是线程私有的,区别在于Java虚拟机栈是提供给Java方法(字节码)使用的,而本地(Native)方法栈是提供给Native方法服务的。与Java虚拟机栈类似,本地方法栈也会在栈深度溢出与栈扩展失败时分别抛出StackOverflowError与OutOfMemoryError异常。

在JDK的内置虚拟机HotSpot中,将Java虚拟机栈与本地方法栈合并在了一起。

4. Java堆

        Java堆是JVM管理的内存中最大的一块区域,其是所有线程共享的在虚拟机启动时创建主要用于存放Java类的对象/实例(所有的对象实例与数组都在Java对上分配内存空间)。

        Java堆是垃圾收集器(GC)管理的内存区域,GC对Java堆进行了细分以便于回收某些对象的内存空间。

如果在Java堆内存中没有完成实例空间的分配,并且堆也无法再拓展时,Java虚拟机将会抛出OutOfMemoryError异常。

5. 方法区

        方法区用于存储被虚拟机(其中的类加载器)加载的类型信息、常量、静态变量、编译器编译后的代码缓存(.class字节码文件),方法区是线程共享的

 如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

6. 运行时常量池

        Class字节码文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中,运行时常量池是线程共享的。        

        常量池避免了频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

        常量池主要包括字面量符号引用两部分: 

 常量池无法满足新的内存分配需求时(方法区的一部分),将抛出OutOfMemoryError异常。

7. 直接内存

        直接内存不是虚拟机运行时数据区域的一部分,也不是虚拟机规范中定义的内存区域,但是这部分也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现。

本机直接内存的分配不受到 Java 堆大小的限制,但是直接内存仍然受到本机总内存地大小及处理器寻址空间的限制。如果各个内存区域的总和大于物理内存限制,就会导致动态扩展时出现 OutOfMemoryError 异常。

对象的创建与内存布局

        以JDK内置虚拟机HotSpot和最常用的内存区域Java堆为例,深入探讨一下HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。

1. 对象的创建

        Java中创建对象的方式有两种:1)通过new关键字在堆中开辟内存空间并调用构造方法初始化;2)实现Cloneable接口并在类中重写clone()方法(直接调用Object类中的clone)通过浅拷贝创建对象。

        当JVM遇到一条字节码指令new时,1)首先会去检查这个指令的参数是否定位到了一个类的符号引用,并检查这个类是否被加载解析和初始化过,没有需要先执行相应的类加载过程;2)对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值并进行必要的设置(对象头);3)new指令之后会继续执行构造函数(即Class文件中的<init>()方法),完成对象的初始化

        

2. 对象的内存布局

        对象在Java堆内存中的存储布局可以分为三个部分:对象头(Header)、实例数据(Instance)、对齐填充(Padding).

        2.1 对象头

                HotSpot虚拟机对象的对象头部分包括两类信息:

                1)Mark Word:用于存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁等;通常大小为32/64位。

                2)类型指针:即对象指向其类型元数据(类信息)的指针,JVM通过类型指针明确该对象是哪个类的实例。

                注意:如果对象是一个Java数组,那么在对象头中还必须有一块用于记录数组长度的数据。

        2.2 实例数据:

                实例数据部分是对象真正存储的有效信息,即程序中定义的各种类型的字段内容。

        2.3 对齐填充:

                由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即任何对象的大小必须为8字节的整数倍,如果对象的实例数据部分没有对齐的化,就需要对齐填充来补全(对象头刚好是8字节的整数倍)。

        

3. 对象的访问定位

        Java程序主要是通过栈上的reference数据(指向对象的引用)来操作堆上的具体对象,HotSpot虚拟机采用直接指针访问,reference中存储的直接就是对象的地址,其好处在于速度跟快,开销更小。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值