深入理解java虚拟机(一)

虚拟机内存

Posted by Kinsomy on April 25, 2019

1.Java虚拟机内存区域



有图上可以看出,java虚拟机的运行时数据区主要分为几个区域,每个线程独有的程序计数器、虚拟机栈、本地方法栈和线程共享的方法区和堆。

1.1 程序计数器

程序计数器保存当前线程执行的字节码指令,jvm通过读取程序计数器的值来获得下一条需要执行的字节码指令,常用的分支、循环、跳转、异常处理、线程回复的功能都是要依赖程序计数器。

为什么程序计数器是线程独有?

因为java的多线程是多个线程轮流使用一个处理器的执行时间,也就是说一个处理器在一个时间只有一个线程可以持有,每一个时间都要通过抢占去获取,当前线程执行之后会让出处理器资源交给下一个线程,这个时候程序计数器要保存每一个线程的指令执行位置,不至于多线程之间相互影响,所以程序计数器必须是每个线程独有的内存区域。

“如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。”

摘录来自: 周志明. “深入理解Java虚拟机:JVM高级特性与最佳实践。” iBooks.

1.2 Java虚拟机栈

Java虚拟机栈也是线程独有的,它的生命周期和线程相同,每个Java方法执行都会创建一个栈帧,里面存储了局部变量表、操作数栈、动态链接、方法出口,执行一个Java方法就是入栈操作,方法执行完成则出栈。

局部变量表存放编译时已知的基本数据类型和对象引用(指针)。“局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。”

摘录来自: 周志明. “深入理解Java虚拟机:JVM高级特性与最佳实践。” iBooks.

“在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。”

摘录来自: 周志明. “深入理解Java虚拟机:JVM高级特性与最佳实践。” iBooks.

1.3 本地方法栈

本地方法栈保存的是Navtive方法,和Java虚拟机栈功能相似,也会报出StackOverflowError和OutOfMemoryError。

1.4 堆

Java堆是线程共享的内存区域,在虚拟机启动时就创建,堆的作用就是存放对象实例,“Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。”

摘录来自: 周志明. “深入理解Java虚拟机:JVM高级特性与最佳实践。” iBooks.

Java堆的可以是不连续的物理内存空间。

堆内存可以通过设置jvm参数-Xmx和-Xms扩展大小,但是当堆内存到达上限再也不能被扩展时就会抛出OOM异常。

1.5 方法区

方法区是线程共享的内存,用于存储已经被加载的类信息、常量、静态变量、JIT编译后的代码数据

1.6 运行时常量池

常量池是方法区的一部分区域,用于存放编译器生成的字面量和符号引用,常量池具有通态性,在编译器可以有内容存入常量池,运行时同样也可以将新产生的常量存入。例如String的intern方法。String的Intern方法详解.

常量池也会有OOM异常。

2. HotSpot虚拟机

2.1 对象创建

1) java代码中new一个对象出来对应了一个指令,虚拟机会根据指令参数先去检查常量池中是否已经有该类的符号引号,再检查这个符号引用代表的类是否被加载、解析和初始化。如果还没有,则会执行类加载。

2) 执行类加载过程后,可以确定新对象需要的内存,虚拟机就会为新生代的对象分配内存,这个分配内存的操作就是在java堆中开辟一块内存控件存放这个对象。

3) 虚拟机将分配到的内存空间都初始化为零值,程序可以直接访问这些实例,拿到它们类型对应的零值。这也就是为什么在java代码中基本对象不用赋初值也可以直接使用的原因。

4) 虚拟机对对象进行必要的设置,例如元数据、哈希码、分代信息等,都会保存在对象头中。

5) 执行<init>方法将对象按照代码初始化。

2.2 内存分配方式

1) 指针碰撞

“假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,”

使用Serial、ParNew等带Compact过程的收集器。

2) 空闲列表

“如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,”

使用CMS基于Mark—Sweep算法的收集器。

2.3 内存分配并发问题

内存分配在并发情况下是线程不安全的,一个线程正在修改内存指针,修改还没有完成,另一个线程也想使用该指针,就会用以前的指针进行操作,这就出现了错误。解决问题有两个方案:

“一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。”

摘录来自: 周志明. “深入理解Java虚拟机:JVM高级特性与最佳实践。” iBooks.

2.4 对象的内存布局

HotSpot虚拟机中,一个对象在内存中分为三个部分:对象头实例数据对齐填充

2.4.1 对象头

对象头存储对象运行时数据:HashCode、GC分代年龄、锁状态、线程持有的锁、偏向线程ID、偏向时间戳等;对象头还存储类型指针,指向类的元数据(对象的实例),数组对象还需要额外存储数组长度。

2.4.2 实例数据

实例数据是对象的内容信息,对象内定义的字段的内容,父类和子类中定义的字段都会被存储下来,相同宽度的类型字段会被分配在一起,父类的中变量存储顺序先于子类。

2.4.3 对齐填充

对齐填充非必需,在HotSpot虚拟机中一个对象的内存大小必须是8的整数倍,当不满足条件时,对齐填充会不全内存空间。