简化版
完整版
JVM内存主要分为堆、虚拟机栈、本地方法栈、方法区、程序计数器等。
名称 | 特征 | 作用 | 配置参数 | 异常 |
---|---|---|---|---|
程序计数器 | 占用内存小,线程私有 | 字节码行号指示器 | 无 | 无 |
虚拟机栈 | 线程私有,使用连续的内存空间 | java方法存储的内存模型,存储局部变量表、操作数栈、动态链接、方法出口等 | -Xss | OOM,stackOverFlow |
本地方法栈 | 线程私有 | 为虚拟机使用到的本地方法服务,方便与外界环境交互 | 无 | OOM,stackOverFlow |
堆 | 线程共享 | 保存对象实例,所有对象实例都要在堆上分配 | -Xmn-Xms-Xmx | OOM java heap space |
方法区 | 线程共享 | 存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据 | -XX:PermSize:16M-XX:MaxPermSize64M | 1.7 OOM PermGen space1.8 OOM Metaspace |
运行时常量池 | 方法区的一部分 | 字面量及符号引用 |
Java7及以前版本方法区位于永久代中。同时,永久代和堆是相互隔离的,但它们使用的物理内存是连续的。
永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。
在Java7中永久代中存储的部分数据已经开始转移到Java Heap或Native Memory中了。比如,符号引用(Symbols)转移到了Native Memory;字符串常量池(interned strings)转移到了Java Heap;类的静态变量(class statics)转移到了Java Heap。
在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区在1.8之前被称为永久代,1.8使用本地内存的元空间作为方法区的实现,存储的类信息、编译之后的代码数据都直接占用的本地内存(但StringTable还是放在堆中)。
本地内存(Native memory)也称为C-Heap,是供JVM自身进程使用的。当Java Heap空间不足时会触发GC,但Native memory空间不够却不会触发GC。
元空间存在于本地内存,意味着只要本地内存足够,它不会出现像永久代中“java.lang.OutOfMemoryError: PermGen space”这种错误。
默认情况下元空间是可以无限使用本地内存的,但为了不让它如此膨胀,JVM同样提供了参数来限制它使用的使用。
表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。
当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。
更深层的原因还是要合并HotSpot和JRockit的代码,JRockit从来没有所谓的永久代,也不需要开发运维人员设置永久代的大小,但是运行良好。同时也不用担心运行性能问题了,在覆盖到的测试中, 程序启动和运行速度降低不超过1%,但是这点性能损失换来了更大的安全保障。
线程共享,主要用于分配实例对象。
Java堆是垃圾收集器管理的主要地方,因此很多的时候也被称为GC堆,Java堆还可以分为年轻代和老年代,年轻代又可以分为Eden空间、From Survivor空间、To Survivor空间,默认是8:1:1的比例。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
堆是全局共享的,但是会存在一些问题,那就是多个线程在堆上同时申请空间,如果在并发的场景中,两个线程先后把对象引用指向了同一个内存区域,那可能就会出现问题。
解决方案是每个线程在堆中都预先分配一小块内存,然后再给对象分配内存的时候,先在这块“私有内存”进行分配,这块用完之后再去分配新的“私有内存”,这就是TLAB分配。
它是从堆内存划分出来的,有了TLAB技术,堆内存并不是完完全全的线程共享,每个线程在初始化的时候都会去内存中申请一块TLAB。TLAB区域的内存其它线程也是可以读取的,只不过无法在这个区域分配内存而已。
Java对象实例和数组元素不一定都是在堆上分配内存,满足特定的条件的时候,它们可以在栈上分配内存。
JVM中的Java JIT编译器有两个优化,叫做逃逸分析和标量替换。
逃逸分析通过分析对象引用的作用域,来决定对象的分配地方(堆 or 栈)
一个子程序分配了一个对象并且返回了该对象的指针,那么这个对象在整个程序中被访问的地方无法确定,任何调用这个子程序的都可以拿到这个对象的位置,并且调用这个对象,这种情况就是对象逃逸;
若指针存储在全局变量或者其它数据结构中,全局变量也可以在子程序之外被访问到,这种情况也是对象逃逸;
而方法内的局部变量始终在方法内没有逃逸,在方法执行完之后自动销毁,则可将方法变量和对象分配到栈上,不需要垃圾回收的介入,提高系统的性能。
public StringBuilder getBuilder1(String a, String b) {
StringBuilder builder = new StringBuilder(a);
builder.append(b);
// builder通过方法返回值逃逸到外部
return builder;
}
public String getBuilder2(String a, String b) {
StringBuilder builder = new StringBuilder(a);
builder.append(b);
// builder范围维持在方法内部,未逃逸,toString方法会new一个String用来返回
return builder.toString();
}
逃逸分析只是栈上内存分配的前提,接下来还需要进行标量替换才能真正实现降低堆内存的压力。
标量,就是指JVM中无法再细分的数据,比如int、long、reference等。相对地,能够再细分的数据叫做聚合量,比如对象。
如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解为纯标量表示时,程序执行时可能不创建这个对象,而改为直接创建方法使用到的标量来代替。拆散后的标量可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了,相当于对象就在栈上分配了。
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
allocate();
}
System.out.println((System.currentTimeMillis() - start) + " ms");
Thread.sleep(10000);
}
// 聚合量(对象),由两个标量a、b组成
public static class MyObject {
// 标量(原始数据类型)
int a;
double b;
MyObject(int a, double b) {
this.a = a;
this.b = b;
}
}
public static void allocate() {
MyObject myObject = new MyObject(2019, 2019.0);
}
/**
* 通过逃逸分析,JVM会发现myObject没有逃逸出allocate()方法的作用域
* 标量替换过程就会将myObject直接拆解成a和b
**/
static void allocate() {
int a = 2019;
double b = 2019.0;
}
锁消除是Java虚拟机在JIT编译是,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
**锁消除基于分析逃逸基础之上,开启锁消除必须开启逃逸分析
**
线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,无法被其它线程访问到,那该对象的读写就不会存在竞争,对这个变量的同步措施就可以消除掉。单线程中是没有锁竞争。(锁和锁块内的对象不会逃逸出线程就可以把这个同步块取消)。
Java虚拟机栈属于线程私有的,生命周期和线程相同。
虚拟机栈描述方法的执行过程,每个栈帧对应一个方法的入栈和出栈,包含局部变量、操作数栈、动态链接和方法出口。
本地方法栈则是用于执行本地方法的。
**局部变量表:**存放了编译期可知的各种基本数据类型、对象引用(reference类型,它不等同于对象本身,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)。
**操作数栈:**一个后进先出(FILO)的操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maxstack的值。
可以看到Math.java代码反汇编后的信息是一些JVM指令。
代码中的JVM指令,对应栈中局部变量表和操作数栈的操作
动态链接:在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
**方法出口:**存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:正常执行完成、出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
在Java虚拟机规范中,对栈这个区域规定了两种异常状况:
栈内存溢出(StackOverflowError)主要有以下几种情况:
Java虚拟机栈于管理Java方法的调用,而本地方法栈(Native Method Stack)用于管理本地方法的调用。本地方法栈,也是线程私有的。
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
Native 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口。如notify,hashcode,wait等都是native方法。
虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
程序计数器是线程私有,存放每个线程接下来要执行的指令。
通过移位寄存器实现,程序计数器不会内存溢出。
程序计数器占一小块内存空间,就是当前线程的执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
编译:把通过用高级语言编写的源程序通过编译器转变为目标程序。
汇编:把汇编源程序转变为目标程序。
反编译
将可执行的程序经过分析转变为高级语言的源代码格式。
一般我们拿到的jar包都是经过编译后的 .class文件,如何想查看源码,就可以通过一些反编译工具,将 .class 文件逆向成 java源代码。
反汇编
反汇编与反编译原材料都是 .class文件。但是反编译是向上的,即根据编译后的结果,反向得到编译前的源码。
反汇编是将可执行的文件中的二进制经过分析转变为汇编程序。是根据编译后的结果,倒推源码编译的过程,从而看出代码逻辑真实编译、执行过程的每一步,对于性能优化、问题追溯等具有十分强大的帮助。
Java反汇编使用jdk自带的工具——javap。可以通过命令行 输入 :javap -c XXX.class 文件来查看该class文件的编译过程。
参考:https://chenchenchen.blog.csdn.net/article/details/106048793
JVM内存结构中堆和栈的区别
参考:
Java8 和 Java7中JVM内存模型有什么区别:https://www.cnblogs.com/july-sunny/p/12628820.html
Java8中JVM内存结构变了:https://cloud.tencent.com/developer/article/1546965
JVM 内存结构|1.7 1.8 区别详解:https://blog.csdn.net/lovely_girl1126/article/details/106806879
面试官问我平时写的Bug的存储位置:https://mp.weixin.qq.com/s/Hr2JSEZ9HH5TTowjXlbaVQ
Linux命令-查看内存、GC情况及jmap 用法:https://chenchenchen.blog.csdn.net/article/details/106048793