本文将介绍JVM的结构
、对象的创建和分配过程
、内存溢出
。
JVM介绍
java文件执行流程:
- .java文件通过JDK的编译器变成.class文件。
- .class文件和java类库,通过类加载器ClassLoader进入JVM的方法区。
- JVM的执行引擎执行,把字节码翻译机器码,并跟硬件进行交互。
JVM是一种规范,它规范了.class文件与本地硬件交互的一种规范。比如对于c/c++开发者来说,他们拥有了对象的“所有权”,对象的开始和终结需要他们的维护。而相对Java开发者,他们在虚拟机内存管理垃圾回收机制下,就不需要维护对象的终结,JVM帮我们自动回收对象,这使开发者更加便捷,但一旦发生内存泄漏和溢出,如果不熟悉JVM则很难找出问题所在,所以学习JVM能帮助我们快速定位内存泄漏和溢出原因。
JVM的特性
平台无关性:
一套java代码可以在window、linux、mac中运行,因为JVM这个中间平台已经做好了与硬件交互的解释。
语言无关性:
JVM接收的是.class文件,所以无论你是写java语言还是其他语言,只要按照规范生成为.class文件就可以与JVM上运行。
JVM的分类
JVM只是一种规范,可以按照规范自定义JVM。本篇文章讲解的是Oracle公司的Hotspot虚拟机。
JVM整体内存结构
JVM的整体内存结构包括:虚拟机栈、程序计数器、堆、方法区、直接内存。
运行时数据区域
虚拟机栈
每启动一个线程都会创建一个新的虚拟机栈
。在线程私有区中包括了虚拟机栈和本地方法栈,但在Hotspot虚拟机中把这两个栈合成一个栈,
作用:
存储当前线程运行java方法的指令、数据、返回地址。
大小限制:
可用-xxs
设置虚拟机栈的大小
栈帧:
它是栈元素,每执行一个方法,就会有相应的一个栈帧进入虚拟机栈。
栈帧结构:
- 局部变量表
局部变量表是一组变量表
存储空间,用于存放方法参数和方法内的局部变量
。在java程序编译为class文件时,就在方法的Code属性的max_locals
数据项中确定了该方法所需要分配的局部变量表的最大容量。
局部变量表以变量槽Slot为最小单位,分别存储了:
- 8大基本数据类型:boolean、byte、char、short、int、float、long、double
- reference:表示对一个对象实例的引用。一、此引用可以直接或间接地查找到对象在Java堆中的数据存放的起始地址索引。二、此引用可以直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息。
- returnAddress:指向一条字节码指令的地址。
如果执行的是实例方法(非static方法),那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过this
来访问这个引用。
操作数栈
在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候通过操作数栈来传参。动态链接
将符号引用指向直接引用返回地址
返回栈帧方法的返回事件。
程序计数器
指向当前线程正在执行的字节码指令的地址。
堆
java堆几乎
所有的对象实例都在这里分配内存,java堆不需要连续的内存,既可以固定大小又可以动态扩展大小,是虚拟机中管理内存中最大的一块,是垃圾回收机制重点回收区域。
java堆是线程共享区域,但是有些内存分配也可以设计为多个线程的私有的分配缓冲区TLAB,以提升对象分配的效率。
java堆结构:
新生代(占1/3):存放生命周期不长的小对象
Eden空间(占 8/10)
From空间(S0,占 1/10)
To空间(S1,占 1/10)老年代(占2/3):存放大对象
方法区
方法区(永久代)是存放已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存。方法区不需要连续的内存,既可以固定大小又可以动态扩展大小。垃圾收集器在此区域主要回收常量池和类型(class对象)的卸载。
运行时常量池
运行常量池是在常量池表中存放的编译期生成的各种字面量和符号引用,这部分内容将在
直接内存
直接内存并不是虚拟机运行时的数据区域。在JDK1.4中加入NIO类,引入一种基于通道(Channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存。然后通过堆里面的DirectByteBuffer对象作为引用进行操作。
JVM结构小结
JVM分为线程私有区和共享区:
私有区中存放着虚拟机栈
和程序计数器
。虚拟机栈跟线程一一对应,每一个线程对应着一个虚拟机栈,在虚拟机栈中以栈帧
为元素,栈帧跟方法一一对应,每执行一个方法就有一个栈帧入栈
,每完成一个方法就有一个对应的栈帧出栈
,栈帧的结构分为:局部变量表、操作数栈、动态链接、方法出口。程序计数器则记录当前方法执行到的字节码的行数,在轮流执行的并发编程中能接上次执行的位置往下执行。
共享区主要有堆
和方法区
。堆中又划分为年轻代和老年代,其中年轻代又划分为:Eden、From、To代,几乎所有的对象实例都在堆中分配。方法区主要存储类型信息和常量、静态变量等。
对象的创建和分配原则
对象的创建过程
对象在创建时首先会进行类加载检查(是否被ClassLoader加载过),即能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表类是否被加载、解析、初始化过,如果没有,则去ClassLoader类加载,否则进行对象分配内存、初始化类变量的默认值、设置对象头、执行类的构造函数。
这里重点讲下分配内存的方式和并发的安全问题,和对象在堆中的结构。
分配内存
对象的分配内存就是在Java堆中分配一块内存给对象,但是根据堆中的已使用内存和空闲内存的存放位置,分为两种分配内存方式:
指针碰撞 : 如果JAVA堆是规整的,已使用内存集体放一边,可闲内存集体放一边,那么只需要指针往空闲的内存移动一定位置就可以为对象分配内存,所以叫指针碰撞
空闲列表:如果JAVA堆是不规整的,已使用内存和空闲内存相互交错在一起,那么就需要维护出一张空闲内存的列表,把新的对象分配在空闲内存列表中,所以叫空闲列表。
多线程并发的分配问题:
堆是线程共享的,所以在多线程时有可能会发生同一位置被多个线程的对象分配,那么也有两种解决办法:
- CAS加上失败重试机制
- 本地线程分配缓冲(TLAB)
每一个线程都在堆中分配一个独立的区域,每个对象的分配内存就是当前线程在堆中分配区域分配。
对象在堆中的结构
对象的内存布局分为:
对象头
MarkWord: 哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳
类型指针:指向Class对象实例数据
存放类的变量数据对齐填充
占位符作用,对象是以8字节为单位
引用对对象实例的访问
使用句柄:通过指向一个句柄池间接的访问对象,优点:对象地址改变后不要更改reference的指向,缺点:查找地址需要时间。
直接指针:reference直接指向堆中的实例地址,优点:时间快,缺点对象实例移动reference也需要改变。
对象的分配原则
是否能在栈上分配
逃逸分析:在方法中创建的对象,没有被其他方法或线程中用到,那么这个对象可以在栈中分配。
优先在Eden中分配:
大多数情况下,对象在Eden中分配,如果Eden区没有足够的空间分配,将触发Minor GC。在Minor GC时如果S0,S1的空间不够分配或GC年龄达到指定值,则根据分配担保机制
转移到老年代。
大对象直接进入老年代
超过指定大小的大对象将直接进入老年代。这样可以避免Eden区和两个S区来回复制。长的字符串和数组是常见的大对象。
长期存活的对象进入老年代
在Eden区的对象实例发生GC时会被移动到S0区并在对象头的GC年龄+1,之后每GC一次,还在S0区的对象的GC年龄就会+1,如果超过了指定次数就会移入老年代。
动态对象年龄判断
如果在S空间中,相同GC年龄的对象的大小的总和大于S空间的一半,则大于或等于此GC年龄的对象全部移入老年代。
空间分配担保机制
在发生Minor GC之前要进行一次空间分配担保机制的判断,决定是发生Minor GC还是Full GC。因为在Minor GC时可能会有对象进入老年代,如果老年代的空间不够就会发生OOM的风险,所以空间分配担保机制则是尽量去降低这种风险发生。
- 发生Minor GC
- 老年代的最大连续空间大于新生代的所有对象总空间
- 如果1不成立,则查看是否可以担保,如果可以担保,继续查看老年代的最大连续空间大于历次晋升到老年代空间的对象的平均大小。
- 发生Full GC
- 如果上述1不成立,且不可以担保
- 如果可以担保,继续查看老年代的最大连续空间小于历次晋升到老年代空间的对象的平均大小。
对象的创建分配小结:
对象的创建流程是,首先ClassLoader类加载检查
:如果该类的符号引用已经被加载、解析、初始化过后则表示已经被ClassLoader加载过。被ClassLoader加载过后,则到堆中分配内存,分配方式有指针碰撞和空闲列表
两种,在分配内存中有并发的安全问题所以有了两种解决方式:CAS和TLAVB
。分配内存地址后,就开始设置类变量的初始值。设置完默认值后,就开始设置对象头,其中对象头包括:GC年龄、哈希码、类型指针等。对象则包括对象头、实例数据和对对齐填充。最后是执行对象的构造函数进行初始化。栈中对堆的对象实例的访问有使用句柄
和直接访问
两种。
对象在分配原则有:
- 根据逃逸分析决定是否在栈中分配
- 新对象优先分配在Eden区
- 大对象直接分配在老年代
- 长期存活的对象进入老年代
- 同年龄的对象总和大于S区的一半,则以上年龄的对象进入老年代。
- 分配担保机制,在每次发生Minor GC之前等会判断老年代的最大连续空间是否足够可能从新生代进来的对象。
内存溢出
堆溢出
java堆用于存储对象实例,只要不断的产生对象,并且GC Root到对象中有可达性导致不能垃圾回收,当堆的对象总量超过规定的最大值就会产生内存溢出OutOfMemeyError。
栈溢出
- 如果是对虚拟机栈存放不下,比如是栈创建太多或栈的容量太大,则会导致内存溢出OutOfMemeyError。
- 如果是对栈帧存放不下,比如栈帧太多或栈帧太大或栈最大容量太小,则会导致栈溢出StackOverflowError。
方法区和运行常量溢出
方法区(元空间)存放着类型信息,比如Class对象,而Class对象
直接内存溢出
直接内存的最大值默认与Java堆的最大值一致。