经常有人会有这么一个疑惑,难道一定要懂得 JVM 的原理吗?我不懂 JVM ,但我照样可以开发。确实,但如果懂得了 JVM ,可以让你在技术的这条路上走的更远一些。
首先你应该知道,运行一个 Java 应用程序,我们必须要先安装 JDK 或者 JRE 。这是因为 Java 应用在编译后会变成字节码,然后通过字节码运行在 JVM 中,而 JVM 是 JRE 的核心组成部分。
JVM 不仅承担了 Java 字节码的分析(JIT compiler)和执行(Runtime),同时也内置了自动内存分配管理机制。这个机制可以大大降低手动分配回收机制可能带来的内存泄露和内存溢出风险,使 Java 开发人员不需要关注每个对象的内存分配以及回收,从而更专注于业务本身。
这个机制在提升 Java 开发效率的同时,也容易使 Java 开发人员过度依赖于自动化,弱化对内存的管理能力,这样系统就很容易发生 JVM 的堆内存异常、垃圾回收(GC)的不合适以及 GC 次数过于频繁等问题,这些都将直接影响到应用服务的性能。
JVM 内存模型共分为5个区: 堆(Heap)
、 方法区(Method Area)
、 程序计数器(Program Counter Register)
、 虚拟机栈(VM Stack)
、 本地方法栈(Native Method Stack)
。
其中, 堆(Heap)
、 方法区(Method Area)
为 线程共享
, 程序计数器(Program Counter Register)
、 虚拟机栈(VM Stack)
、 本地方法栈(Native Method Stack)
为 线程隔离
。
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。
堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 区和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。
随着 Java 版本的更新,其内容又有了一些新的变化:
在 Java6 版本中,永久代在非堆内存区;到了 Java7 版本,永久代的静态变量和运行时常量池被合并到了堆中;而到了 Java8,永久代被 元空间
(处于本地内存)取代了。
看到这儿,就想到了 GC 回收算法,不用急,我会在下一篇文章中进行讲解。
什么是方法区?
方法区主要是用来存放已被虚拟机加载的类相关信息,包括 类信息
、 常量池
(字符串常量池以及所有基本类型都有其相应的常量池)、 运行时常量池
。这其中,类信息又包括了类的版本、字段、方法、接口和父类等信息。
JVM 在执行某个类的时候,必须经过加载、连接、初始化,而 连接
又包括验证、准备、解析三个阶段。
在加载类的时候,JVM 会先加载 class 文件,而在 class 文件中便有类的版本、字段、方法和接口等描述信息,这就是 类信息
。
在 class 文件中,除了 类信息
,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各种 字面量
和 符号引用
。
那 字面量
和 符号引用
又是什么呢?
字面量包括字符串(String a=“b”)、基本类型的常量(final 修饰的变量),符号引用则包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、字段的名称和描述符以及方法的名称和描述符。
当类加载到内存后,JVM 就会将 class 文件 常量池
中的内容存放到 运行时常量池
中;在解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)。
例如:
类中的一个字符串常量在 class 文件中时,存放在 class 文件常量池中的。
在 JVM 加载完类之后,JVM 会将这个 字符串常量
放到 运行时常量池
中,并在解析阶段,指定该字符串对象的索引值。
运行时常量池
是全局共享的,多个类共用一个运行时常量池,因此,class 文件中常量池多个相同的字符串在运行时常量池只会存在一份。
OutOfMemoryError
出现在方法区无法满足内存分配需求的时候,比如一直往常量池中加入数据, 运行时常量池
就会溢出,从而报错。 程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。
由于 Java 是多线程语言,当执行的线程数量超过 CPU 数量时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。
由此可见,程序计数器和上下文切换有关。
虚拟机栈是线程私有的内存空间,它和 Java 线程一起创建。
当创建一个线程时,会在虚拟机栈中申请一个线程栈,用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息,并参与方法的调用和返回。
每一个方法的调用都伴随着栈帧的入栈操作,方法的返回则是栈帧的出栈操作。
可以这么理解,虚拟机栈针对当前 Java 应用中所有线程,都有一个其相应的线程栈,每一个线程栈都互相独立、互不影响,里面存储了该线程中独有的信息。
StackOverflowError OutOfMemoryError
本地方法栈跟虚拟机栈的功能类似,虚拟机栈用于管理 Java 方法的调用,而本地方法栈则用于管理本地方法的调用。
但本地方法并不是用 Java 实现的,而是由 C 语言实现的。
也就是说,本地方法栈中并没有我们写的代码逻辑,其由 native
修饰,由 C 语言实现。
以上就是 JVM 内存模型的基本介绍,大致了解了一下5个分区及其相应的含义和功能,由此可以继续延伸出 Java 内存模型、 GC 算法等等,我也会在之后的文章中进行讲解。如果你有什么想法,欢迎在下方留言。
有兴趣的话可以访问我的博客或者关注我的公众号、头条号,说不定会有意外的惊喜。
https://death00.github.io/