浅谈JVM(1)
浅谈JVM(1)
一、JVM的内存模型
JDK 1.8 中,JVM的内存模型主要分为五个部分:堆、原空间、虚拟机栈、本地方法栈和程序计数器 。

这几个部分可以分成线程共享 和线程私有 两类,线程共享的内存可以供所有线程共同使用且访问;线程私有的内存则会随着线程的创建而分配,随着线程的销毁而回收。
- 线程共享
- 堆 (Heap):
堆是JVM中最大的一块内存,在JVM启动的时候创建,专门用来存放数组 和对象实例 。堆中还包含了字符串常量池 ,用于存储字符串字面量和常量。
从内存回收的角度,堆中又分为新生代(占1/3)和 老年代(占2/3),新生代又分为1个Eden区 和2个Survivor区 (From & To)。
当堆中没有内存分配给对象实例 ,并且堆再也无法拓展内存 的时候,就会出现OOM异常 (OutOfMemoryError)。 - 元空间 (MetaSpace):
元空间用来存储类元信息 、常量 、方法字节码 等。元空间包含运行时常量池 ,用于存储编译时期生成的各种字面量和符号引用。
在JDK 1.7及之前,元空间被称为方法区 ,并且它的内存分配在JVM内部;在JDK 1.8之后,元空间在本地内存 中分配。
元空间可以不实现垃圾收集,内存不足 时会抛出OOM异常。
- 堆 (Heap):
- 线程私有
- 虚拟机栈
每个线程都会有一个虚拟机栈,生命周期与线程相同 。
它存储方法调用产生的栈帧 ,包括局部变量 、操作数栈 、方法返回地址 等。
递归层数过多时可能会产生StackOverflowError ,空间不足时会产生OOM异常 。 - 本地方法栈
与虚拟机栈类似,专门用于存储本地方法 (Native Method)的调用信息。
可能会抛出 StackOverflowError 和 OutOfMemoryError 。 - 程序计数器
用于存储当前线程正在执行的字节码指令地址 ,如果是Native方法,则计数器值为null 。
是JVM中最小的一块内存区域,不会出现OOM 。
- 虚拟机栈
二、对象创建过程
在Java中创建对象主要分为五步:
- 类加载检查
当虚拟机遇到new指令时,会检查这个指令的参数是否能在常量池 中找到对到一个类的符号引用 ,并且检查这个类是否被加载、解析和初始化过 。如果没有,则会执行相应的类加载过程。 - 内存分配
JVM会为对象分配内存空间。对象的所需内存大小在类加载过程中就可以确定,因此分配内存就是在虚拟机划定一块连续的空间,主要有两种方式:- 指针碰撞:如果堆中的空闲内存是规整的 (空闲的内存都紧密排列),JVM就会通过移动指针 来分配内存。
- 空闲列表:如果堆中的空闲内存是碎片化的 ,JVM就会维护一个空闲列表,记录可用的内存块,并分配合适的区域。
- 零值初始化
分配完空间之后,JVM会对分配的内存进行初始化,将所有的字段都设为零值 (引用值为null,boolean为false,数字为0)。这一步保证了对象的字段赋值之前 具有默认值,避免未初始化的变量被访问 。 - 设置对象头
对象头包括Mark Word 、klass pointer 、数组长度 等。Mark Word用于存储对象的哈希码 、分代GC年龄、锁的占有情况 等。klass pointer指向对象所属类的元数据的地址 。 - 执行构造方法
用方法来进行对象的初始化。构造方法会根据代码逻辑对对象进行赋值 ,同时调用父类的构造方法完成继承链的初始化 。
三、类加载过程
JVM进行类加载的过程主要分5步:加载 、链接 (验证 、准备 、解析 )、初始化 、使用 、卸载 。

- 加载
加载过程也分为三步:- 首先会根据被加载类的全限定名称 (包名、类名)去获取相应的.class文件的二进制字节流
- 然后会将二进制文件中的静态数据结构转化成方法区(元空间)的运行时数据结构,即将类的元信息 (字段、方法、父类等)加载到方法区
- 最后会在堆中创建一个Class类的对象 ,作为改类的访问入口,也可以供反射调用
- 验证
验证需要验证四个方面:- 文件格式:验证字节码文件 符合.class文件 的格式规范
- 类元信息:验证该类的元信息是否符合语法要求,如是否继承了final修饰的类 、父类是否存在 等
- 字节码:验证字节码指令是否符合语法要求,如访问越界 、类型转化错误 等
- 符号引用:验证符号引用 是否能转化为直接引用
- 准备
JVM会为该类的静态变量开辟内存空间,并赋上零值 。 - 解析
将类中符号引用 转化成直接引用 。 - 初始化
JVM会为类中的静态变量赋上具体的值,同时执行静态代码块中的内容。 - 使用
使用类或者对象。 - 卸载
类的卸载非常苛刻,需要同时满足三个条件:- 该类的所有实例 都已经被回收
- 该类的类加载器 也被回收
- 该类的
java.lang.Class对象没有被引用
四、类加载的双亲委派原则
类加载的双亲委派原则,就是指当一个类加载器需要加载某个类时,首先会请求自己的父类加载器加载此类,判断缓存是否命中。如果委托到启动类加载器还没有命中,那么加载该类的请求就会逐层向子类加载器委托,知道该类属于某个子类加载器的加载范畴,才会开始启动类加载过程。
类加载器的层次结构
- 启动类加载器 :它负责加载JVM中的核心类库,位于最顶层,通常由本地方法实现
- 拓展类加载器 :它负责加载/lib/ext路径下的类
- 应用程序类加载器 :它负责加载ClassPath路径下的类
- 自定义类加载器 :由用户自行实现.
双亲委派的优势
- 唯一性 :避免多个类加载器加载同一个类
- 安全性 :避免恶性篡改的类替代核心库中的类
- 模块化管理 :不同的模块可用交给不同的类加载器管理
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Aromatic!



