• JVM概述和内存管理(未完待续)


    一. JVM概述

    1. Java代码的运行过程

    在这里插入图片描述

    1. 首先代码会被javac编译器 编译 成.class字节码(类的基本信息、常量池、方法定义)存于方法区;
    2. 然后字节码被JVM通过(方法区的)类加载器进行 加载 到方法区(元空间);
    3. 接着字节码中的方法定义中的虚拟机指令被解释器 解释 成机器码(根据不同的平台生成不同的机器码),这个过程会去常量池中查表;
    4. 然后分配执行这段指令需要的资源—主要是内存。
    5. CPU执行指令把结果写回内存了

    2. Java跨平台运行如何实现 ?

    在这里插入图片描述

    为了实现平台无关性,java的编译器javac并不是将java的源程序直接编译成与平台相关的汇编指令,而是编译成一种中间语言,即java的class字节码文件
    字节码是可以跨平台运行的代码,通过不同平台上的JVM虚拟机生成对应平台(Linux、Windows、Android等)所需要的机器码(JVM运行于硬件层之上,屏蔽各种平台的差异性)

    3. JVM 和用户进程的关系 ?

    java my.class
    
    • 1

    运行以上的一个java程序,对应创建JVM 来加载和运行你的 Java 类;

    过程:
    操作系统会创建一个进程来执行这个java可执行程序,
    而每个进程都有自己的虚拟地址空间
    JVM 用到的内存(包括堆、栈和方法区)就是从进程的虚拟地址空间上分配的。

    在这里插入图片描述
    需要注意的是,JVM 内存只是进程空间的一部分,除此之外进程空间内还有代码段、数据段、内存映射区、内核空间等。
    JVM 的角度看,JVM 内存之外的部分叫作本地内存;

    4. JVM结构

    在这里插入图片描述
    Java源代码编译成二进制字节码,经过类加载器加载到JVM中去运行
    类放在方法区,实例对象在堆内存,而堆中的方法在调用时会用到 虚拟机栈、程序计数器、本地方法栈
    然后方法执行时的每行代码是由 执行引擎中的解释器进行逐行执行
    热点代码会被JIT即时编译器来编译,即优化后的执行
    GC会对堆中不再被引用的对象进行回收
    还有一些Java不方便实现的,需要调用底层操作系统的功能需要和本地方法接口打交道

    二. JVM内存管理

    在这里插入图片描述

    线程私有:

    1. 程序计数器

    引入
    在JVM的多线程场景下,多个线程分时交替执行,
    而同一时刻cpu只执行一条指令,那么就会发生上下文切换,也就是说切换线程时要 ①保存当前线程的指令,②然后恢复下一个线程的指令;
    那么就需要每个线程有一个程序计数器,用来记录指令地址,以使得线程切换后恢复到正确的位置;

    概念
    程序计数器是线程私有的一块较小的内存空间,看作是当前线程所执行的字节码的行号指示器
    物理上是通过寄存器来实现的,寄存器是cpu组件中读取速度最快的单元。

    功能⽤于记录当前线程执行的位置(当前线程中执行的字节码指令的地址),从⽽当线程被切换回来(上下文切换)的时候能够知道该线程上次运行到哪儿了。

    特点
    ①线程私有;
    ②程序计数器是JVM中唯一一个不会出现内存溢出的区域(堆、栈、方法区都可能会内存溢出)

    2. 虚拟机栈

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

    每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存;
    栈帧中存有局部变量表,用于存对象引用、基本数据类型;
    但是栈顶只有一个,对应着当前正在执行的那个方法;
    每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈出栈的过程。

    虚拟机栈和本地方法栈为什么是私有的 ?
    为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地⽅法栈是线程私有的。

    垃圾回收是否涉及栈内存 ?
    不会;
    栈内存 存的就是一次次方法调用所产生的栈帧内存,而栈帧内存会随着方法调用结束 栈帧的弹出而释放,所以不需要GC管理。 GC只是回收堆内存中无用的对象。

    栈内存分配越大越好吗 ?
    可以指定栈内存Xss的大小;
    不会。栈越大,反而会让线程数越少。因为物理内存大小是固定的,而栈内存越大,那么线程数就会少! 并不会增加效率。
    在这里插入图片描述

    方法内的局部变量是否线程安全 ?
    即看多个线程对这个变量是共享的还是私有的。而局部变量是线程私有的,所以是线程安全的;
    而如果局部变量是引用数据类型,并且逃逸出了方法的作用范围,需要考虑线程安全的问题。(而变量是基本数据类型的时候是安全的)

    2.1 虚拟栈内存溢出

    什么情况下导致栈内存溢出 ?
    1.一个线程就有一个栈空间,当不断产生线程,栈空间被分配光了,新的线程再无内存可以分配了!报OutOfMemory;
    2.递归时没设置终止条件,方法栈帧不断入栈没有出栈,耗尽了栈空间或者虚拟机限制了压栈的深度,会报StackOverFlow;
    在这里插入图片描述

    3.栈帧太大(不常见),而默认一个栈在1mb左右,

    解决
    1. 线程运行诊断,程序是否出错

    • 例1:cpu占用过多
      top 查看进程监控, 32655进程占用过高
      但是top只能定位到进程,不能定位到线程在这里插入图片描述
      故使用ps命令查看进程以及对应线程:
      ps H -eo pid, tid, %cpu -H 显示进程的层次 -eo显示特定指标 pid 进程id, tid线程id, %cpu占用率
      在这里插入图片描述
      JDK工具列出该进程下的所有线程:
      jstack 32655
      占用过高的是32655线程,十进制显示的线程号,但是jstack中显示的线程号是16进制的!需要换算为7f99
      然后可以找到
      在这里插入图片描述
      然后再去java源码中去修改。 (第8行代码)

    • 例2:迟迟得不到结果,可能是死锁
      使用jstack 命令定位到 死锁:在这里插入图片描述
      2. 调整栈空间大小
      -Xss 为每个线程分配的内存大小;
      在相同物理内存下,减少Xss能生成更多的线程,但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右

    3. 本地方法栈

    本地方法栈就是调用一些native 本地方法 时,需要给本地方法提供的内存。

    本地方法: 不是由Java编写的代码, 由于java不能直接与操作系统底层、硬件打交道,所以需要C、C++编写的本地方法来直接与操作系统底层打交道,java代码就可以间接的靠本地方法调用底层的功能。如clone()方法


    在这里插入图片描述
    native修饰的本地方法,且没有方法体。 java通过调用本地方法去间接调用底层的功能

    线程共享:

    1. Heap堆 / GC堆

    通过new关键字创建对象都会使用堆内存,堆用来存放对象实体,是GC管理的内存区域;

    特点
    1.是虚拟机所管理的内存中最大的一块
    2.有垃圾回收机制;
    3.Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。
    4.如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError内存溢出异常。

    结构:
    新生代
    老年代

    1.1 堆内存溢出

    java.lang.OutOfMemoryError: Java heap space

    原因
    如果不断的产生对象,且堆中的对象都持有强引用导致无法被GC回收,就会导致堆内存溢出;

    解决:
    1. 检查代码是否出现死锁;
    2. 是否创建了过多过大的对象;
    3. 调节参数
    可以手动设置堆内存大小: -Xmx -Xms
    -Xms JVM初始分配的堆内存
    -Xmx JVM最大允许分配的堆内存,按需分配
    一旦对象容量超过了JAVA堆的初始容量,将会自动扩容到-Xmx大小。

    排查堆内存溢出的问题时,可以把堆内存设置的小一些,今早暴露出堆内存溢出的问题;

    工具:

    1. jmap -heap +进程id (通过jps看进程id)
      在这里插入图片描述
      在这里插入图片描述
    2. jconsole
      在这里插入图片描述在这里插入图片描述

    2. 方法区

    方法区存的是:.class 字节码文件(类基本信息、常量池、类方法定义)、运行时常量池类加载器

    JDK1.6 永久代:
    在这里插入图片描述
    永久代的运行时常量池中有字符串常量池StringTable;
    永久代和堆相互隔离,但物理内存是连续的;

    JDK1.7中,字符串常量池、静态变量从永久代移动到堆中;

    JDK1.8 元空间
    在这里插入图片描述

    为什么JDK8中永久代转化为了元空间 ?
    1.启动时MaxPermSize非堆内存要固定大小,难调优; 若使用默认值MaxPermSize 很容易遇到内存溢出的错误;而元空间使用本地内存,仅受本地内存大小限制
    2.字符串常量池在永久代中,容易内存溢出
    3.Sun公司收购了JRockits,要合并hotspot和JRockits的代码,技术上难,而JRockits没有永久代,故舍弃永久代。

    2.1 方法区内存溢出

    Java.lang.outofMemoryError:PermGen Space
    java.lang.OutOfMemoryError: Metaspace

    原因:
    1.字符串常量池内容太多
    2.当类加载时,类加载器会将字节码文件加载到方法区,而方法区并不会被GC清理;所以如果加载了过多的类,可能导致方法区内存溢出;

    解决: 防止类加载过多导致的溢出
    JDK6:设大PermSizeMaxPermSize 非堆内存
    JDK7:设大Xms、Xmx, 为字符串常量池留空间
    JDK8后:增大 XX:MaxMetaspaceSize ,由于元空间是用本地内存实现的,当你不配置的时候,JVM会动态去分配,你机器的本地内存足够,一般是不会出现内存溢出的;

    2.2 (类 / 静态)常量池

    位置:
    .class二进制字节码包含了:类的基本信息,常量池,类方法定义(虚拟机指令等)

    概念
    常量池就是一张,用于存放编译期间产生的字面量和引用,这部分内容将在类加载后存放到方法区的运行时常量池中;;
    虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量(整数、字符串)等信息

    作用
    给虚拟机指令提供常量符号,根据常量符号可以找到常量;

    2.3 运行时常量池

    位置: 运行时常量池是方法区的一部分;

    常量池是在.class字节码文件中,当被【类加载】,它的常量池信息就会放入运行时常量池(内存中),并把符号引用转变为直接引用(解析);

    2.4 StringTable字符串常量池

    引入:
    字符串常用,而频繁的创建字符串对象,对性能的影响是非常大的,
    所以,用常量池的方式可以很大程度上降低对象创建、分配的次数,从而提升性能。

    本质:
    StringTable串池是长度固定的Hash表,
    当创建字符串对象时,会将字符串对象作为key去串池中查找有没有这个字符串对象,如果没有则放入,如果有则使用;

    位置:
    JDK1.7之前,字符串常量池存于方法区中的运行时常量池;
    JDK1.7及之后字符串常量池被从方法区拿到了堆中;

    为什么要将StringTable移到堆中?
    答: 永久代的回收效率很低、周期长,所以对StringTable的回收效率不高;
    而在JDK1.7 之后将其移到堆中,堆中只需要Minor GC就会触发垃圾回收机制;

    StringTable常见问题

    2.5 直接内存

    https://blog.csdn.net/u010515202/article/details/106056592

  • 相关阅读:
    kotlin 注解 @Parcelize 使用
    SpringCloud 简介
    企业自建应用对接企业微信发送消息接口
    vue3 vite4 安装eslint进行初始化时报错
    pytorch -- torch.nn.Module
    IP地址,子网,掩码的计算
    智能合约升级原理01---起源
    学习ArkTS --页面路由
    软件测试银行项目网上支付接口调用测试实例
    Java-面向对象
  • 原文地址:https://blog.csdn.net/Swofford/article/details/126013034