深入理解Java虚拟机一 虚拟机内存管理机制

前言

文章是看了《深入理解Java虚拟机》书后进行的整理和总结,算是一个读书笔记吧。

  • 深入理解Java虚拟机一 虚拟机内存管理机制
  • 深入理解Java虚拟机二 虚拟机类加载机制
  • 深入理解Java虚拟机三 垃圾回收机制

一、虚拟机运行时数据区

1、简介

java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域在进程启动时候就存在,有的则伴随着线程的存在而存在。
了解虚拟机的这些结构对我们理解虚拟机的设计方式,程序编写,解决线上问题等都会有极大的帮助。


Java虚拟机运行时数据区

以上的内存区域划分是java虚拟机规范的规定,但规范只是要求要有这些区域,却并没有要求实现方式,所以虚拟机在实现这些区域的时候其实是由一些自己的优化设计在里面,比如HotSpot虚拟机在jdk 8中就将方法区设计为了元数据区并存放在了虚拟机外部的直接内存中,而在jdk 7中则是在虚拟机内存中划分了一块永久代来存储。

2、程序计数器

程序计数器是一块较小的空间,它可以看作是当前线程执行的字节码的行号指示器,在虚拟机的概念模型里,字节码解释器就是通过改变这个计数器的值来选去吓一跳需要执行的指令,分支、循环、跳转、异常等都依赖都依赖程序计数器。
在java虚拟机中多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,一个处理器在同一时间只能处理一个线程,为了让切换后能正确的恢复并继续执行,每个线程都会这一个独立的程序计数器,各线程互不影响。
这种内存是属于线程私有内存。

这部分的信息存储比较固定,所以不会存在内存溢出的情况。

3、Java虚拟机栈

和程序计数器一样,虚拟机栈也是线程私有的,他的生命周期和线程相同。
在java的每一个方法执行时都会创建一个栈帧用于存储局部变量标,操作数栈、动态连接、方法出入口信息等,每一个方法从调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

在这个区域会有两种异常情况出现:

  • 线程请求的栈深度大于虚拟机允许的深度,会抛出StackOverflowError的异常
    栈是一种后进先出的数据结构,上面我们说了,每个方法执行就会在栈里面存入一个栈帧,方法执行完毕这个栈帧就会被弹出并释放。
    比如方法A调用了方法B,方法B调用了方法C,那么入栈的顺序就是A先入栈,然后B入栈,然后C入栈,C必定先执行完毕(我们讨论的是单线程情况,毕竟栈属于线程私有资源),C出栈,然后B执行完毕B出栈,最后A执行完毕,A出栈,就是这样的过程。
    什么情况很容易出现栈溢出呢,方法深度太深,迟迟执行不完。比如递归,这是最容易出现栈溢出的场景的,所以通常我们在使用递归的时候都应该充分考虑到这种情况。
  • 如果虚拟机栈可以动态扩展,在扩展时申请不到不足够的内存时会抛出OutofMemoryError

栈的虚拟机配置参数为-Xss384k,384k则代表栈的空间,调整这个参数可以调整每个虚拟机栈的大小

4、本地方法栈

本地方法栈与虚拟机栈的作用类似,区别是虚拟栈是针对java方法,而本地方法栈针对的是native方法。
在有些虚拟机中(比如HotSpot)直接将本地方法栈和虚拟机栈合二为一。

5、Java堆

java堆在大多数程序中都是虚拟机所管理的最大一块的空间,几乎所有的对象实例都是存放在这个区域。
垃圾回收也是针对这块区域来说的。
从垃圾回收的角度来说的话,现在大部分的垃圾收集器都是分代回收,一整个堆空间会被分为老年代和新生代。
更细致一些的,新生代还会被分为Eden区和Survivor 0区和Survivor 1区等。
内存的划分与存放的内容是没有关系的,都是对象实例,划分只是为了更好的回收内存。
堆空间如果已经用满并且无法扩展时,继续申请内存则会抛出OutOfMemoryError。
这块区域也是各个线程共享的区域。
-Xms4G -Xmx4G 这两个命令分别可以调整堆的最小空间和最大空间
-Xmn512M为设置年轻代的空间
Sun官方建议年轻代的大小为整个堆的3/8左右。
年轻代如果设置的过小会导致对象更容易被放进老年代,从而引发FullGC。
年轻代设置的过大也会导致年轻代GC时间过久,影响程序性能。
所以设置合理的大小是一个很有必要的过程。

6、方法区

这块区域也是一个线程共享的区域,主要是存储被虚拟机加载的类信息,常量、静态常量等数据。

7、运行时常量池

这部分内容是方法区的一部分,类中除了有类的版本、字段、方法等信息外,还有一项就是常量池,用于存放编译期生成的字面量和符号引用。
我们的String类型常量也就是放在这块空间。

8、直接内存

直接内存并不是虚拟机运行时数据区的一部分,但这部分频繁被使用,也可能导致OutOfMemoryError。
NIO可以使用native函数直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
这个操作如果出现获取不到足够的内存也会抛出OutOfMemoryError。

二、虚拟机对象管理

1、对象的创建

我们创建对象的过程是使用new关键字,而虚拟机遇到一条new的指令时,首先会将去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查类是否已被加载、解析、初始化过。如果没有则会触发类加载过程。
类加载检查通过后则会虚拟机会为新生对象分配内存。
内存分配有指针碰撞和空闲列表两种方式,使用哪种方式取决于虚拟机的GC如何管理这片内存。
因为堆内存属于线程共享资源,那么在多线程环境下分配内存则会存在线程不安全的情况,这种情况下虚拟机采用了CAS配上失败重试的方式保证了更新分配操作的原子性,另一种是把内存分配的动作按照线程划分在不同的空间中执行,即在每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲区,线程在自己的缓冲区分配就能保证安全性。
内存分配完成后虚拟机需要将分配到的内存空间都初始化为零值(对于基本类型来说零值就是0,对于引用对象来说,零值就是null),接下来虚拟机会对对象进行必要的设置,如这个对象是哪个类的实例,如何得到元数据信息,对象的哈希吗,GC分代年龄信息等。
这些工作完成后,一个对象就产生了,但对于java来说,对象的创建才刚开始,方法还没执行,在执行了方法后,一个对象才算是真正的产生出来了。

2、对象的内存布局

对象在内存中存储分为三部分

  • 对象头
  • 实例数据
  • 对齐填充

对象头包括两部分信息,第一部分存储对象自身的运行时数据,如哈希吗,GC分代年龄,所状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
另一部分是对象类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象属于哪个类的实例。
实例数据就是对象真正存储的有效信息,如定义的各种字段的内容。
第三部分对齐填充并不是必要的,也没什么含义,只是因为虚拟机要求对象的地址必须是8字节的整数倍,对象头正好是8字节,所以如果实例数据没有对齐则需要补充。

3、对象的访问定位

对象的访问有两种方式

  • 句柄访问
  • 直接指针

我们知道在栈上会存储引用对象的引用地址,如果是句柄访问的话是有一个句柄池,句柄池会维护栈上的引用地址和对象实际地址的关系,这种方式好处的就是句柄地址可以保持稳定,触发垃圾回收后对象被移动了,栈上的引用地址也不会改变。
而使用直接指针的方式则是栈上的引用地址直接指向对象在内存中的位置,这种的好处是访问更快,减少了一次寻找地址引用的时间。Hotspot虚拟机就是采用这种方式来实现的。

你可能感兴趣的:(深入理解Java虚拟机一 虚拟机内存管理机制)