Java内存区域
运行时数据区域
Java虚拟机在执行Java程序的过程中会把它管理的内存划分成若干个不同的数据区域。
- 线程私有的:
程序计数器,虚拟机栈,本地方法栈 - 线程共享的: 堆、方法区、直接内存
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能够恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
程序计数器主要有两个作用:
1)字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流量控制,如:顺序执行、选择、
循环、异常处理。
2)在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪了。
Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。
栈绝对算得上是JVM运行时数据区域的一个核心,除了一些Native方法调用是通过本地方法栈实现的,其他所有的Java方法调用都是通过栈来实现的。
方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
栈由一个个栈帧组成,而每个栈帧中都有:局部变量表、操作数栈、动态链接、方法返回地址。
本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用的Native方法服务。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间。
堆
Java虚拟机所管理的内存中最大的一块,Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
方法区
方法区属于是JVM运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。
当虚拟机要使用一个类时,它需要读取并解析Class文件获取相关信息,再将信息存入方法区。方法区会存储已被虚拟机加载的类信息、字段信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
HotSpot虚拟机对象探秘
HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
Java对象的创建过程:
1、类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
2、分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载器完成之后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。分配方式有“指针碰撞”和“空闲列表”两种,选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
- 内存分配的两种方式
- 指针碰撞:
- 适用场景:堆内存规整(即没有内存碎片)的情况下;
- 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
- 使用该分配方式的GC收集器:Serial,ParNew
- 空闲列表:
- 适用场景:堆内存不规整的情况下。
- 原理:虚拟机会维护一个列表,该列表会记录哪些内存块是可用的,在分配的时候,找一块足够大的内存块来划分给对象实例,最后更新列表记录。
- 使用该分配方式的GC收集器:CMS
选择以上两种方式中的哪一种,取决于Java堆内存是否规整,而Java堆内存是否规整,取决于GC收集器的算法是“标记-清除”,复制算法内存也是规整的。
- 内存分配并发问题
- 在创建内存时有一个很重要的问题,就是线程安全,因为在实际的开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方法来保证线程安全:
- CAS+失败重试:CAS是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
- TLAB:为每一个线程预先在Eden区分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。
3、初始化零值
内存分配完成时,虚拟机需要将分配到的内存空间都初始化为零值(不包含对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4、设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中,另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
5、执行init方法
在上面工作都完成之后,以虚拟机的视角来看,一个新的对象已经产生了,但是从Java程序的视角来看,对象创建才刚开始,方法还没有执行,所有的字段都还为0。所以一般来说,执行new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全生产出来。
对象的内存布局
对象在内存中的布局可以分为3块区域: 对象头、实例数据、对齐填充。
堆空间的基本结构
Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java自动内存管理最核心的功能是堆内存中对象的分配与回收。
Java堆是垃圾回收器管理的主要区域,因此也被称为GC堆。
从垃圾回收的角度来说,由于现在的收集器基本都采用分代垃圾收集算法,所以Java堆被划分为了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法。
内存分配和回收原则
对象优先在Eden区分配
垃圾清除算法
- 标记-清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象。在标记完成后统一回收掉所有没有被标记的对象。