什么是JVM
首先JVM是运行Java代码的假想计算机,运行在操作系统之上的,与硬件没有直接的交互。
Java源文件经过编译后形成字节码文件,通过JVM的类加载的机制进入Java虚拟机的运行数据区,再通过一套的字节码指令集,将Java的代码翻译成计算机能够识别的指令,最终实现Java代码在计算机上的运行实现。
- 其中的JVM内存布局的概念:指的就是JVM的运行数据区
JVM运行数据区
1、JVM中内存的划分主要为5部分:虚拟机栈、本地方法栈、程序计数器、方法区和堆
2、通过上图可以查看到,JVM内存可以分为两部分,其中一部分为线程私有内存,包括的有程序计数器、本地方法栈和虚拟机栈;第二部分线程共享的内存划分为方法区和堆
线程私有内存
- 1、程序计数器
作用:用来指示和记录当前线程在执行字节码时的行号信息(地址信息),当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
每一个线程都有一个程序计数器,线程之间程序计数器互不干扰,相互独立;
注意:程序计数器是唯一一个没有outofmemoryerror 错误的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
- 2、虚拟机栈
作用:每一个方法在执行的过程中,都会创建一个栈帧用来存储局部变量表、操作数栈、动态链接和方法出口信息,其生命周期和线程是相同的;
对应的每一个方法从调用到结束,就意味着在虚拟机栈帧上的入栈和出栈的操作;
对于执行引擎来说:每一个处于栈帧顶部的方法栈帧才是有效的,也被称为当前方法;
注意:该内存可能会出现的异常:
stackoverflowerror 意味着栈帧的个数超过虚拟机栈帧的最大的深度,
outofmemoryerror 意味着虚拟栈帧无法申请足够的内存;
- 3、本地方法栈
本地方法栈和虚拟机栈是功能相同的,区别在于本地方法栈适用于的是本地方法;
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
线程共享的内存
- 1、方法区
作用:就是存储对象,对象包括被虚拟机加载的类的信息、常量、静态变量、编译后的代码、运行时常量池信息;
运行时常量池:Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域;
方法区也被称为永久代:方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式
方法区不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常;
为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?
整个永久代有一个 JVM 本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError。
可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小
- 2、堆
堆的作用:此内存区域的唯一目的就是存放对象实例、数组对象;是JVM内存回收的主要区域,因此也被称作GC堆,现在收集器基本都采用分代垃圾收集算法。
- 新生代(Young Generation):eden、s0,s1
- 老年代(Old Generation):tentired
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常;
- 3、直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现。
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据
本机直接内存的分配不会收到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制
- 4、运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。