【内功修炼系列2】给JVM把个脉

2,079 阅读10分钟

不得不了解的JVM

今天要聊到JVM(Java虚拟机)了,作为Java开发来说,JVM是你走出Java开发实践之路的第一块基石。因为你跑Java程序必须装JDK或JRE包,它们都包括了JVM,可以说JVM是Java语言的核心发动机。JVM这部分内容显得有些晦涩难读,而且这部分内容我也不太想讲,因为讲的人太多了。JVM内容你讲的清楚,不能代表你厉害,但是你讲不明白,那一定代表你不行。在某种程度上看,JVM的相关知识已经成为Java开发能力的下限,谁也绕不开,谁也躲不掉。既然这样,就让我们一起尝尝这份苦瓜。我会结合JVM的内存结构、GC原理、配置调优几个方面给大家分享自己的知识,还请大家不吝赐教。

JVM内存结构

在 Java 中,JVM 内存结构主要分为堆、程序计数器、方法区、虚拟机栈本地方法栈。这个知识点也是在面试中必须出现的问题,我面试一些开发人员,经过一些简单介绍之后,第一个问题就是:“来,先介绍一下JVM内存结构”。为了更加直观,上图!

JVM的5个模块具体都是干什么的呢?让我们接着看。

程序计数器

程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。《深入理解Java虚拟机》

大家都知道 Java 是多线程语言,当程序中执行的线程数超过了 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源,这块有过多线程开发经验的同学应该比较熟悉。

一个线程的时间片如果用完了,或者是由于异常、等待导致这个线程的 CPU 资源被提前抢夺,那么这个时候就需要程序计数器记录当前运行到哪,下一条需要执行指令。退出的线程下次在此获得CPU资源的时候,就会继续执行,不会乱。这就好比小时候去网吧打单机RPG游戏,由于游戏一天打不完,在下机的时候做了一个存档。等下次再来网吧的时候,就到那台机器继续加载存档,继续哈皮。

特点:

  1. 具有线程隔离特点;
  2. 占用的空间非常小,可以忽略不计;
  3. Java 虚拟机规范中唯一一个没有规定任何OOM的区域;
  4. 程序运行的时候,程序计数器是有值的,其记录的是正在执行的字节码的地址;
  5. 执行native 本地方法时,程序计数器的值为空,原因是native是非Java字节码实现,无法统计;

堆是 JVM 内存中最大的一块内存空间,程序中创建的几乎全部对象和数组都分配到了堆中,该内存被所有线程共享,堆内存中不会存放基本数据类型。在JDK1.8后,堆被划分为新生代和老年代。新生代又被进一步划分为 Eden(伊甸园)和 Survivor(幸存者)区。幸存者区又被分为From和To两大部分。具体每个区都存放什么东西,流程是怎样的,我们后面一起看,先看图:

在JDK1.8之前是有永久代的,永久代属于堆的一部分。因为永久代(PermGen)的内存默认为8M,经常爆出OOM异常,而且在回收的时候,回收率很低。所以在JDK1.8以后,被元空间代替了。元空间存储的位置是本地内存(这些内容都可以在Java官网找到)。所以一些面试的时候,在解释堆内存结构之前,要先指定一个版本再说。

虚拟机栈

Java虚拟机栈描述的是Java方法执行的内存结构:每个方法执行的同时会创建一个栈帧。

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的Java虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、运行时常量池和方法返回地址等信息。

当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。

面试基本上要问:栈帧概念、栈帧的出栈、压栈、运行程序的方法必须的栈帧必须在栈顶。还有StackOverflowError这个异常是由于执行程序申请的内存高于栈的内存报错。

特点:

  1. Java虚拟机栈也是线程隔离的,它的生命周期与线程相同(随线程而生,随线程而灭);
  2. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  3. 保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回;

方法区

方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、运行时常量池、字符串常量池。类信息又包括了类的版本、字段、方法、接口和父类等信息。

