JVM、(二)JVM内存结构
@
一、程序计数器(PC)
定义: Program counter Register
作用:记住下一条JVM指令的执行地址。(通过寄存器来实现的)

二进制字节码通过解释器被解释为机器码,然后机器码才能够交给CPU去执行。
当第1条JVM指令被解释翻译执行后 程序计数器会去记录下一条JVM指令的执行地址然后接着执行。
特点:
- 线程私有的。程序运行时会开启多个线程,CPU会对这些线程进行调度,每个线程有着自己的时间片。当时间到但是线程1还未执行完,线程1的程序计数器便会记录此时执行到的指令的位置;然后切换到线程2去执行,同理线程2也有自己的程序计数器。
![在这里插入图片描述]()
- 不会存在内存溢出。
- 如果正在执行的是一个Java方法,那么它记录的就是正在执行的虚拟机字节码指令的地址;如果执行的是一个 native 方法,那么它的值应该为空。
二、虚拟机栈(Virtual Stack)
定义:
- 栈 - 线程运行需要的内存空间;由多个栈帧(Frame)组成。
- 栈帧 - 每个方法调用时需要的内存;
- 栈与栈帧的关系 - 每个线程只能有一个活动栈帧,对应着正在执行的那个方法(位于栈顶部的那个栈帧);
- 一个栈中可能存在多个栈帧。
比如说某个方法执行时会需要一些参数、局部变脸、返回地址。这些都被记录在栈帧这个内存空间中。也可能在执行该方法时还调用了其余的方法,此时该栈中就会存储多个栈帧,当方法执行完后栈帧便会出栈,内存自动释放掉。 - 虚拟机栈是线程私有的。
![在这里插入图片描述]()
局部变量表:
局部变量表存在于虚拟机栈中,它存放了编译器可知的各种Java 虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)、returnAddress类型(指向了一条字节码指令的地址)。
- 局部变量表中的存储空间以局部变量槽(slot)来表示(double、long占2个,其余的占1个);
- 当进入一个方法时,这个方法需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
面试问题:
- 垃圾回收是否涉及栈内存?
不涉及,因为栈内存是随着线程中方法执行的结束而弹出栈自动释放掉的。 - 栈内存分配越大越好吗?
不是,因为物理内存的大小是一定的。如果栈内存划分大,那么能够执行的线程数就会减少,它只能够方便进行更多次的方法递归调用,不会增大方法运行的效率。
![在这里插入图片描述]()
- 方法内的局部变量是否是线程安全的?
如果方法内局部变量没有逃离方法的作用范围就是线程安全的;反之不安全,比如局部变量作为方法返回值返回,有可能被其它线程使用。
举例:
![在这里插入图片描述]()
方法1中的 StringBuilder是线程私有的,因为它是作为局部变量出现的;
方法2中的不是线程安全的,因为作为引用类型出现它可能还会被其它线程所引用,比如说在main 方法中开启了新线程调用 m2(StringBuilder sb)方法。
方法3中的 StringBuilder不是线程安全的,因为它将 StringBuilder作为返回值返回,那么其它线程就有可能拿到这个值去修改 。
栈内存溢出
- 栈帧过多(递归爆炸);
- 栈帧过大(不太容易出现);
- 实例:
![在这里插入图片描述]()
![在这里插入图片描述]()
![在这里插入图片描述]()
线程运行诊断
- CPU占用过多:
- 使用
top定位哪个进程对CPU的占用过高; ps H -eo pid,tid,%cpu | grep 进程id(使用ps命令进一步定位是哪个线程引起的cpu占用过高)jstack 进程id获取指定进程的线程运行情况;可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号。
![在这里插入图片描述]()
但是 jstack 打印出的线程编号是 16进制的,需要首先将 十进制的存在问题的线程编号换算为16进制然后去 jstack 打印结果中寻找。
- 程序运行很长时间没有结果
- 比如说死锁问题
![在这里插入图片描述]()
三、本地方法栈
- 概念:Java代码调用本地方法时所占用的内存。给本地方法的运行提供内存空间。
本地方法举例:
![在这里插入图片描述]()
四、堆
介绍
- 概念: 通过 new 关键字创建的对象都会使用到堆内存;堆中只能存储对象的实例。
- 特点:
- 堆是线程共享的,堆中的对象都需要考虑线程安全问题;
- 有垃圾回收机制。
堆内存溢出
- 举例:
![在这里插入图片描述]()
报错:java.lang.OutOfMemoryError:java heap space
可以通过-Xmx堆内存大小来设置程序中堆的内存
![在这里插入图片描述]()
堆内存诊断
- jps 工具: 查看当前系统中有哪些 java进程;
- jmap 工具:查看堆内存占用情况;
- jconsole 工具:图形界面的,多功能的监测工具,可以连续监测;
注意使用时需要保证 项目jdk版本和 idea版本一致。
详细参考jhsdb jmap --heap --pid 进程号** **报错
IntelliJ IDEA设置JDK版本
应用实例1:


