运行时数据区域

Java虚拟机管理的内存包括以下几个运行时数据区域
(图片来自《深入理解JVM虚拟机》)
在这里插入图片描述
程序计数器

就是当前线程所执行的字节码的行号指示器,标记了当前线程执行到了哪一行指令,是为了确保线程切换之后能恢复到刚才执行的位置,每个线程都会有一个独立的程序计数器,是线程私有的内存;如果当前执行的是Java方法,那么该计数器指向的就是一个字节码指令地址,如果是Native方法,那么该计数器的值就是空,在这个区域不会报出OutOfMemoryError异常

Java虚拟机栈

线程私有的,生命周期与线程相同;每一个Java方法执行的时候,都会自动创建一个栈帧,里面存放局部变量表,操作数栈,动态链接,方法和接口信息等等,方法的调用和执行结束分别对应着栈帧在虚拟机栈中的入栈和出栈的过程

其中,局部变量表存放了编译器可知道的各种基本数据类型,对象引用和returnAddress类型,总之,他所需要的内存空间在编译期间完成分配,进入这个方法的时候,局部变量表所需要的内存空间就是完全确定了的,运行期间也不会改变局部变量表的大小

这部分可能会报出两种异常,stackOverflowError请求的栈深度大于虚拟机栈允许的深度;当前大部分都是可拓展的,也有允许限制最大深度的,当拓展时没办法申请到更多的内存会抛出OutOfMemoryError

本地方法栈

与Java虚拟机栈非常相似,不过一个是为执行Java方法(字节码)服务,一个是为执行Native方法服务;这里简单解释一下Native方法,是以Native关键字生命,不提供函数体,以C/C++编写遵循Java本地接口规范(JNI);这部分也会抛出StackOverflowError和OutOfMemoryError

Java堆

这部分是所有线程所共享的一块内存区域,虚拟机启动的时候创建。这部分区域就是为了存放对象的实例,也是垃圾收集器主要管理的区域;

这部分区域当然也有一些划分方法,比如说他可被细分为:新生代和老年代;在细致一点它被分为:Eden空间、From Survivor空间、To Survivor空间等。线程共享的Java堆中也可能会划分出多个线程私有的缓冲区;当然,不论怎么划分,只是为了更好的回收和分配,存储的任然都是对象实例,具体的一些回收算***在后面提及

方法区

也是线程共享的一块内存区域,存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机规范中把方法区描述为堆的一部分,但是这部分也有一个说法叫“永久代”。当然并不是说这部分的东西会永远存在,这区域的内存回收主要是对常量池的内容进行回收和类的卸载,然而因为条件相当苛刻,所以回收成果比较难令人满意。当方法区无法满足内存分配需求时会抛出OutOfMemoryError异常

运行时常量池

在上面的方法区也提到了,运行时常量池是属于方法区的一个部分,用来存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。这部分空间是具备动态性的,Java语言不要求常量一定是在编译器才能产生,运行期间也可将新的常量放入池中,比如说String的intern()方法

HotSpot 虚拟机为例

对象的创建

当遇到了一个new指令时,首先去检查常量池中,定位到一个类的符号引用,并且检查这个引用多代表的类是不是被加载、解析、初始化过,如果没有,那么就要先执行类的加载过程

然后虚拟机将开始为新生对象分配内存空间。需要多少空间在类加载完成之后便可以确定下来,其实就是在Java堆中划一块地方给这个对象,分配的方法可因为内存是否规整分为两种,内存是否规整要看采用的垃圾回收器是执行哪种回收算法来处理,这些也会在后面提到

指针碰撞 :假如说Java堆中的内存是严格工整的,已经使用了的在一遍,还未使用的在另一边,中间一个指针来标明。分配的时候把当前指针向空闲空间方向挪动需要的内存大小个距离。

空闲列表:Java对中的内存不是严格工整的,就需要虚拟机维护一个列表,来记录那些内存块是可用的,分配的时候找到一个足够大的空间划分给对象实例,并且更新这个列表。

分配内存的时候要考虑到并发的问题,有可能在给对象A分配内存还没来得及更改指针的时候,对象B使用了原来的指针分配内存。解决这个问题的方法其一是让操作原子化,进行同步处理。其二是为每一个线程预先分配一小块内存空间(本地线程分配缓冲 TLAB)那个线程要分配内存,先在自己的TLAB上分配,用完了需要分配新的TLAB时进行同步锁定。

内存分配后虚拟机会将分配到的内存空间初始化为零值(不包括对象头),如果使用TLAB,这个工作也可在TLAB分配完之后做,就是分配好屋子之后先打扫干净在入住

然后是对象头的相关设置,从虚拟机的角度来说,现在已经创建了一个对象,但是这还不是我们想要的对象,他还得按着我们的意愿进行一系列的初始化操作,一般来说在new指令执行之后会紧接着执行init方法

对象的内容布局和访问定位
对象在内存中的布局一般可分为:对象头、实例数据、对齐填充

建立了对象是为了使用,访问堆中的对象一般有两种方式
(图片来自《深入理解JVM虚拟机》)
1:通过句柄来访问对象,好处是reference中存放稳定的句柄地址,对象被移动的时候只会改变句柄中的示例数据指针,reference本身不需要修改
在这里插入图片描述
2:直接指针访问,直接存储对象的地址,好处是访问速度更快,相比句柄访问的方式少了一次指针定位。
在这里插入图片描述
小结:
1、运行时数据区域
2、创建对象内存的分配,初始化;对象内存中的布局和定位