注:栈内存、堆内存诊断都没学

概念

定义

java二进制字节码的运行环境

好处

  • 一次编写,到处运行

    • 消除了字节码和底层操作系统之间的的差异
    • 对外提供了一个一致的运行环境
    • jvm可以用解释的方法来执行二进制字节码来达到代码的平台无关性
  • 自动内存管理,垃圾回收功能

  • 数组下标越界越界检查

    • 不会覆盖其他代码的内存
  • 多态

    • 提升扩展性
    • 使用虚方法表的方式实现

概念对比

JVM: Java虚拟机

JRE: Java的运行时环境 <= JVM + 基础类库

JDK: JRE基础上加上一些javac,javap,内存监测工具什么的 <= JVM + 基础类库 + 编译工具

流程

先通过类加载器加载到JVM内存结构

JVM内存结构包括:

  • 方法区: 存放类
  • :存放类的实例对象
  • 虚拟机栈:实例对象调用方法时用到
  • 程序计数器:实例对象调用方法时用到
  • 本地方法栈:实例对象调用方法时用到

接下来到了执行引擎:

  • 方法执行时,每行代码由解释器进行逐行执行
  • 方法内的热点代码,也就是被频繁调用的代码,会被即时编译器进行编译。

可以竟是看做为优化代码

  • GC垃圾回收里面不再被引用的模块进行垃圾回收

还有一些是Java代码无法实现,需要调用底层代码的功能:

  • 通过本地方法接口调用操作系统的一些功能方法

内存结构

程序计数器

引言

JVM指令 => 解释器 => 机器码 => CPU

  • Java源代码经过编译,成为二进制字节码(JVM指令),所有平台下都是一致的
  • 接下来每一条JVM指令都被解释器(java虚拟机执行引擎的一个组件)解释成一条机器码
  • CPU拿着机器码执行命令

CPU只认机器码

作用

程序计数器负责在一些JVM指令的执行过程中,记住下一条JVM指令的执行地址

当JVM指令最后变成机器码被CPU执行后,解释器就去程序计数器找要执行的指令地址

物理上程序计数器是通过一个叫寄存器的东西实现的,程序计数器是对寄存器的一些屏蔽和抽象

寄存器是整个CPU读取速度最快的单元(因为读取非常频繁)

使用Java虚拟机在手机的时候,就将CPU的寄存器作为程序计数器来存储地址

特点

  • 线程私有

在多个线程运行的时候,CPU会有一个调度器组件来给每个线程分配时间片

如果线程1的代码在时间片内没有执行完,线程1的代码就会被暂存,CPU资源切换到线程2

线程2的时间片用完了再切换

在线程切换的改成中,如果要记住下一条指令执行到哪里了,就要用到程序计数器

程序计数器现在就属于被暂存的线程

每个线程都有自己的程序计数器()因为各自执行的地址是不一样的

  • JVM规范中唯一一个不会存在内存溢出的区

虚拟机栈

作用

  • 每个线程运行时所需要的内存,称为虚拟机栈

一个栈内由多个栈帧组成

一个栈帧对应一次方法的调用

线程执行代码,代码由多个方法组成,每个方法运行需要的内存就是栈帧

栈帧的内存存着方法参数,局部变量,返回地址

方法1被调用,分配栈帧1,栈帧1入栈;方法1调用方法2,分配栈帧2,栈帧2入栈…方法2执行完,栈帧2出栈。出栈就是释放内存

  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

注意点

  • 出栈自动回收内存,不需要用到GC,GC是堆用的

  • 栈内存Linux、macOS、Oracle Solaris默认1m

  • 栈内存不是越大越好,因为物理内存是固定的,单个栈内存大了,可以容纳的线程数量就少了

  • 局部变量是线程安全的,因为俩线程调用一个方法,各自都有各自的局部变量

  • 但是全局变量(static)对各个方法是共享的。静态变量值的改变发生在方法调用完毕之后。当方法内部进行静态变量的修改时,实际上只是修改了该方法中对静态变量的局部变量,这并不会影响在内存中存储的静态变量值。当方法执行完毕后,再次访问该静态变量时,才能看到被更改的值。因此,静态变量值的改变情况都发生在方法调用完毕之后。

  • 判断方法内局部变量是否线程安全:

    • 没有进出:不是通过参数引用进来,没有return出去
    • 基本类型当然是线程安全的

栈内存溢出的场景

  • 栈帧过多,只入不出(错误的递归调用)

比方说一个部门类和员工类,部门类有一个员工类的List,员工类有一个自己所属部门的属性

转换成json时便会出现循环依赖

此时可以在员工类的部门字段上加上@JsonIgnore来忽略这个属性

  • 栈帧过大(理论上才会存在?)

本地方法栈

分配给不是由Java编写的代码

作用

通过new关键字,创建对象都会使用堆内存

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

堆内存溢出

  • 集合内元素在集合作用范围内无法被垃圾回收,如果元素过多过大会导致堆内存溢出
  • 垃圾回收后,内存占用仍然很高

方法区

存储类的信息

内存溢出

  • 1.8以前会导致永久代内存溢出
  • 1.8之后会导致元空间内存溢出