[JAVA笔记]-基础-类加载过程和JVM虚拟机的内存模型

类加载过程和JVM虚拟机的内存模型

为什么需要了解JVM内存模型?

  • JVM 内存模型和开发Java并发多线程应用程序息息相关
  • 掌握它有助于我们更好地开发Java并发多线程程序

为什么这么说呢?

image

  • 如上图所示,一个*.java 经过javac.exe 编译器编译成了*.class.
  • 然后由类加载器将其装载到运行时数据区的各个模块
  • 这个运行时数据区就是我们要关注的重点。

类的完整加载过程如下:

image

  • 一个*.java 经过javac.exe 编译器编译成了*.class.
  • 类加载器将*.class 的所有相关数据加载到运行时数据区
  • 然后交给JVM执行引擎执行
  • 运行时数据区的各个模块就是JVM内存模型的核心
  • 如果明白我们的程序数据是如何存放读取和运行的,将有助于我们更好地开发并发多线程程序以及处理开发中出现的一些问题。

类的加载过程解析

好了,我们先来详细了解下类的加载过程. image

  • 1.*.java 文件通过 Javac.exe编译器编译成 *.class 字节码文件

*.java —>词法解析—>(通过token流) 语法解析—》语义分析—>生成字节码

  • 2.ClassLoader 将*.class 字节码数据加载到运行时数据区
    • 字节码必须通过类加载过程到JVM环境后才能执行,执行有三种方式
      • 1.解释执行
      • 2.JIT编译执行
      • 3.JIT编译和解释混合执行
    • 字节码类加载过程涉及到 三个阶段
      • 1.Load 读取*.class 文件产生二进制流,初步校验类的相关信息,创建实例
      • 2.Link
        • 2.1 验证,更详细的校验,比如数据类型是否正确等
        • 2.2 准备,为静态变量分配内存,并设置默认值
        • 2.3 解析,确保类和方法之间相互引用正确性,完成内存结构布局
      • 3.Init 执行类的构造器方法,虚拟机栈中通过返回值赋值。加载的方式分为编译执行和解释执行
  • 3.最后 JVM 执行引擎调用运行时数据区的数据和本地接口操作本地类库。

JVM内存模型

  • 那么传说中的Java JVM内存模型是什么样的呢?如果好奇跟我一起来看看。 下图是Java JVM 内存模型经典布局: image
  • Java 经典内存模型包括堆,虚拟机栈,程序计数器,本地方法栈,元空间。
    • Java堆(Heap):是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
    • 元空间:元空间就是JVM规范中方法区的实现,方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    • 程序计数器(Program Counter Register):程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
    • 虚拟机栈:JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    • 本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

虚拟机栈,栈帧,操作栈的关系

帮助记忆和理解:

  • 虚拟机栈是JVM的心腹大将,
  • 当前方法的栈帧都是正在战斗的战场
  • 其中的操作栈都是参与战斗的士兵

JDK 1.8 JVM新特性

  • JDK 1.8 同 JDK 1.7 比,最大的差别就是:元数据区取代了永久代。
  • 元空间的本质和永久代类似,都是对 JVM规范中方法区的实现。
  • 不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。 如果上述还是不太理解这句话的意思,我觉得可以看看这张图 image
    • JVM Stack(虚拟机栈)
    • 栈是一个后进先出的数据结构
    • JVM中的虚拟机栈是描述Java方法执行的内存区域,它是线程私有的。
    • 栈中的元素用于支持虚拟机进行方法调用
    • 每个方法从开始调用到执行完成的过程就是栈帧从入栈到出栈的过程。
    • 在活动线程中,只有位于栈顶的帧菜是有效的,成为当前栈帧。
    • 正在执行的方法成为当前方法,栈帧是方法运行的基本结构。
    • 在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。
    • StackOverflowError 表示请求的栈溢出,导致内存耗尽,通常出现在递归方法中
    • Java虚拟机栈由多个栈帧构成
    • 栈帧包括
      • 局部变量表 局部变量表是存放方法参数和局部变量的区域
      • 操作栈
        • 操作栈是一个初始状态为空的桶式结构栈。
        • 在方法的执行过程中,会有各种指令往栈中写入和提取信息。
        • JVM的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈。
        • 字节码指令集的定义都是基于栈类型的
        • 栈的深度在方法元信息的stack属性中
      • 动态链接
        • 每个栈帧中包含一个常量池中对当前方法的引用,目的是支持方法调用过程中的动态链接。
      • 方法返回地址
        • 方法执行有两种退出情况,
          • 第一:正常退出
            • 正常执行到任何方法的返回字节码指令,如RETURN IRETURN,ARETURN等
          • 第二:异常退出 无论何种退出情况,都将返回至方法当前被调用的位置,方法退出的过程相当于弹出当前栈帧。
        • 退出可能有三种方式
          • 返回值压入上层调用栈帧
          • 异常信息抛给能够处理的栈帧
          • PC计数器指向方法调用后的下一条指令
    • Native Method Stacks(本地方法栈)
    • Native Method Stacks(本地方法栈)是线程私有的
    • 虚拟机栈主内,本地方法栈主外,这个内外是相对于JVM来说的。
    • 本地方法栈为Native方法服务
    • 最著名的本地方法是System.currentTimeMillis();
    • 程序计数寄存器(Program Counter Register)
    • 在程序计数寄存器中,Register 的命名来源于CPU的寄存器,CPU只有把数据装载到寄存器才能运行。
    • 每个线程在创建后都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等
    • 线程执行和恢复都要依赖程序计数器
    • 程序计数器在各个线程之间互不影响,此区域也不会发生内存溢出异常

