JVM,是Java Virtual Machine(Java虚拟机)的缩写,要完全弄明白JVM,可能需要花很多时间去学习、研究。
胖子语录:点成线,线成面,切勿贪心,否则一脸懵逼
我们先了解、弄清楚以下几点,剩下的,读者自行深造。推荐纸质书《深入理解Java虚拟机》or深入理解Java虚拟机,建议一样来一发,要雨露均沾,同时加深印象,虽然内容一样的。
1.Java内存区域与内存溢出异常
1.1 JVM运行时数据区
1.1.1 程序计数器
程序计数器(线程私有),占一个非常小的内存空间。它可以看成当前线程所执行的字节码解释器的行号指示器。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,如:分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖它完成。
注意这里:
- 如是线程执行的是Java方法的话,计数器记录的是正在执行的虚拟字节码指令的地址;如果线程执行的是native方法(至于什么叫native方法,读者自行查阅),则计数器记录的为undefined。
- 程序计数器的内存区域,是唯一一个JVM规范中,没有规定任何OutOfMemoryError情况的区域。
1.1.2 JVM栈
描述的是Java方法执行的动态内存模型
栈帧:每个方法在执行时,都会创建一个栈帧,用来保存:局部变量表、操作数栈、动态链接、方法出口等。方法执行完毕,栈帧销毁。
所以,每个方法从调用到执行结束,对应着该栈帧从JVM栈入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本数据类型、引用类型、返回地址类型(指向了下一条字节码的地址)。局部变量表的大小,在编译期已经可以确定,在运行时期不会发生改变。
栈的大小:
- StackOverFlowError:线程请求的栈深度大于JVM所允许的深度,报该错误
- OutOfMemoryError:如果JVM栈可以动态扩展,而扩展时无法申请到足够的内存,报该错误
1.1.3 本地方法栈
JVM栈为虚拟机执行Java方法(也就是字节码)服务的。JVM虚拟机实现可能需要C Stacks来支持Native语言,这个C Stacks就是本地方法栈,本地方法栈是为虚拟机使用到的Native方法服务的。
1.1.4 Java Heap(Java 堆)
对于大多数应用来说,这块区域是JVM所管理的内存中最大的一块。线程共享,主要存放实力对象和数组,几乎所有的对象实例都会在这里分配内存。内存会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB),可以位于物理位置上不连续的空间,但是逻辑位置要连续。
Java Heap存储的对象,被垃圾收集器管理,这些受管理的对象,无法显式的销毁。
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
1.1.5 方法区
所有线程共享的运行时内存区域。用来储存已被JVM加载的类信息、运行时常量池、字段、方法信息、静态变量、即时编译器编译后的代码等数据。方法区是Java Heap的逻辑组成部分,它一样是物理上不需要连续,而且可以选择在方法区中不实现垃圾收集。
运行时常量池:用于存放编译期生成的各种字面量和符号引用,编译器和运行期(String 的 intern() )都可以将常量放入池中,内存有限,无法申请时抛出OutOfMemoryError。
1.2 HotSpot虚拟机
主要介绍数据是如何创建、如何布局以及如何访问的。
1.2.1 对象的创建
当遇到new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载。
类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空间内存中划分一块区域(通过“指针碰撞-内存规整”或“空闲列表-内存交错”的分配方式)。
因为每个线程在堆中都会有私有的分配缓冲区(TLAB),这样可以很大程序避免在并发情况下频繁创建对象造成的线程不安全。
内存空间分配完成后会初始化为0(不包含对象头),接下来就是填充对象头,把对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息存入对象头。
执行new指令后执行init方法后,才算一份真正可用的对象创建完成。
1.2.2 对象的内存布局
在HotSpot虚拟机中,分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
对象头(Header):包含两部分
第一部分:用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,32位虚拟机占32bit,64位虚拟机占64bit,官方称“Mark Word”。
第二部分:类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,官方称“Klass Word”。
如果是Java数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过Java对象元数据确定大小,而数组对象不可以。
实例数据(Instance Data)
对象的属性。程序代码中,所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。
对其填充(Padding)
不是必须需要,主要是占位,为了减少堆内存的碎片空间(不一定准确),保证对象大小是某个字节的整数倍。
1.2.3 对象的访问定位
使用对象时,通过栈上的reference数据来操作堆上的具体对象。
通过句炳访问
Java堆中会分配一块内存作为句炳池。reference存储的是句炳地址。
句炳地址包含指向对象实例数据、对象类型数据的指针
使用直接指针访问
reference中直接存储对象地址
两者比较
句炳访问:在对象移动(GC)只改变句炳包含的实例指针地址,reference自身不需要修改。
直接访问:速度快,省了一次指针定位的开销。
结论
如果对象频繁GC,句炳访问好;
如果对象频繁访问,直接指针访问好。