原文链接:
https://blog.csdn.net/copy55/article/details/81208768
目录
一. java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。java虚拟机所管理的内存将会包括以下几个运行时数据区域。
常量池
直接内存
2、对象的创建
为对象分配内存空间:
问题:对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改了一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象a分配内存,指针还没来得及修改,对象b又同时使用了原来的指针来分配内存的情况。
3.对象的内存布局:
4.对象的访问定位
————————————————————————————
一. java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。java虚拟机所管理的内存将会包括以下几个运行时数据区域。
1.程序计数器:
-
它可以看作是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。
-
每个线程都拥有一个程序计数器,在线程创建时创建。
-
但是如果正在执行的是Native方法,这个计数器值则为空。
-
这个区域是唯一一个在java虚拟机规范中没有规定任何outofMemoryError情况的区域。
2.虚拟机栈:
-
与程序计数器一样是线程私有的,它的生命周期与线程相同。
-
虚拟机栈描述的是java方法执行的内存模型。
-
每个栈由一系列栈帧组成,每一个栈帧保存着一个方法的局部变量表,操作数栈,动态链接,方法出口等信息。
-
每一个方法的调用和执行完成,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
(局部变量表存放了编译期可知的各种基本数据类型,对象引用(reference类数据)和returnAddress类型(指向一条字节码指令的地址),局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的)。
注意: 如果线程请求在栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果扩展是无法申请到足够的内存,就会抛出outofMemoryError异常。
3.本地方法栈:
与虚拟机栈作用相似,区别是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机的native方法服务。
4.java堆:
-
java堆是虚拟机所管理的内存中最大的一块,
-
是所有线程共享的一块内存区域,
-
所有的对象实例以及数组都要在堆上分配,
-
gc的主要区域,对于分代gc来说堆也是分代的。(基于gc算法java堆可以细分为:新生代和老年代,再仔细一点有Eden空间,From Survivor空间,To Survivor空间等等)。
-
如果在堆中没有内存完成实例分配,并且堆也无法在扩展时,将会抛出outofMemoryError异常。
5.方法区:
-
所有线程共享的。
-
用于存储已被虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码等数据。
-
通常和永久代关联在一起。这个区域的内存回收主要是针对常量池的回收和对类型的卸载。
常量池:
用于存放编译期生成的各种字面量和符号引用 ,这部分内容在类加载后进入方法区的运行时常量池中存放
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但是这部分内存也被频繁使用,二期也可能导致outofmemoryError异常出现)
JDK1.4 中新加入了NIO类(New Input/Output):
-
引入了一种基于通道(channel)与缓冲区(buffer)的I/O方式
-
它可以使用native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
2、对象的创建
-
虚拟机遇到一条new指令时,首先将去检查该指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,那必须先执行相应的类加载过程。
-
在类加载检查通过之后,接下来虚拟机为新生对象分配内存。新生对象所需内存的大小在类加载完成之后便可完全确定。然后为对象分配内存空间。
-
内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为0值(不包括对象头),如果使用本地线程分配缓冲(TLAB),这一工作过程也可以提前至分配时进行,接下来,对对象进行必要的设置,例如这对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希吗,对象的GC分代年龄等信息。这些信息存放在对象头中。
为对象分配内存空间:
-
指针碰撞的方式:假设java堆中内存是绝对规整的,所用用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
-
空闲列表:如果java堆中的内存并不是规整的,已使用的内存和空间的内存相互交错,那就没办法简单进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
使用serial parnew 等收集器时,系统采用的是指针碰撞的分配方式,而CMS这种基于标记清除的算法,常采用空闲列表。
问题:对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改了一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象a分配内存,指针还没来得及修改,对象b又同时使用了原来的指针来分配内存的情况。
解决方案:
-
是对分配内存空间的动作进行同步处理——实际上虚拟机采用了CAS(compare and sweep)配上失败重复的方式保证更新操作的原子性。
-
把内存分配的动作按照线程划分在不同的空间之中进行,每一个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲,哪个线程要分配内存,就在哪个线程的本地线程分配缓冲上分配。使用参数 -XX:+/-UserTLAB来设定是否使用TLAB
3. 对象的内存布局:
对象在内存中存储的布局可以分为3块区域:对象头,实例数据和对其填充。
-
对象头:第一部分用于存储对象自身的运行时的数据,如哈希码,Gc分代年龄等。另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,如果是数组还记录数组长度的数据。
-
实例部分:是对象真正存储的有效信息,也是在程序代码中定义的各种类型的字段内容。
-
对其填充:不是必然存在的,它仅仅起着占位符的作用。(对象的大小必须是8字节的整数倍)
4.对象的访问定位
java程序通过栈上的reference数据来操作堆上的具体对象,主流有两种访问的方式:
- 使用句柄:java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例指针与类型数据的具体地址。(优点:对象移动了,只会改变句柄中的实例数据指针)
- 使用直接指针访问:reference中存储的直接就是对象实例的地址。(速度快,节省一次指针定位的开销)