一、JVM概念
1.1、什么是JVM
-
Java Virtual Machine:Java虚拟机,用来保证Java语言跨平台
-
Java虚拟机可以看做是一台抽象的计算机,如同真实的计算机那样,它有自己的指令集以及各种运行时内存区域
-
Java虚拟机与Java语言并没有必然的联系,它只与特定的二进制文件格式(class文件格式所关联)
-
Java 虚拟机就是一个字节码翻译器,它将字节码文件翻译成各个系统对应的机器码,确保字节码文件能在各个系统正确运行
-
所谓java能实现跨平台,是因为在不同平台上运行不同的虚拟机决定的,因此java文件的执行不直接在操作系统上执行,
-
而是通过jvm虚拟机执行,我们可以从这张图看到,JVM并没有直接与硬件打交道,而是与操作系统交互用以执行java程序。
好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态
1.2、JVM组成
1.3、运行流程
这张图是jvm的组成图,分为四部分:
-
类加载器
- 类加载器的作用是加载类文件到内存。比如我们写一个HelloWorld.java的程序,首先使用javac命令进行编译,生成HelloWorld.java的字节码文件,怎样才能执行.class文件呢。就需要用药类加载器将字节码文件加载到内存中,然后通过jvm后续的模块进行加载执行程序。ClassLoader只管加载,至于是否能够执行,则不属于它的负责范围,由执行引擎负责。
-
执行引擎
- 执行引擎也叫解释器,负责解释命令,提交操作系统执行
-
本地接口
- 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有一个聪明的、睿智的调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。目前该方法使用的是越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。
-
运行时数据区
- 运行数据区是整个JVM的重点。我们所有写的程序都被加载到这里,之后才开始运行,Java生态系统如此的繁荣,得益于该区域的优良自治。整个JVM框架由加载器加载文件,然后执行器在内存中处理数据,需要与异构系统交互是可以通过本地接口进行!
二、jvm内存结构
2.1、程序计数器
2.1.1、定义
Program Counter Register 程序计数器(寄存器)
-
程序计数器是一小块的内存区域,可以看做当前线程执行字节码的行号指示器,在虚拟机的概念模型里,字节码解释工作就是通过改变这个计数器的值来选取下一个要执行的字节码指令。
-
比如分支控制,循环控制,跳转,异常等操作,线程恢复等功能都是通过这个计数器来完成。由于jvm的多线程是通过线程的轮流切换并分配处理器执行时间来实现的。因此,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。
-
因此,为了线程切换后能回到正确的执行位置,每条线程都需要自己独有的程序计数器,多条线程计数器之间互不影响,独立存储。我们称这类内存区域为线程私有的内存区域。
-
如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行native方法,则这个计数器则为空(undefined)此内存区域是Java中虚拟机中唯一一个没有规定任何OutOfMemoryError的内存区域。
作用,保存当前执行指令的地址,一旦指令执行,程序计数器将更新到下一条指令
特点
- 是线程私有的
- 不会存在内存溢出
2.2、虚拟机栈
2.2.1、定义
Java Virtual Machine Stacks (Java 虚拟机栈)
-
每个线程运行时所需要的内存,称为虚拟机栈
-
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
-
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
-
与程序计数器一致,Java虚拟机栈也是线程私有的,生命周期与线程相同。
-
虚拟机栈描述的是方法的执行内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,方法出口等信息。每一个方法从执行到结束的过程,就对应一个栈帧从入栈到出栈的过程。
-
局部变量表存放了编译器可知的四类八种基本数据类型,对象引用(refrence),它不等同于对象本身,可能是指向对象起始地址的引用指针。
-
局部变量表的内存分配在编译期已经完成分配了,其中64位长度的long和double会占用两个局部变量空间,其余的数据类型只占一个。当进入一个方法时,这个方法需要在栈中分配多大的内存空间是完全能够确定的,方法运行期间不改变局部变量表的大小。
-
如果线程在栈中申请的深度大于虚拟机所允许的深度,将出现StackOverFlowError异常; 如果虚拟机栈可以动态扩展(当前大部分虚拟机支持动态扩展,当然也允许固定长度的虚拟机栈),如果扩展无法申请到足够的内存,就会抛出OutOfMemoryError异常。
问题辨析
- 方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
2.2.2、栈内存溢出
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
2.2.3、线程运行诊断
案例1: cpu 占用过多
定位
- 用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
- jstack 进程id
- 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
2.2.4、栈帧
组成:局部变量表、操作数栈、动态链接、方法返回地址
局部变量表:
存放局部变量的列表;
一个局部变量可以保存类型为boolean、byte、char、short、float、reference和returnAddress的数据两个局部变量可以保存一个类型为long和double的数据;
局部变量使用索引来进行定位访问,第一个局部变量的索引值为零;
操作数栈:
也称为操作栈,它是一个后进先出的栈;
当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作;
一个完整的方法执行期间往往包含多个这样出栈/入栈的过程;
简单理解, 操作数栈是线程实际的操作台;
动态链接:
简单的理解为指向运行时常量池的引用;
在class文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用来表示的,动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用;
2.3、本地方法栈
本地方法栈的功能和特点类似于虚拟机栈,也是线程私有的。
不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的Java方法
如何去服务native方法?
native方法使用什么语言实现?
怎么组织像栈帧这种为了服务方法的数据结构?
虚拟机规范并未给出强制规定,因此不同的虚拟机实可以进行自由实现
2.4、堆
2.4.1、定义
-
对于大多数应用来说,Java堆(Java Heap)是JVM所管理的内存中最大的一块区域,且Java堆是被所有线程所共享的一片区域,在虚拟机启动时创建。
-
该区域的唯一目的就是存放实例对象,几乎所有的对象实例都在这里分配空间。这一点在JVM规范上描述的是:所有的对象实例以及数组都要在堆上分配空间。
-
Java堆是垃圾收集器管理的管理的主要区域,因此很多时候被称为GC堆。从内存分配的角度讲,由于现在的垃圾回收机制都是分代垃圾回收,所以堆中可以再划分为老年代和新生代,再细的划分为Eden区,Survivor区,其中Survivor区又可细分为From Survivor区和To Survivor区。根据JVM的规范规定,Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。
-
就像我们的磁盘一样,既可以是固定大小的,也可以是可扩展的。不过当前主流的都采用可扩展的策略(采用-Xmx 和 -Xms控制)。如果在堆中没有完成内存分配,且堆也没有可扩展的内存空间,则会抛出OutOfMemoryError异常。
Heap 堆
- 通过 new 关键字,创建对象都会使用堆内存
特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
2.4.2、堆内存溢出
2.4.3、堆内存诊断
- jps 工具
查看当前系统中有哪些 java 进程 - jmap 工具
查看堆内存占用情况 jmap - heap 进程id - jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
案例
垃圾回收后,内存占用仍然很高
2.5、方法区
2.5.1、定义
-
方法区与java堆一样,有各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码等数据。</