【JVM】内存结构

若无特殊说明,本文内容都是针对 HotSpot 虚拟机而言。

一、程序计数器

1、作用
程序计数器用来指示当前线程执行到哪一条JVM指令。
2、特点
①线程私有的。②不会内存溢出。

补充:java程序源代码到CPU执行的过程:
我们编写了源代码 → 源代码经过编译之后变成class文件 → class文件再经过JVM中的解释器变成机器码 → CPU执行机器码。如下图所示:
在这里插入图片描述
二、虚拟机栈

1、作用
线程调用Java方法时需要的内存空间,存放了局部变量表、操作数栈、动态链接、方法返回地址等。
虚拟机栈中存的是一个个栈帧,每个方法对应一个栈帧。 当我们调用一个方法时,就将一个栈帧入栈;当方法运行结束时,就将该栈帧出栈。
栈帧中存储了方法参数、局部变量、返回地址等内容。
如下图所示:
在这里插入图片描述
总结一下:
在这里插入图片描述
补充:虚拟机栈是线程私有的。
2、问题辨析

  • 垃圾回收是否涉及栈内存?
    不涉及。因为栈帧在方法被调用结束之后就会自动消失了,所以不需要进行垃圾回收。

  • 栈内存分配越大越好吗?
    栈内存并非越大越好,因为物理内存是有限的,因此栈内存的大小会影响到线程数量。比如现在有物理内存512M,当栈内存为1M时,最多可以有512个线程;但如果栈内存为2M,则最多只能由256个线程。
    【例子中的数字不一定百分百跟现实中的虚拟机契合,理解下这个意思就得了。】
    补充:栈内存设大一点的话,递归的深度可以深一些(因为可以存的栈帧多了),不过这不一定就能提升效率。

  • 方法内的局部变量是否线程安全?
    在这里插入图片描述
    要判断变量是否是共享变量,此外,还要判断变量是否有逃离方法的作用范围。

3、栈内存溢出
两种情况会导致栈内存溢出:

  • 栈帧过多(通常是因为递归死循环导致)。
  • 栈帧过大(发生可能性较小)。

P.S.:①可以通过 -Xss 设置栈内存。②栈内存溢出报的错: java.lang.StackOverflowError

4、线程运行诊断
在这里插入图片描述
三、本地方法栈

1、作用
给本地方法的运行提供内存空间。
2、什么是本地方法?
Java中调用的不是用Java编写的代码。

本地方法就是Java代码里面写的native方法,它没有方法体。是为了调用C/C++代码而写的。

补充:本地方法栈是线程私有的。

四、堆

1、作用
存放的是对象,所有通过new关键字创建的对象都会使用堆内存。
2、特点

  • 堆内存是线程共享的,需要考虑线程安全问题。
  • 有垃圾回收机制。

3、堆内存溢出
当对象不能被回收(一直在被使用)时,如果对象一直增大,就可能增大到超出堆内存大小。
堆内存诊断工具:
在这里插入图片描述
此外,还有一个jvirsualvm工具,功能更加强大。

P.S.:①可以通过 -Xmx 设置栈内存。②堆内存溢出报错:java.lang.OutOfMemoryError: Java heap space

五、方法区

1、认识方法区

  • 方法区存储了跟类结构相关的信息,比如:已经被虚拟机加载的类信息、类的成员变量、成员方法数据(参数)、成员方法和构造方法的代码、运行时常量池等。
  • 方法区在JVM启动时被创建。
  • 方法区是线程共享的。
  • 方法区可能导致内存溢出,这时会报错:OutOfMemoryError
  • 方法区是一种规范,但不同JVM的具体实现不太一样。比如方法区规定了方法区应该位于堆空间中,但实际JVM不一定将方法区放在堆空间中。
    以HotSpot虚拟机不同版本对方法区的不同实现为例:
    jdk1.6及以前,HotSpot中的方法区称为永久代:
    永久代中包含了类信息(下图中的Class)、类加载器(ClassLoader)以及运行时常量池,其中运行时常量池还包含了StringTable(字符串常量池)。
    在这里插入图片描述
    jdk1.8之后,HotSpot中的方法区称为元空间:
    与永久代不同的是,元空间位于本地内存(即操作系统内存)中,且StringTable不再被包含在元空间中,而是被放到了堆中。
    在这里插入图片描述

2、方法区内存溢出
以元空间为例:
可以通过设置元空间大小:-XX:MaxMetaspaceSize=8m
内存溢出会报错:java.lang.OutOfMemoryError:Metaspace
P.S.:元空间是位于操作系统内存中的,一般不会内存溢出。

3、运行时常量池
二进制字节码(*.class文件)的组成:类基本信息、常量池、类的方法定义(包含了虚拟机指令)。可通过反编译查看字节码文件:通过指令javap - v

1)什么是常量池:常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
2)什么是运行时常量池:常量池是*.class文件中的。当一个类被加载的时候,其常量池信息就会被放入(内存区域中的)运行时常量池,并把里面的符号地址变为真实地址。