jps : 查看系统中的java进程。 如果报错需要切换到 jdk安装目录的bin目录下进行。

jmap -heap 进程id:查看对应进程的堆内存占用情况

堆分配前:

堆分配后:

gc回收后:

应用案例2:

使用 jvisualvm 来监控进程的堆内存分配:




对象
对象创建的完整过程:
1. 类加载检查:java虚拟机遇到一条new 指令时,会首先去检查该指令的参数能否在常量池中定位到一个类符号的引用,并检查这个引用对应的类是否已经被加载、初始化。
2. 对象分配内存:从Java堆中划分一块指定大小的内存块。但是Java的堆中的内存可能存在规整和不规整两种情况。①堆内存规整:堆中空闲部分和已使用部分通过一个指针分隔开,分配时将指针超空闲部分移动指定的距离即可(这种方式称为指针碰撞)。②堆内存不规整:使用和未使用的糅杂在一起,此时虚拟机会维护一个列表,上面记录哪些内存块可用,然后参照列表进行分配。 总的来说,由于堆的内存受到垃圾回收器的管理,因此内存是否规整与垃圾回收器的回收算法相关;比如 Serial、ParNew等垃圾收集器采用的是标记整理方式进行回收,内存是规整的;而 CMS 垃圾回收器采用的是标记清除的方式进行垃圾回收,会产生内存碎片不规整。
- 注意! 这个过程中还存在一个问题,如果是在并发情况下进行对象内存分配,那么指针的移动是否会出现问题?
- 比如说指针正在给对象A分配内存,来不及修改此时对象B又使用原来的指针分配内存。解决该问题有两种方案:①对内存分配的动作采用同步处理,虚拟机正是采用CAS配上失败重试的方法来保证更新操作的原子性;(具体CAS好像与Unsafe 对象相关,后面直接内存部分会提到)。②将内存分配的动作按照线程划分在不同空间进行,每个线程在Java堆中预先划分一定的空间,分配更新时分别进行互不干扰。
3. 将分配到的内存空间初始化为零值:简单来说就是给对象的成员变量设置默认的初始值。
4. 其余必要设置:确定这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
对象的内存布局:
- 对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding);
- 对象头主要包括两类:①对象自身的运行时数据,比如哈希码、GC分代年龄、锁状态标志、线程持有的锁;②类型指针,通过这个指针来确定该对象是哪个类的实例;特别的若对象是一个Java数组,那么还需要一块用于记录数组长度的数据;
- 实例数据记录的是对象的具体有效信息和数据;
- 对齐填充没有什么特别的作用,仅仅起到填充符的作用。
对象的访问定位:
主流的访问方式包括:句柄访问、直接指针访问。
-
句柄访问:Java堆中会额外划分出一块内存充当句柄池,reference 中存储的是对象的句柄地址。而句柄中包含了对象的对象实例数据和对象类型数据的地址;
![在这里插入图片描述]()
-
直接指针访问:reference中存储的直接就是对象的地址,对象的实例数据中包含了对象类型数据的地址。
![在这里插入图片描述]()
五、方法区
构成
-
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
-
方法区与堆一样都是各个线程共享的内存区域;
-
方法区在JVM启动的时候被创建,并且它的实际物理内存空间中和Java堆区一样都可以是不连续的;
-
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:
java.lang.OutOfMemoryError:PermGen space或者java.lang.OutOfMemoryError:Metaspace
![在这里插入图片描述]()
![在这里插入图片描述]()
![在这里插入图片描述]()
- jvm1.6 用了一个永久代(PermGen)作为方法区的实现,永久代中包含了运行时常量池(
StringTable)、类加载器(ClassLoader)、类的相关信息(Class); - jvm1.8 后用元空间代替了永久代作为方法区的实现。元空间和方法区类似,都是对JVM中方法区的实现,不过元空间与永久代的最大区别在于:元空间不在虚拟机设置的内存中、而是使用本地内存(永久代更容易遇到内存溢出的问题,而元空间只要没有触碰到进程可用的内存上限就不会有问题)。不过1.8将 StringTable移动到了堆中。这是因为永久代的回收效率很低(需要 FULLGC时才能触发垃圾回收 )这样会占用大量的内存,而StringTable在堆中只需要 Minor GC时便能够触发垃圾回收,能够大大减轻字符串对内存的占用。
- 运行时常量池 VS 常量池:
方法区中包含了运行时常量池;字节码文件中包含了常量池;一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包含各种字面量和对类型、域和方法的符号引用。 - 为什么需要常量池?
一个Java 源文件中的类、接口 编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存储在字节码中,换另一种方式,可以存到常量池中,这个字节码包含了指向常量池的引用。在动态连接的时候回到运行时常量池。 - 常量池中有什么?
数量值、字符串值、类引用、字段引用、方法引用。 - 运行时常量池:
- 运行时常量池是在jvm虚拟机完成类装载操作之后,将class文件中的常量池载入到内存中,并保存在方法区中。
- 运行时常量池中包含多种不同的常量,包括编译器期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
- 运行时常量池相对于Class文件中的常量池的另一重要特征是:具备动态性。
方法区的内存溢出