总结:

  • Java 内存模型包括堆,虚拟机栈,程序计数器,本地方法栈,元空间。
    • 堆(Heap):
      • 存储着几乎所有实例对象
      • 堆由垃圾回收器自动回收
      • 堆区由各大线程共享使用
    • 虚拟机栈存放我们写的程序方法和局部变量
    • 程序计数寄存器,线程执行和恢复都要依赖程序计数器
    • 本地方法栈存放JNI调用的本地方法,如 System.currentTimeMillis();
    • 元空间:定义的静态变量,以及一些常量等数据
    • 堆和元空间(元数据区)是线程共享的
    • 虚拟机栈 、本地方法栈、程序计数器都是线程内部私有的 别人总结的: image

JVM 调优

堆Heap

  • (堆区)大小控制
    • 堆区初始值 -Xms256M
    • 堆区最大值 -Xmx1024M

-X 表示JVM运行参数 ms 是memory start简称,代表最小堆容量 mx 是memory max 简称,代表最大堆容量

Heap (堆区)注意事项

线上生产环境需将Xms 和Xmx 设置一样,避免GC调整堆带来的额外压力

Heap (堆区)分类

  • 堆分为两大块:新生代和老年代
    • 新生代就是对象产生之初,
    • 老年代就是快销毁的时候。

Heap (堆区)调优参数

-XX:MaxTenuringThreshold=15 这个参数能配置计数器的值达到某个阈值的时候,对象从新生代晋升至老年代。 如果参数为1,新生代直接移至老年代。默认值为15 -XX:+HeapDumpOnOutOfMemoryError 设置这个参数可以在JVM内存溢出的时候输出堆内信息

Metaspace(元空间)

JDK8 中使用元空间替换永久代 如果出现以下类似的异常

"Exception in thread 'dubbo client x.x connector' java.lang.OutOfMemeoryError:PermGenspace"
  • 修复方案如下:

-XX:MaxPermSize=1280m

总结

  • 关于线程共享还是线程私有隔离
    • 线程共享的有:堆和方法区
    • 线程私有隔离的有: 程序计数器,虚拟机栈,本地方法栈
  • 里面都放了啥?
    • 堆:存放new出来的所有对象实例
    • 程序计数器:存放线程执行到哪一步了
    • 虚拟机栈: 线程中定义的局部变量、操作- 栈、动态链接、方法出口等
    • 方法区:也叫元空间,被虚拟机加载的类信息、静态(static)变量,常量(final),即时编译器编译后的代码等数据。
    • 本地方法栈:存放Native方法
  • 什么是Native方法?

简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。

  • 为什么分为 线程共享和非线程共享的呢?
    • 首先我们熟悉一下一个一般性的 Java 程序的工作过程。一个 Java 源程序文件,会被编译为字节码文件(以 class为扩展名),每个java程序都需要运行在自己的JVM上,然后告知 JVM 程序的运行入口,再被 JVM 通过字节码解释器加载运行。那么程序开始运行后,都是如何涉及到各内存区域的呢?
    • 概括地说来,JVM初始运行的时候都会分配好 Method Area(方法区) 和Heap(堆) ,而JVM 每遇到一个线程,就为其分配一个 Program Counter Register(程序计数器) , VM Stack(虚拟机栈)和Native Method Stack(本地方法栈), 当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。
Java  jvm 
更新时间:2019-10-18 14:27:35

本文由 Alicyu 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.alicyu.com/archives/classload
最后更新:2019-10-18 14:27:35

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×