4、StringTable(字符串常量池)
通过反编译来了解StringTable
1)创建字符串
在这里插入图片描述
这句代码反编译后的指令如下:
在这里插入图片描述

  • ldc #2 //这句指令的意思是:到常量池中加载2号位置的信息,这个信息可能是一个常量,也可能是一个引用对象。
  • astore_1 //意思是将刚才从常量池中加载到的数据,存到(虚拟机栈中对应方法的)栈帧中的局部变量表中。

深入了解常量池
①常量池中的信息在class文件被加载进来的时候,就会被放到(内存中的)运行时常量池中。如下图中是部分常量池信息:
在这里插入图片描述
②但这时候我们定义的常量(如下图中的a、b、ab等)还只是常量池中的符号,还没有被实例成Java对象,得等执行到相应代码的时候才进行实例化。比如例子中,执行到String s1 = "a";(也就是虚拟机指令ldc #2)的时候才会把常量池中的符号a变为字符串对象“a”。
之后,这个对象还会被放到StringTable中去。
【补充:以字面量形式创建字符串对象的时候,先到StringTable中找有没有相同内容的对象,如果找到了,就直接使用这个对象;否则就创建对象并且存到StringTable中(当然在astore的时候就会存到栈帧里去了)。】
由此可见,以字面量形式创建的字符串对象是位于StringTable中的。
③StringTable本质是一张哈希表,所以也会保证里面存的对象是唯一的。StringTable的长度是固定的,并且不能进行扩容。

2)字符串拼接(字符串变量拼接)
在这里插入图片描述
这句代码反编译后的指令如下:
在这里插入图片描述
①首先,new一个StringBuilder对象并执行StringBuilder的构造方法(第9 - 13条)。
②然后分别从栈帧中加载变量s1和s2,并执行append方法(第16 - 21条)。
③最后执行StringBuilder的toString()方法,并存入栈帧中的局部变量表(第24 - 27条)。
需要注意的是,StringBuilder的toString()方法,会new一个新的字符串对象,因此最终这个拼接后的字符串是存在堆内存中的。
总结一下,这句代码在底层中实际执行了以下操作:
在这里插入图片描述
3)字符串拼接(字面量形式)
在这里插入图片描述
反编译后:
在这里插入图片描述
javac在编译期间进行了优化,当以字面量形式进行拼接时,拼接后的字符串在编译期就已经能够被确定为“ab”了,因此会直接到StringTable中查找有没有这个对象,有的话就返回,没有的话再新建对象并存入。

4)intern()方法
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池。
JDK1.8版本:
在这里插入图片描述
无论放入是否成功,都会返回StringTable中的字符串对象。

String ab = new String("ab");
String abIn = ab.intern();
System.out.println(ab == abIn); // true
System.out.println(ab == "ab"); // true

调用intern()方法将字符串"ab"放入StringTable之后,变量ab指向的字符串对象和StringTable中(刚被放入的)对象是同一个。

补充:JDK1.6版本的intern()方法:
在这里插入图片描述
5)StringTable内存回收

6)StringTable调优
①通过-XX:StringTableSize可以设置StringTable的桶个数(前面提到StringTable是哈希表的结构)
②考虑是否需要将字符串对象入池:可以通过intern方法减少重复入池。

六、直接内存(Direct Memory)

1、认识直接内存
直接内存的3个特点:
在这里插入图片描述
要理解直接内存,我们需要先了解Java读写文件的过程。
没有使用直接内存的情况:
如下图所示,Java在读写文件时,必须通过调用操作系统提供的接口来执行,因此CPU会从用户态切换到内核态。
而操作系统在读取文件时,会先将文件内容读取到系统缓冲区中。但Java无法访问这个系统缓冲区,因此Java在堆内存中开辟了一个区域,叫做java缓冲区,每次读取文件的时候,文件内容都需要从系统缓冲区被复制到Java缓冲区中,这样Java程序才能读取到文件内容。
在这里插入图片描述
使用直接内存的情况:
没有使用直接内存时,每次读取文件都要进行一次文件内容的复制(从系统缓冲区复制到Java缓冲区),这个复制操作造成了性能不必要的浪费。
因此,我们使用直接内存来解决这个问题,如下图所示:在系统内存中开辟一块叫做直接内存的区域,这块区域能够被操作系统和Java程序共享。因此,只要操作系统将文件内容读取到这里,Java程序就能够直接访问到文件内容,无需再将文件内容从系统内存复制到Java堆内存。
在这里插入图片描述

2、使用直接内存
1)申请直接内存:调用ByteBuffer的allocateDirect()方法:
在这里插入图片描述
2)释放直接内存:
释放内存的原理:直接内存是不受JVM管理的,因此直接内存不是通过JVM的垃圾回收来释放,而是通过主动调用过Unsafe类的freeMemory()方法来手动释放。
此外,ByteBuffer的实现类内部使用了一个Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被JVM垃圾回收掉了,就会有线程主动调用clean()方法(这个clean方法本质是调用了Unsafe类的freeMemory()方法)来释放内存。
总结一下:
在这里插入图片描述
补充:如果我们禁用了主动GC,那么ByteBuffer对象可能不会被GC掉,从而导致直接内存释放不了。这种情况下如果我们还要释放直接内存的话,可以在代码中直接调用Unsafe类的freeMemory()代码。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值