身为一个职业的Java程序员,每天打交到最多的就是jvm,那么套用孙子的一句话“知己知彼方能百战不殆”,熟悉jvm也就意味着是我们进阶路上必过之槛,下面先来张图,大概说明下jvm的内存分布
从图上可以看出Java内存主要分5个部分,下面会针对这5个部分分别进行说明
方法区:主要是保存的信息是类的元数据。方法区与堆空间类似,是被JVM中所有的线程共享的区域。方法区中最为重要的是类的类型信息、常量池、域信息、方法信息。类型信息包括类的完整名称、父类的完整名称、类型修饰符和类型的直接接口。常量池包括类方法、域等信息所引用的常量信息。域信息包括域名称、域类型和域修饰符、方法信息包括方法名称、返回类型、方法参数、方法修饰符、方法字节码、操作数栈和方法栈帧的局部变量区大小以及异常表。方法区是线程间共享的,当两个线程同时需要加载一个类型时,只有一个类会请求ClassLoader加载,另一个线程则会等待。
Java堆:堆在JVM规范里是一种通用性的内存池,用于存放所有的Java对象。堆是一个运行时数据区,类的对象从中分配空间,堆的优势是可以动态地分配内存大小,生存周期也不需要事先告诉编译器。
因为这里讲到了Java堆,所有有必要讲一下Java堆的优化,1是将生命周期较长的Java对象从heap中移到heap之外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的,另外逃逸分析与栈上分配这样的优化技术同样也是降低GC回收频率和提升GC回收效率的有效方式。
下面来说下逃逸分析。如果一个对象的指针被多个方法或线程引用时,那就可以称这个指针发生了逃逸
public class escapeAnalysisClass{ public static B b; public void globalVariablePointerEscape(){//给全局变量赋值,发生逃逸 b = new B(); } public B methodPointerEscape(){//方法返回值,发生逃逸 return new B(); } public void instancePassPointerEscape(){ methodPointerEscape().printClassName(this);//实例引用发生逃逸 } }
通过上面的例子我们可知,逃逸分析通常是全局变量赋值、方法返回值、实例引用传递(详见另一篇文章)
虚拟机栈:JVM的架构是基于栈的,即程序指令的每一个操作都要经过入栈和出栈这样的组合型操作才能完成。虚拟机栈是一种可以被用来快速访问的存储区域,该区域位于通用RAM里面,通过使用它的所谓的“”栈指针”,可以访问处理器。栈是一种快速有效的分配存储方法,访问速度仅次于寄存器,堆栈指针若向下移动,则分配新的内存,若向上移动,则释放那些内存。由于Java虚拟机需要预先去生成相应的内存空间,所以但我们尝试运行程序的时候,Java虚拟机必须知道被存储在栈内的所有数据的确切大小和生命周期,以便按照上面陈述的分配存储方法通过上下移动堆栈指针来动态调整内存空间。
综上对比堆和栈,可知栈的优势是访问速度比堆要快,它仅次于寄存器,并且栈数据是可以被共享的。栈的缺点是存储在栈里面的数据大小与生存期必须是确定的,从这一点来看,栈明显缺乏灵活性。
Java虚拟机栈也是线程私有的内存空间,它和Java线程在同一时间创建,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
再来深入探讨虚拟机栈的内部结构。
虚拟机栈在运行时使用一种叫作栈帧的数据结构保存上下文数据,栈帧里面存放了方法的局部变量表、操作数栈、动态链接方法和返回地址等信息。每一个方法的调用都伴随着栈帧的入栈操作,相对的,方法的返回则表示栈帧的出栈操作。如果方法调用时,方法的参数和局部变量相对较多,那么栈帧中的局部变量表就会比较大,栈帧会不断膨胀以满足方法调用所需传递的信息增大需求。
栈帧由三部分组成,即局部变量取,操作数栈和帧数据区。
程序计数器:当前线程所执行的字节码的行号指示器。在多线程环境下,为了让线程切换后能恢复到正确的执行位置,每个线程都要一个独立的程序计数器,各个线程之间互不影响、独立存储,因此这块内存是线程私有的。
本地方法栈:本地方法栈用于管理本地方法的调用。