栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JVM学习笔记(一):JVM内存模型

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JVM学习笔记(一):JVM内存模型

文章目录
    • 1、程序计数器
      • 1.1、定义
      • 1.2、作用
      • 1.3、特点
    • 2、虚拟机栈
      • 2.1、定义
      • 2.2、问题辨析
      • 2.3、栈内存溢出(StackOverflowError)
      • 2.4、线程运行诊断
    • 3、本地方法栈
    • 4、堆
      • 4.1、定义
      • 4.2、堆内存溢出(OutOfMemoryError)
      • 4.3、堆内存诊断
    • 5、方法区
      • 5.1、定义与组成
      • 5.2、方法区内存溢出
      • 5.3、运行时常量池
      • 5.4、常量池和串池的关系
      • 5.5、StringTable特性

1、程序计数器 1.1、定义

Program Counter Register 程序计数器(寄存器)

  • 作用:记住下一条jvm指令的执行地址
  • 特点:1.是线程私有的,2.不会存在内存溢出
1.2、作用


如图所示,右列代码就是我们所说的源代码,而左列就是经过jdk编译得到的二进制字节码(JVM指令)。字节码通过解释器解释为机器码,最终才会被CPU运行。

那这跟程序计数器有什么关系呢?

在这个过程中,我们的程序计数器会记住JVM指令的地址(也就是左列中的数字),程序计数器拿到地址后,解释器才会将程序计数器记录的JVM指令进行执行,然后程序计数器继续获取下一条JVM指令,直到执行完所有的JVM指令,所以程序计数器的作用是:记住下一条jvm指令的执行地址,如果没有程序计数器,解释器根本不知道接下来执行哪条JVM命令

在物理上,实现程序计数器是通过寄存器,它是CPU组件中读取速度最快的单元!所以JVM在设计的时候,就把CPU中的寄存器当作程序计数器!

1.3、特点

众所周知,Java语言是支持多线程的,当线程1执行JVM指令时,假设执行到第9条JVM指令,恰巧此时CPU切换到线程2,不在继续执行当前JVM指令,这时程序计数器就会把下一条JVM指令(第10条)记住,而且程序计数器与线程1是私有的!如果线程2执行过程中,线程1抢到时间片(CPU执行权),恢复运行时就知道自己接下来应该执行第10条JVM指令。

每个线程都有自己的私有的程序计数器,因为他们各自执行代码的指令地址不同!这是程序计数器第一个特点

第二个特点是:程序计数器是JVM中唯一个不会存在内存溢出的区域!

2、虚拟机栈 2.1、定义

栈数据结构的特点是:先进后出

那么虚拟机栈是干嘛用的?
我们的Java程序线程运行时候,需要为每一个线程划分内存空间。所以虚拟机栈可以理解为:线程运行时需要的内存空间。

一个线程运行时需要一个栈,将来多个线程运行时候就需要多个虚拟机栈。

每个栈内由多个栈帧组成。一个栈帧对应着一次方法的调用,每个方法运行时需要的内存,我们就称之为栈帧。

当线程调用一个方法时,就会把栈帧放入栈中,运行完方法后,就会把栈帧释放。一个虚拟机栈对应多个栈帧。

每个线程只能有一个活动栈帧,就是当前正在执行的那个方法。

2.2、问题辨析
  1. 垃圾回收是否会涉及栈内存?
    不会,因为栈内存实际上就是一次次方法调用产生的栈帧内存,栈帧内存在对应的方法调用结束后弹出栈,就是自动被释放回收,所以不需要垃圾回收管理我们的栈内存。垃圾回收只是回收堆内存中的无用对象,而栈内存不需要进行垃圾回收处理

  2. 栈内存分配越大越好吗?
    栈内存可以通过运行代码时,通过虚拟机参数来指定。栈内存划分的越大会让线程数变少。为什么?
    因为我们的物理内存(我的电脑物理内存为16GB)大小是一定的,一个线程使用的是栈内存,一个线程假设使用1mb内存,物理内存假设有500mb,理论上可以有500个线程同时运行。如果我们栈内存变大导致线程使用内存变大为2mb,那么只能同时运行250个线程。
    所以栈内存并不是越大越好,它的空间划分大了,线程占用空间也大,物理空间是固定的,导致线程数量就会变少!一般采用系统默认栈内存大小

  3. 方法内局部变量是否是线程安全?
    判断这个问题只需要看局部变量是多个线程共享,还是单个线程私有。
    如果方法内局部变量没有逃离方法的作用范围,就是线程安全的。
    如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

2.3、栈内存溢出(StackOverflowError)
  • 栈帧过多(方法递归调用)

  • 栈帧过大

    我们可以手动设置栈内存大小:

2.4、线程运行诊断

