.java文件会被编译器编译为.class文件,然后由JVM中的类加载器加载各个类的字节码文件,加载完毕后,交由JVM执行。JVM会用一段空间来存储程序执行期间需要的数据和相关信息,这段空间一般称为Runtime Data Area运行时数据区,也就是JVM内存。
程序计数器是一个记录着当前线程所执行的字节码的行号指示器。
JVM 采用 CPU 时间片轮转算法来调度多线程。当被挂起的线程重新获取到时间片时,它必须知道上次执行到哪里才能继续执行,因此程序计数器就是记录某个线程的字节码执行位置。
OutOfMemoryError
情况的区域。https://www.jianshu.com/p/ecfcc9fb1de7
描述 Java 方法执行的内存模型,用于存储栈帧。
每个线程中调用一个相同或不同的方法,都会创建一个新的栈帧。调用的方法链越多,创建的栈帧越多(递归)。每个方法从调用到执行完成的过程,就对应入栈到出栈的过程。在 Running 线程中,所有的指令都只能针对当前帧(位于栈顶的帧)进行操作。
存储局部变量表、操作数栈、动态连接、方法返回地址、附加信息等信息。
导致栈内存溢出的情况:
StackOverflowError
。OutOfMemoryError
。与虚拟机栈几乎相同,对象是 Native 方法。为虚拟机使用到的 Native 方法服务。JVM 规范中对本地方法栈没有强制规定,不同虚拟机可以自由实现。比如 HotSpot VM 将本地方法栈和 Java 虚拟机栈合二为一。
StackOverflowError
和 OutOfMemoryError
错误。最大的内存空间,被所有线程共享,用来存储对象实例及数组内容。几乎所有的对象实例都会存储在堆中分配。
OutOfMemoryError
异常。JVM 规范把方法区描述为堆的一个逻辑部分,但它有一个别名 Non-Heap(非堆),目的是与 Java 堆区分开来。方法区并不等同于永久代,只是因为 HotSpot VM 使用永久代来实现方法区,对于其他的 Java 虚拟机并不存在永久代概念。
String.intern()
方法。受方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError
异常。Byte
、Short
、Integer
、Long
、Character
、Boolean
都实现了常量池技术, Float
和 Double
则没有实现。 Byte
、Short
、Integer
、Long
、Character
这 5 种整型的包装类也只是在对应值在 -128~127
之间时才可使用对象池。【JDK 6】
永久代物理上是堆的一部分,和新生代,老年代地址是连续的。
【JDK 8】
元空间属于本地内存。
java.lang.OutOfMemoryError: PermGen space
。java.lang.OutOfMemoryError: Metaspace
。逃逸分析是 Java 虚拟机中的一种优化技术,但它并不是直接优化代码,而是为其他优化手段提供优化依据的分析技术。JDK8 默认开启。
逃逸分析的基本行为就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,称为方法逃逸;也可能被外部线程访问到,称为线程逃逸。
对象的三种逃逸状态:
private Object o;
private HashMap<Integer, Object> map = new HashMap<>();
// 给成员变量赋值,发生全局逃逸
public void test1() {
o = new Object();
}
// 存储在已逃逸对象中,发生全局逃逸
public void test2() {
Object o = new Object();
map.put(0, o);
}
// 作为方法返回值,发生全局逃逸
public Object test3() {
return new Object();
}
// 实例引用传递,发生参数逃逸
public void test4() {
Object o = methodPointerEscape();
}
// 纯粹的局部作用域,没有逃逸
public void test5() {
Object o = new Object();
}
把一个 Java 对象拆散,根据程序访问的情况,将其使用到的成员变量恢复到基本数据类型来访问,就叫标量替换。
【标量】
一个数据无法再分解为更小的数据来表示了,Java 虚拟机中的基本数据类型 byte
、short
、int
、long
、boolean
、char
、float
、double
以及 reference
类型等,都不能再进一步分解了,这些就可以称为标量。
【聚合量】
一个数据可以继续分解,就称为聚合量。对象就是最典型的聚合量。
【替换过程】
如果一个对象没有逃逸,则运行时可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来替代。
将对象拆分后,除了可以让对象的成员变量在栈上分配和读写外(栈上存储的数据,有很大概率会被虚拟机分配至物理机器的高速寄存器中存储),还可以为后续的进一步优化手段创造条件。
class User {
int age;
int id;
}
public void test() {
// 由于User对象没有逃逸,且User对象可以被拆分为两个标量
// 因此这个User对象可以被分配在栈中
User user = new User();
// user.id = 1;
}
基于逃逸分析和标量替换。JDK8 默认开启。
【原理】
方法内局部变量对象未发生逃逸,则使用标量替换将该对象分解,并在栈上分配内存,不在堆中分配,分配完成后,继续在调用栈内执行。
方法执行完后自动销毁,线程结束后栈空间被回收,局部变量对象也被回收,不需要 GC ,提高系统性能。
public static void alloc() {
byte[] b = new byte[2];
b[0] = 1;
}
public static void main(String[] args) {
// 短时间内在堆内存中大量创建和销毁对象,会频繁GC,引发内存抖动,最终的执行时间约900ms左右
// 使用栈上分配可以完全避免堆内存的内存抖动,最终的执行时间约6ms左右
for (int i = 0; i < 100000000; i++) {
alloc();
}
}
【使用场景】
对于大量的零散小对象,栈上分配的速度快,可以避免 GC 带来的 Stop The World。但栈空间比较小,因此大对象不适合进行栈上分配。
如果一个对象没有逃逸,对这个变量的同步措施就可以消除掉。单线程中是没有锁竞争。(即锁和锁块内的对象不会逃逸出线程,就可以把这个同步块取消)
public static void alloc() {
byte[] b = new byte[2];
// 不会线程逃逸,所以该同步锁可以去掉
// 开启使用同步消除执行时间 10 ms左右
// 关闭使用同步消除执行时间 3870 ms左右
synchronized (b) {
b[0] = 1;
}
}
public static void main(String[] args) {
for (int i = 0; i < 100000000; i++) {
alloc();
}
}