【温故知新-Java虚拟机篇】1.内存模型

来源:互联网 发布:python高级编程第二版 编辑:程序博客网 时间:2024/06/13 23:04

该系列博客暂且定义为《深入理Java解虚拟机》的笔记,有些坑等后续看完书再填,有不对的地方多指教

以下的图片均来自本书。

1.运行时数据区

运行时数据区主要由虚拟机栈、本地方法栈、堆、方法区以及程序计数器组成。如下图所示:

其中线程私有的有:虚拟机栈、本地方法栈、程序计数器

线程共享的有:堆、方法区

1)程序计数器(Program Counter Register,记录目前线程所执行的指令信息):

a.是当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一个执行的字节指令,如分支、跳转、循环、异常处理、线程恢复等基础功能都需要依赖它来完成。

b.由于Java多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的(即计算机操作系统中的调度),同一时刻,一个内核只会执行一条线程中的命令,因此为了线程切换回来的时候,能够正常的继续执行,每个线程都有一个独立的程序计数器,来存储该线程所应该执行的命令行号,即程序计数器是私有的。

2)Java虚拟机栈(Java Virtual Machine Stack,主要存储局部变量的引用,即对象地址,方法执行中所需要的东西):

a.虚拟机栈也是线程私有的,它的生命周期与线程相同。

b.虚拟机栈描述的是Java方法的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数表栈、动态链接、方法出口等信息。

c.其中局部变量表存放了

1.编译期可知的各种基本数据类型(boolean、byte、char、shot、int、float、long、double)

2.对象引用(reference类型)

3.returnAddress(指向了一条字节码指令的地址)

4.局部表所需要的内存空间在编译期完成分配,

d.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出栈溢出异常(StackOverflowError),即方法嵌套调用层数过多,如递归

f.如果虚拟机栈可以动态扩展(目前大多数都支持),如果无法申请到足够的内存,则抛出内存溢出异常(OutOfMemoryError)

g.jdk5.0后,默认的栈大小为1M,经过测试大概可支持深度为11000+(无限递归),可以通过参数-Xss10M 来修改大小。

3)本地方法栈(Native Method Stack,与虚拟机栈类似,主要存储本地方法的相关数据):

a.与虚拟机栈一样,区别为虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native(简单理解为本地其他语言方法库)方法服务。

b.有些虚拟机的实现贾昂本地方法栈与虚拟机栈合二为一,如Sun Hotpot虚拟机

c.本地方法栈也会抛出StackOverflowError和OutOfMemoryError

4)Java堆(Java Heap,对象实例真正存储的地方):

a.Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

b.几乎所有的对象实例都放在堆上。

c.Java堆是垃圾收集管理器的主要区域,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为新生代和老年代一个对象刚生成时,在新生代,每次逃过一次新生代的垃圾回收,对象年龄+1,待年龄超过一定值(一般默认15岁),就会被放入老年代。下一章会将垃圾回收。

d.可以使用  jvisualvm命令来查看目前的JVM堆大小设置,可以通过参数-Xms512m设置堆初始大小,参数-Xmx1g设置堆最大值。

4)方法区(Method Area,存储常量、静态变量、类信息、类对象的地方:

a.方法区与堆一样,也是线程共享的内存区域。

b.方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。

c.方法区也被称为“非堆”内存,在HotSpot虚拟机上,被称为“永久代”,这样垃圾收集器可以像管理堆一样去管理方法区,但新版本(1.7之后)的HotSpot已经开始逐步采用Native Memory的实现方法区,1.7已经把字符串常量移出。

d.非堆内存可以通过-XX:PermSize=64MB设置初始化大小,通过XX:MaxPermSize=256MB设置最大大小

5)运行时常量池(Runtime Constant Pool):

a.运行时常量池是方法区的一部分,用于存放编译期生成的各种字面常量和符号引用,这部分内容将在类加载后进入方法去运行时存放。

 6)直接内存(Direct Memory):

a.直接内存是服务器的实际内存,不属于虚拟机。

b.JDK1.4加入了NIO(New IO)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了Java堆和Native堆中来回复制数据。

2.对象创建过程:

1)虚拟机遇到一条new指令时,首先检查这个指令的参数能否在常量池中定位一个类的符号引用。

2)然后检查这个符号引用代表的类是否已被加载、解析和初始化过。

3)如果没有,那么执行类加载过程(挖坑,后续填)。

4)在Java堆中划分一块确定大小的内存,划分内存方法有

a)对于规整内存的“指针碰撞”方法,即指针指向第一块空闲内存(如地址a),分配后,指针指向地址a+对象大小。

b)对于使用内存和空闲内存是交错时,则使用“空闲列表”来记录空闲内存,然后分配。

c)内存是否规整,是由垃圾收集器是否带有压缩整理功能决定。

d)解决内存分配的线程安全问题:

1.采用同步方法,即所有内存分配为线性——实际上虚拟机采用CAS(Compare And Swap)配上失败重试来保证原子性

先挖坑,之后填)。

2.每个线程在创建时预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),线程需要分配内存时

,在缓冲中分配,缓冲不够时才使用锁。

5)将内分配到的内存都初始化为零值。

6)虚拟机对对象做必要的设置,如对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。

这些信息存放在对象的对象头(Object Header)中。

3.对象的内存布局

都在HotSpot中,对象在内存中存储可以分为3块区域:对象头(Object Header)、实例数据(Instance Data)和对其填充(Padding)

1)对象头,主要包含两个部分:

a)用于存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程只有的锁、偏向线程ID、偏向时间戳等。

b)另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

c)如果对象为数组,那么对象头中还必须有一块用于记录数组长度的数据。

2)对象实例数据:

a)对象代码中定义的各种类型的字段内容。无论是父类继承下来的,还是子类中定义的,都记录下来

b)存储的顺序收到虚拟机分配策略参数(FieldAllocationStyle)和字段在Java源码中定义顺序所影响。

1.HotSpot中的默认分配策略为相同宽度字段分配到一起,如long/double(8字节), int(4字节), short/char(2字节),byte等

,这样可以防止内存浪费(内存对齐)

2.在分配策略下,父类的变量在子类变量之前。

3.如果CompactFields参数值为true(默认),那么子类中较窄的变量也可能插入到父类变量空隙中。

3)填充:

由于HostSpot的自动内存管理系统要求对象起始地址必须为8字节的整数倍,也就是说对象大小必须为8字节的整数倍

,对于不满足的将进行数据填充。


4.内存的访问定位:

为了使用对象,我们需要啊通过栈上的reference数据来操作堆上的具体对象。reference实现如何去定位、访问堆中的对象

,不同虚拟机实现有不同的方式,主流以下两种:

1)使用句柄访问,使用句柄的方式,需要在堆中划分一块区域作为句柄池,reference中存储的就是句柄的地址

句柄中包含了对象实例数据以及类型数据各自的具体地址,如下图


2)指针访问,栈中的reference存储对象的真实地址,对象中存储类型数据的地址,如下图


3)句柄访问的最大好处就是reference中存储稳定的句柄地址,在对象被移动时(如垃圾回收后,内存整理)

只会改变句柄中的实例数据地址(类数据部需要改动),reference本身不修改。

4)直接用指针访问的好处是访问对象的速度快,因为不需要两次地址定位(句柄模式,先到句柄,再到对象)。

缺点就是需要更改栈中的reference地址。

《【回头学Java-虚拟机篇】——2.垃圾收集器》

原创粉丝点击