JVM概述和内存管理

导读:本篇文章讲解 JVM概述和内存管理,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一. 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

运行以上的一个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()、hashCode()方法


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

线程共享:

1. Heap堆 / GC堆

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

特点
1.是虚拟机所管理的内存中最大的一块
2.有GC垃圾回收机制;
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)
    可以看到堆内存(Eden、幸存区、老年代等)和元空间的内存大小
    在这里插入图片描述
    在这里插入图片描述
  2. jconsole
    在这里插入图片描述在这里插入图片描述

2. 方法区

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

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

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

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

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

2.1 方法区内存溢出

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

原因:
1.字符串常量池内容太多(JDK 1.6)
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 直接内存

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。
这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据;
在这里插入图片描述

直接内存在JVM虚拟机内存中创建了一个指针引用,指针指向的数据存储于JVM之外的本地内存上;

特点:
1.常见于 NIO 操作时,用于数据缓冲区;
2.分配回收成本较高,但读写性能高(不需要java和内存的相互拷贝,直接物理地址映射);
3.不受 JVM 内存回收管理;

非直接内存一般需要两次拷贝,从磁盘到内核缓冲区,在到应用层的缓冲区;修改数据后再拷贝两次到磁盘/网卡;
在这里插入图片描述
而直接内存:
在这里插入图片描述
会在操作系统划出一块缓冲区,而这块缓冲区,操作系统和Java共享! Java可以直接操作修改这块直接内存的数据
类似Kafka中的零拷贝,然后可由这块直接内存拷贝至 磁盘/网卡;

直接内存 是 零拷贝 的 实现原理;

优点:
避免操作系统中内核态和用户态之间反复切换,实现文件的高效存取,读写I/O性能高

缺点:
没有自动回收机制的管理,可能内存溢出;

2.5.1 直接内存的分配与回收

分配:
DirectByteBuffer构造函数中,使用了 UNSAFE对象的allocateMemory()和setMemory()来对直接内存的分配;
回收需要主动调用freeMemory() 方法
在这里插入图片描述

回收:
DirectByteBuffer的构造函数中有Cleaner虚引用,用于检测byteBuffer对象,一旦byteBuffer对象(堆中)被GC回收,那么就通过Cleaner的clean()方法,最终调用UNSAFE.freeMeomry方法释放之前申请的直接内存;
也可以说直接内存的回收要依赖于堆中byteBuffer对象是否被回收;
在这里插入图片描述

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/89221.html

(0)
小半的头像小半

相关推荐

半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!