通过循环20000次来往方法区中加载多个类的信息,使得方法区内存溢出。

但是只有在 JVM1.6之前才有可能产生方法区内存溢出,因为它使用的是虚拟机的内存,而JVM1.8以后使用的是本地内存很大不会溢出。但是在修改了虚拟机参数后可以做到内存溢出。 -XX:MaxMetaspaceSize=8m

常量池
- 常量池就是一张表,字节码文件中的虚拟机指令根据这张表找到需要执行的类名、方法名、参数类型、字面量等信息。
- 字节码文件中包含了 类的基本信息(版本号、字段),常量池,类方法定义,虚拟机指令。 查看字节码信息:
javap -v xxx.class






运行时常量池
- 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入到运行时常量池,并把里面的符号地址变为真实地址。
StringTable 面试题:

StringTable 实例:
-
基础的
String str = "xxx"
![在这里插入图片描述]()
常量池中的信息,都会被加载到运行时常量池中。开始时 a、b、ab 都是作为常量池中的符号,还没有变为 java字符串对象。等到程序运行到这里时,字节码中的虚拟机指令(ldc)会将该符号变为字符串对象,并且放入到串池(StringTable,是一个 hashtable结构,不能扩容)中。每次都会先到串池中检查是否有对应的字符串,如果不存在再创建然后加入。 -
字符串拼接
String s4 = s1 + s2
![在这里插入图片描述]()
![在这里插入图片描述]()
![在这里插入图片描述]()
因此得到结果为 false -
字符串拼接
String s4 = "a" + "b"
![在这里插入图片描述]()
底层做法:javac在编译期间的优化,结果已经在编译期间确定为 "ab";而String s4 = s1 + s2中s1与 s2均为变量可能在之后发生变化,因此不能提前确定,必须在运行期间使用StringBuilder获取。 -
使用
intern方法主动将字符串放入串池
JDK 1.8下:
![在这里插入图片描述]()
![在这里插入图片描述]()

JDK 1.6下:

s.intern() :
- jdk1.8 下如果字符串已经在堆中生成了,那么只需要在常量池中记录一下首次出现的实例引用。如果没有则放入串池并返回串池中的那个对象。
- 会将字符串s 拷贝一份并将该拷贝的那份放入串池中,而原本的字符串s 如果存在于堆中,此时仍然在堆中。最终返回串池中的对象(1.6中)
StringTable特性:
- 常量池中的字符串仅是符号,第一次用到才变为对象;
- 利用串池的机制,来避免重复创建字符串对象;
- 字符串变量拼接的原理是 StringBuilder(1.8中);
- 字符串常量拼接的原理是编译期优化;
- 可以使用
intern方法,主动将串池中还没有的字符串对象放入串池。
StringTable位置:
1.8以前 StringTable 位于永久代中,需要满足 FULLGC时才能触发垃圾回收,会造成大量内存空间无法被回收;
1.8以后 StringTable 位于堆中,需要满足 MinorGC 时才能触发垃圾回收,因此能够大大减少字符串占用的内存空间,及时回收。
- 证明如下:
![在这里插入图片描述]()
jvm1.6 下循环26000次往 StringTable中放入字符串,同时设置永久代的最大内存空间:-XX:MaxPermSize=10m;
此时会报错OutOfMemoryError :PermGen space,说明1.6中 StringTable确实位于永久代中。

jvm1.8下会报错 GC overhead limit exceeded,这是由于垃圾回收的一个限制导致的UseGCOverheadLimit(如果98%的时间花费到垃圾回收上,但是只有2%的堆空间被回收,就会触发该错误)。

此时需要添加相应的虚拟机参数:-Xmx10m -XX:-UseGCOverheadLimit

如图所示,此时报错 Java heap space 堆空间不足,证明1.8中 StringTable位于堆中。
StringTable垃圾回收机制:
虚拟机配置: -Xmx10m -XX:+PrintStringTableStatisttics -XX:+PrintGCDetails -verbose:gc




StringTable性能调优:
- 调整StringTable 中桶的个数:
- 命令:
-XX:StringTableSize=桶个数(最小1009)
![在这里插入图片描述]()
![在这里插入图片描述]()
![在这里插入图片描述]()
![在这里插入图片描述]()
- 考虑将字符串对象是否入池
![在这里插入图片描述]()
![在这里插入图片描述]()
打开 jvisualvm可以发现String、char数组占用了80%的内存;


可以发现字符串入池后 占用的内存只有30%左右
六、直接内存
介绍
- Direct Memory,直接内存是属于操作系统的内存;
- 常见于 NIO操作时,用于数据缓冲区;
- 分配回收成本较高,但是读写性能高;
- 不受 JVM 内存回收管理;
案例对比



图示

首先Java代码本身不具有读写文件的能力,它需要调用操作系统的方法来进行文件读写。CPU 方面会由用户态切换为内核态;内存方面会在系统内存划分一块系统缓存区,将磁盘文件读取到系统缓冲区然后再将系统缓冲区中的文件数据读取到Java 缓冲区(堆内存分配的)。
但是这样依赖就需要开辟两份缓冲区,这样会造成不必要的数据复制,效率不高。

使用 ByteBuffer 后则会开辟一块Java和系统都能够共享的区域 direct memory(直接内存),这样一来就不用重复复制效率便提高了。
内存释放原理


- 使用
Unsafe对象完成直接内存的分配(allocateMemory)回收(freeMemory),并且回收需要主动调用freeMemory方法。 ByteBuffer的实现类内部,使用了Cleaner(虚引用) 来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean 方法调用freeMemory来释放直接内存。
- 源码分析:
![在这里插入图片描述]()
![在这里插入图片描述]()
![在这里插入图片描述]()
![在这里插入图片描述]()

禁用显示回收对直接内存的影响
一般会禁用 System.gc() 来避免显式的进行垃圾回收。但是显式gc被禁用 ByteBuffer就不能被回收,相应的直接内存就得不到释放。因此一般推荐使用 UnSafe 对象直接 freeMemory 进行释放。
public class test2 {
static int _1GB = 1024*1024*1024;
//反射获取UnSafe 实例对象
public static Unsafe getUnsafe(){
Unsafe unsafe = null;
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return unsafe;
}
//分配直接内存并且回收
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
//分配内存
long base = unsafe.allocateMemory(_1GB);
unsafe.setMemory(base,_1GB,(byte) 0);
System.in.read();
//释放内存
unsafe.freeMemory(base);
System.in.read();
}
}
参考文章:
JVM 完整深入解析
JVM系列(二) - JVM内存区域详解
JVM --方法区(超详细)
Java常量池详解




































浙公网安备 33010602011771号