案例:CPU占用过高

定位方式:

  • 用top命令定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack 进程id:查看到进程中所有线程的信息,可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号
3、本地方法栈

该方法栈的作用:为本地方法运行提供内存空间

例如Object类中有很多本地方法(native修饰),此类方法并没有被具体实现,而是仅仅提供一个接口,调用C、C++中的代码

代码展示:

public class Object {

    public final native Class getClass();

    public native int hashCode();
    
    protected native Object clone() throws CloneNotSupportedException;

    public final native void notify();

    public final native void notifyAll();

    public final native void wait(long timeout) throws InterruptedException;

    protected void finalize() throws Throwable { }
}
4、堆 4.1、定义

上述提到的这些内存模型都是线程私有的,而接下来介绍的堆是线程共享的。

我们通过new关键字创建的对象,都会存放在堆内存中!

特点:

  1. 它是线程共享的,堆内存中的对象都需要考虑线程安全问题
  2. 具有垃圾回收机制
4.2、堆内存溢出(OutOfMemoryError)

可能这里会有疑问:既然堆内存具备垃圾回收机制,那么为什么还会造成堆内存溢出?

因为我们垃圾回收的条件是堆内存中的对象不被引用,然而有时候,我们可能会创建出大量被引用的对象,这时候就不会进行回收,并且不断占用堆内存空间,此时就会造成堆内存溢出问题!

当然堆内存和栈内存一样,都可以通过IDEA手动设置内存大小如下:

4.3、堆内存诊断

首先介绍几个堆内存诊断的工具:

  1. jps:查看当前系统中有哪些 java 进程
  2. jmap:查看堆内存占用情况 jmap - heap 进程id
  3. jconsole:图形界面的,多功能的监测工具,可以连续监测
  4. jvisualVM:可视化的JVM诊断工具

工具使用展示:

  1. 执行jps,查看一下当前程序进程
  2. 执行jmap -heap 进程号,查看一下该进程的内存占用情况

接下来演示一下jconsole工具的使用:

我们在idea终端输入 jconsole就会出现图形化界面的工具,连接上去之后就可以看到堆内存的使用情况了:

5、方法区 5.1、定义与组成


方法区说白了就是用来存储类相关的数据。
在JDK1.6版本,该版本方法区具体的实现是使用永久代
在JDK1.8版本,永久代的实现方式被废弃了! 方法区依旧是一个概念,此时方法区的实现变成了元空间,在此版本中方法区的内存结构不是JVM进行管理的,而是移到本地内存中,所谓本地内存就是操作系统内存。
还有一个区别就是JDK1.8版本的StringTable不会再放到方法区常量池中,而是移到堆内存当中!

5.2、方法区内存溢出
  • 1.8版本之前会出现永久代内存溢出
  • 1.8版本之后会出现元空间内存溢出

1.8版本的元空间默认情况下使用系统内存,不使用JVM内存。而且默认情况下没有设置上限。所以默认情况下执行下方代码很难造成方法去内存溢出:

所以我们需要配置元空间内存参数,设置为8MB:

此时再次运行就会导致方法区内存溢出:

5.3、运行时常量池

不论是1.6还是1.8版本,运行时常量池都会存在于方法区当中。
在1.6版本,运行时常量池中还会包含StringTable(串池)
讲解运行时常量池之前,先了解一下什么是常量池。

常量池就是一张常量表,JVM指令根据这张常量表找到想要执行的类名、方法名、参数类型、字面量等信息。常量池是*.class文件中,当该类被加载,它的常量池信息就会放入到运行时常量池中,并把里面的符号地址转为真实地址。

5.4、常量池和串池的关系

代码如下:

常量池最初存在于字节码文件,运行时常量池的信息,会加载到运行时常量池。ldc #2会把a符号变成"a"字符串对象,同时会准备一个StringTable(数据结构为HashTable,长度固定不能扩容),此时就会在串池中寻找是否有相同的"a",发现没有就会把"a"放入串池,如果有直接使用串池中的对象,串池中的对象是唯一的。

因此类推…

那么String s4 = s1 + s2;,这个新生成的s4是怎么工作的呢?

此时我们可以看到,进行拼接操作,就会创建一个StringBuilder,然后调用多个append(),将"a","b"拼接在一起,最后调用toString(),该方法底层会new String,astore 4表示将转换后的结果存入到s4变量中。

所以s3中的"ab"在串池中,而s4中的"ab"在堆内存中。

那么Stirng s5 = "a" + "b";,这种情况是怎样的呢?

ldc #4代表在常量池中找#4,就是直接拼接好的”ab“,发现串池中已经有"ab",就不会去创建新的对象,结果已经在编译期间确定了。

5.5、StringTable特性
  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
    1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/288601.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号