内存结构
(图源水印)
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
- Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
- Execution engine(执行引擎):执行classes中的指令。
- Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
- Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
1.程序计数器
Java源代码—>二进制字节码—>解释器—>机器码—>CPU
-
作用:记住下一条JVM指令的执行地址,根据地址信息找到指令来执行,通过改变计数器的值来选取下一条需要执行的字节码指令
-
物理上通过寄存器来实现“程序计数器”
-
特点:(1)线程私有:**每个线程有各自的程序计数器**;
Q: 为什么要有程序计数器? A:因为线程不具备记忆功能
(2)唯一不会内存溢出的区
2.虚拟机栈
- 虚拟机栈是线程运行需要的内存空间,是线程私有的。
- 一个栈可以看作是多个栈帧组成的,一个栈帧可以看作是一个方法运行(调用、执行)时所需要的内存,栈帧需要多大的局部变量表、多深的操作数栈在程序编译时确定,并且写入方法表的Code属性中。
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
问题辨析:
-
垃圾回收是否涉及栈内存?
栈内存只有一次次调用产生的栈帧内存,栈帧内存在每一次方法调用之后都会被弹出栈,会被自动回收掉,不需要垃圾回来来管理栈内存。
垃圾回收是回收堆内存中的无用对象,不涉及栈内存。
-
栈内存分配越大越好吗?
栈内存可以通过虚拟机来指定,默认都是1024kb;
栈内存越大会让线程数变少,因为物理内存一定,所以设置的栈内存越大,线程数目会变少。
-
方法内的局部变量是否线程安全?
线程是否安全取决于线程对这个变量是共享的还是这个变量对每个线程是私有的;
每个线程会在栈内分配栈帧来存储局部变量,但对于共享变量需要考虑线程安全。
即如果方法内的局部变量没有逃离方法的作用范围,则是线程安全的。
如果局部变量引用了对象并逃离方法的作用范围,则需要考虑线程安全。
栈内存溢出 StackOverFlowError
-
栈帧过多导致栈内存溢出
eg:因为方法递归调用过多/没有正确的终止条件
两个类之间的循环引用
-
栈帧过大导致栈内存溢出
3.本地方法栈
Java虚拟机调用本地方法(object类里的诸多方法)时需要提供的内存空间
虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
4.堆
通过new关键字创建的对象都会使用堆内存。
特点:
- 是线程共享的,堆中的对象都需要考虑线程安全问题。
- 有垃圾回收机制。
堆内存溢出 OutOfMemoryError
垃圾回收机制针对那些没有被使用的对象,如果不停创建对象且这些对象一直在被使用,则不会出发垃圾回收机制,从而导致堆内存溢出。
堆内存诊断
- jps工具
- jmap工具
- jconsole工具
5.方法区
(图源B站黑马程序员视频)
- 被所有Java虚拟机线程共享;
- 存储的是跟类相关的信息如成员方法、构造器方法、运行时常量池;
- 在虚拟机启动时被创建,且逻辑上是堆的一部分(实现时不一定);
-
1.6和1.8在实现方式上不同:
1.6通过永久代来实现,StringTable在常量池中 1.8通过元空间来实现,StringTable在堆中。 *优化原因:StringTable使用频率高但1.6中永久代效率偏低。 eg:通过不断往StringTable中存入对象且引用来造成内存空间不足。
6.运行时常量池
💡 二进制字节码:类的基本信息、常量池、类方法定义,包含了虚拟机指令
- 常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
- 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会被放入运行时常量池,并将里面的符号地址变为真实地址。
- 常量池中的信息,都会被加载到运行时常量池中,这是还只是常量池中的符号,还没有变成Java中的字符串对象,直到代码执行之后对象才会被创建出来。
- 并且会准备好StringTable一个串池空间,创建完字符串对象如果串池中没有就会放入串池中,如果串池中有,就会使用串池中的对象。
- StringTable是一个hashtable结构,不能扩容,只有对象用到了才会创建,用不到不会创建
public class StringTable {
public static void main(String[] args) {
String s1="a";
String s2="b";
String s3="ab";
String s4=s1+s2;
//创建一个StringBuilder对象 无参构造 StringBuilder.append("a").append("b").toString()
String s5="a"+"b";
//javac在编译期间的优化,在编译期间结果已经可以确定 s4中的s1和s2为变量无法确定
System.out.println(s3==s4); //False
System.out.println(s3==s5); //True
}
}
💡 String.intern():将这个字符串对象尝试放入串池,如果串池中有则不会放入,如果没有则放入串池,并将串池中的对象返回 (针对jdk1.8)
💡 String.intern():将这个字符串对象尝试放入串池,如果串池中有则不会放入,如果没有则把此对象复制一份放入串池,再把串池中的对象返回 (针对jdk1.8)
StringTable性能调优
- -XX:StringTableSize 通过设置桶个数来加快哈希查找从而使读取字符串速度加快;
- 考虑是否将字符串对象入池,减少字符串对内存的占用
7.直接内存
(图源B站黑马程序员视频)
==常见于NIO操作时,用于数据缓冲区;
==分配回收成本高,但读写性能好;
==不受JVM内存回收管理。
- 数据在被内核态处理时,为了避免数据先后经过系统内存和Java堆内存,被拷贝两次而导致传输效率过低从而设置Direct Memory;
- 它是操作系统划分的一块内存,但Java代码可以直接访问;
- 直接内存也存在内存溢出的问题。
参考:
1.《深入理解Java虚拟机》
2.B站黑马程序员视频黑马程序员JVM完整教程