特点:

  1. 方法区与堆一样,是各个线程共享的内存区域;
  2. 在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
  3. 跟堆空间一样,可以选择固定大小或者可扩展;
  4. 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,抛出OOM异常;
  5. 关闭JVM就会释放这个区域的内存;

本地方法栈

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

JVM运行机制

上面跟大家介绍了JVM内存结构所涉及到的概念,现在就让我们用代码实例看下,我们平常写的代码是怎样在JVM中运行的。传统手艺:上代码!

根据代码我我们分析下整个JVM加载class的整个流程。

  1. JVM 通过本身的配置向操作系统申请内存,根据内存大小找到操作系统的内存分配表,然后把内存段的起始地址和终止地址分配给 JVM,之后JVM进行内部分配;
  2. JVM根据启动参数分配堆、栈以及方法区的内存大小;
  3. class 文件加载、验证、准备以及解析,为静态变量分配内存;
  4. 初始化阶段。在这个阶段中,JVM 首先会执行构造器clinit方法,编译器会在.Java 文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为clinit方法;
  5. 执行方法,启动主线程,执行main方法,在堆内存中创建People对象,赋值;
  6. 创建JvmRunDemo对象,调用code非静态方法,code方法属于对象 JvmRunDemo,此时code方法入栈,并通过栈中的people引用调用堆中的People对象;之后,调用静态方法showPeopleInfo,静态方法属于JvmRunDemo类,之后放入到栈中也是通过 people 引用调用堆中的 People对象。

结尾

今天给大家分享了JVM的一些核心概念,并大概讲述了JVM加载class的过程,由于内容很多,篇幅有限。这篇就介绍这么多,下篇会逐步进行拆分讲解,并结合实际代码和代码反编译手段,深入讲解各个环节。这部分内容属于Java语言的核心基础,希望大家持续关注,谢谢。

往期文章:

《【内功修炼系列1】线性数据结构(上篇)》

《【内功修炼系列1】线性数据结构(下篇1)》

《【内功修炼系列1】线性数据结构(下篇2)》

重要消息:系列规划

上周末分享了内功修炼系列1的内容上下三篇文章,花了不少时间做整理,也占用了自己的休息时间。虽说内容是基础知识点,自己也比较熟悉,但是整理过一遍,自己还是有不少收获。这就是初中语文老师说的“温故而知新”,“书读百遍其义自见”的精髓吧。从文章的阅读量和留言,发现看了的同学还是有收获的,这也是我比较高兴的点。这也是我写文章的根本意义:让每个读者都有所收获。

有些同学可能认为写的这些东西太基础了,工作中可能比较少关注,甚至有些东西用不到。我也仔细想了一下,确实不同阶段的同学,迫切需求点不同。

  1. 对于刚毕业同学,可能就是想把东西先熟练用起来,基础东西再说。
  2. 对于2-3年开发经验的同学,可能就为了面试,疯狂刷题,基础知识点看起来比较有用。
  3. 对于架构师来说,这些东西可能太浅了,不是我工作中架构思维的点。

我在公司的内部分享中,也经常提到,一次分享是不可能覆盖到所有人的需求,除非是“员工手册”。。大家只能从分享的内容中自己筛选、思考,从中获取自己需要的东西。毕竟软件开发技术,基本主线概念和思维方式是想通的。

针对以上提到的这些,我计划出品三个系列《内功修炼系列》、《实用工具系列》、《高屋建瓴系列》。

  • 【内功修炼系列】主要是针对Java基础内容分享系列,意在加深对基础的理解;
  • 【实用工具系列】主要针对日常开发用到的工具、中间件等,帮助大家提高效率,解决实际问题;
  • 【高屋建瓴系列】从架构思维出发,对于现有业务举例,分享架构搭建方法;

这些系列主题,我会进行穿插分享。希望大家可以持续关注,我出品的这些系列都是保证原创,分享个人多年的开发经验和积累。甚至可能有些地方观点有误,还请大家多多指教。你们的支持才是我最大的动力!感谢。

特别声明

本文为原创文章,如需转载可与我联系,标明出处。谢谢!