Java面试- JVM 内存模型讲解

常常有人会有这么一个疑惑,难道 Java 开发就必定要懂得 JVM 的原理吗?我不懂 JVM ,但我照样能够开发。确实,但若是懂得了 JVM ,可让你在技术的这条路上走的更远一些。java

<!-- more -->git

JVM 的重要性

首先你应该知道,运行一个 Java 应用程序,咱们必需要先安装 JDK 或者 JRE 。这是由于 Java 应用在编译后会变成字节码,而后经过字节码运行在 JVM 中,而 JVM 是 JRE 的核心组成部分。github

优势

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)线程隔离性能

堆(Heap)

堆是 JVM 内存中最大的一块内存空间,该内存被全部线程共享,几乎全部对象和数组都被分配到了堆内存中。优化

堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 区和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。线程

随着 Java 版本的更新,其内容又有了一些新的变化: >在 Java6 版本中,永久代在非堆内存区;到了 Java7 版本,永久代的静态变量和运行时常量池被合并到了堆中;而到了 Java8,永久代被元空间(处于本地内存)取代了。code

为何要用元空间替换永久代呢?

  1. 为了融合 HotSpot JVM 与 JRockit VM,由于 JRockit 没有永久代,因此不须要配置永久代。
  2. 永久代内存常常不够用或发生内存溢出(应该是 JVM 中占用内存最大的一块),产生异常 java.lang.OutOfMemoryError: PermGen。在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,因为 PermGen 中类的元数据信息在每次 FullGC 的时候均可能被收集,回收率都偏低,成绩很难使人满意;还有,为 PermGen 分配多大的空间很难肯定,PermSize 的大小依赖于不少因素,好比,JVM 加载的 class 总数、常量池的大小和方法的大小等。

看到这儿,天然就想到了 GC 回收算法,不用急,我会在以后的文章中进行讲解,如今仍是以 JVM 内存模型为主。

方法区(Method Area)

什么是方法区? >方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息常量池(字符串常量池以及全部基本类型都有其相应的常量池)、运行时常量池。这其中,类信息又包括了类的版本、字段、方法、接口和父类等信息。

类信息

JVM 在执行某个类的时候,必须通过加载、链接、初始化,而链接又包括验证、准备、解析三个阶段。

在加载类的时候,JVM 会先加载 class 文件,而在 class 文件中便有类的版本、字段、方法和接口等描述信息,这就是类信息

常量池

在 class 文件中,除了类信息,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各类字面量符号引用

字面量符号引用又是什么呢?

字面量包括字符串(String a=“b”)、基本类型的常量(final 修饰的变量),符号引用则包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、字段的名称和描述符以及方法的名称和描述符。

运行时常量池

当类加载到内存后,JVM 就会将 class 文件常量池中的内容存放到运行时常量池中;在解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)。

例如: >类中的一个字符串常量在 class 文件中时,存放在 class 文件常量池中的。 > >在 JVM 加载完类以后,JVM 会将这个字符串常量放到运行时常量池中,并在解析阶段,指定该字符串对象的索引值。

运行时常量池是全局共享的,多个类共用一个运行时常量池,所以,class 文件中常量池多个相同的字符串在运行时常量池只会存在一份。

讲到这里,你们是否是有些头晕了,说实话,我在看到这些内容的时候,也是云里雾里的,这里举个例子帮助你们理解:

public static void main(String[] args) {
        String str = "Hello";
        System.out.println((str == ("Hel" + "lo")));

        String loStr = "lo";
        System.out.println((str == ("Hel" + loStr)));

        System.out.println(str == ("Hel" + loStr).intern());
    }

其运行结果为:

true
false
true

第一个为 true,是由于在编译成 class 文件时,可以识别为同一字符串的, JVM 会将其自动优化成字符串常量,引用自同一 String 对象。

第二个为 false,是由于在运行时建立的字符串具备独立的内存地址,因此不引用自同一 String 对象。

最后一个为 true,是由于 String 的 intern() 方法会查找在常量池中是否存在一个相等(调用 equals() 方法结果相等)的字符串,若是有则返回该字符串的引用,若是没有则添加本身的字符串进入常量池。

涉及到的Error

  1. OutOfMemoryError出如今方法区没法知足内存分配需求的时候,好比一直往常量池中加入数据,运行时常量池就会溢出,从而报错。

程序计数器(Program Counter Register)

程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。

因为 Java 是多线程语言,当执行的线程数量超过 CPU 数量时,线程之间会根据时间片轮询争夺 CPU 资源。若是一个线程的时间片用完了,或者是其它缘由致使这个线程的 CPU 资源被提早抢夺,那么这个退出的线程就须要单独的一个程序计数器,来记录下一条运行的指令。

因而可知,程序计数器和上下文切换有关。

虚拟机栈(VM Stack)

>虚拟机栈是线程私有的内存空间,它和 Java 线程一块儿建立。 > >当建立一个线程时,会在虚拟机栈中申请一个线程栈,用来保存方法的局部变量、操做数栈、动态连接方法和返回地址等信息,并参与方法的调用和返回。 > >每个方法的调用都伴随着栈帧的入栈操做,方法的返回则是栈帧的出栈操做。

能够这么理解,虚拟机栈针对当前 Java 应用中全部线程,都有一个其相应的线程栈,每个线程栈都互相独立、互不影响,里面存储了该线程中独有的信息。

涉及到的Error

  1. StackOverflowError出如今栈内存设置成固定值的时候,当程序执行须要的栈内存超过设定的固定值时会抛出这个错误。
  2. OutOfMemoryError出如今栈内存设置成动态增加的时候,当JVM尝试申请的内存大小超过了其可用内存时会抛出这个错误。

本地方法栈(Native Method Stack)

>本地方法栈跟虚拟机栈的功能相似,虚拟机栈用于管理 Java 方法的调用,而本地方法栈则用于管理本地方法的调用。 > >但本地方法并非用 Java 实现的,而是由 C 语言实现的。

也就是说,本地方法栈中并无咱们写的代码逻辑,其由native修饰,由 C 语言实现。

总结

以上就是 JVM 内存模型的基本介绍,大体了解了一下5个分区及其相应的含义和功能,由此能够继续延伸出 Java 内存模型、 GC 算法等等,我也会在以后的文章中进行讲解。若是你有什么想法,欢迎在下方留言。

有兴趣的话能够访问个人博客或者关注个人公众号、头条号,说不定会有意外的惊喜。

https://death00.github.io/

> 本文由博客一文多发平台 OpenWrite 发布!

相关文章
相关标签/搜索