把csdn上的东西转移过来,原文也是我自己:https://blog.csdn.net/w635614017/article/details/65935062
(主要是参考了书籍《深入理解Java虚拟机》)
虚拟机:运行时数据区域
1.程序计数器:
线程私有,是一块较小的内存空间,看成是当前线程所执行的字节码行号指示器。多线程是通过线程轮转分配处理器执行时间的。为了线程切换之后能恢复到正确的执行位置,每个线程都需要一个行号指示器。
2.虚拟机栈:
线程私有,生命周期与线程相同,每个方法执行的时候回创建一个栈帧,储存局部变量表、操作数栈、动态链接、方法出口balabala...每个方法从调用直到执行完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。
3.本地方法栈:
与虚拟机栈作用相似,区别:虚拟机栈执行java方法,本地方法栈执行Native方法。有的虚拟机会把他俩合二为一。
4.java堆:
线程共享,是java虚拟机管理的内存的最大一块,唯一目的:存放对象实例。规范:所有对象和数组都要在堆上分配。堆是java垃圾回收机制的重要区域(GC堆:Garbage Collected Heap)分为新生代和老年代。
5.方法区:
与堆一样,线程共享。储存已被虚拟机加载的类信息、常量、静态变量、代码balabala...有个名字:非堆Non-Heap。并非数据进入方法区就如同永久代的名字!记住!
6.运行时常量池:
是方法区的一部分。用于存放编译器生成的各种字面量和符号引用,这些内容将在类加载后进入方法区的运行时常量池存放。重要特征是有动态性。
7.直接内存:
不是虚拟机运行时数据区的一部分,但是频繁被使用,容易导致OutOfMemoryError异常出现。
对象的创建
为了方便理解,我画了张图:
对象的创建过程
过程是这个样子滴:
第一步:首先接收到new指令之后会去检查这个指令能否在常量池中定位到一个类的符号引用,然后再检查其是否被加载过。如果没有加载过,需要先加载,然后进入下一步。
第二步:为新生对象分配内存。有两种方式——1.指针碰撞 2.空闲列表 ,选用哪种方式是通过堆中的内存是否规整决定的。如果堆中的内存规整,选择指针碰撞。如果堆中内存不规整,则选用空闲列表。
指针碰撞:
假设堆中的内存是规整的,那么我们可以看做分配过的内存在一侧,未被分配的内存在另一侧。指针作为中间分界点的指示器,那么分配内存的过程就变成了指针向空闲一侧挪动,挪动对象大小相等的距离即可。
空闲列表:
假设堆中内存不规整,那么已分配和未分配的内存相互交错,没有办法简单的进行指针碰撞。那么虚拟机会维护一个列表,这个列表上记录了哪些内存块可用,哪些内存块不可用。分配的时候,从列表中找到一块可以容纳的空间划分给对象即可。
在分配期间,我们需要考虑的不仅仅是分配方式,还有线程安全问题——当正在给一个对象分配内存的时候,指针还没有来得及修改,另一个对象又使用了原来的指针去分配内存,怎么办?
有两个方案:1.对分配空间的动作,进行同步处理。2.把内存分配的动作,按照线程划分到不同空间去进行。
第三步:将内存空间初始化为零值。保证对象的实例字段不赋初值就可以直接使用。
第四步:虚拟机对对象进行必要的设置。
在虚拟机看来,经过了这四步,新的对象已经产生了,但是!Java程序需要执行init方法,所以在执行new指令之后,紧接着就会执行init方法,去初始化。
对象的内存布局
对象的内存布局对象的访问定位
Q:对象创建已经完成,如何访问对象呢?
A:通过栈上的reference数据,操作堆上的具体对象。
两种方式:
1.使用句柄
方式:Java堆会划分出来一块内存作为句柄池,reference储存对象的句柄地址。句柄中有实例数据和类型数据具体信息。
优势:reference储存的是稳定的句柄地址,当对象移动时,只改变句柄中的实例数据指针,reference本身不变。
2.直接指针
方式:堆对象的布局中需要考虑如何设置访问类型数据的信息。reference储存的直接就是对象地址。
优势:速度快。
网友评论