之前所知, Java的文件会先由javac编译,转成.class文件, 然后由JVM运行。下图则是更详细的步骤.
由于.class文件本身就是一个类,我们会先实行类加载,然后把类放入运行数据区(内存), 然后再由执行引擎.
JVM中内存分配.
线程私有:程序计数器,虚拟机栈,本地方法栈
线程共享: 堆,方法区
线程私有:生命周期随着线程生命周期
程序计数器: Java是多线程的, 线程中会进行切换, 所以需要记录下当前线程的地址(行号) 是唯一不会out of memory的区域.
虚拟机栈: 储存当前线程所需的数据, 指令, 返回地址.
每个线程都有属于自己的栈帧(里面有调取方法, 局部变量表(八大常用数据类型),操作栈,动态链接和返回地址, 其中无限recursion就会造成StackOverFlow,虚拟机栈)
本地方法栈: 存入native的底层方法(不是用JavaCode来写的)
线帧中的具体结构:
局部变量表: Java中的基础类型是存在局部变量表中,局部变量在线帧中,线帧在虚拟机中
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。
操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。
当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。
动态连接
在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。
这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。
动态连接是一个将符号引用解析为直接引用的过程。当java虚拟机执行字节码时,如果它遇到一个操作码,这个操作码第一次使用一个指向另一个类的符号引用
那么虚拟机就必须解析这个符号引用。在解析时,虚拟机执行两个基本任务
1.查找被引用的类,(如果必要的话就装载它)
2.将符号引用替换为直接引用,这样当它以后再次遇到相同的引用时,它就可以立即使用这个直接引用,而不必花时间再次解析这个符号引用了。
方法返回
当一个方法开始执行时,可能有两种方式退出该方法:
正常完成出口
异常完成出口
正常完成出口是指方法正常完成并退出,没有抛出任何异常(包括Java虚拟机异常以及执行时通过throw语句显示抛出的异常)。如果当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者(调用它的方法),或者无返回值。具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令确定。
异常完成出口是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。
无论是Java虚拟机抛出的异常还是代码中使用athrow指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器,就会导致方法退出。
无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态。
方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压如调用者的操作数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。
一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。
线程共享区域
堆:对象实体, 运行时常量池(符号引用,而不是常量)
-Xms参数设置最小值
-Xmx参数设置最大值
方法区:类信息,常量(final定义的量), 静态变量(static), 加了修饰的变量, 即时编译后的代码(元空间JDK1.8(元空间):
JVM各版本内存区域的变化
JDK1.8 使用元空间 MetaSpace 替代方法区,并且并且元空间与JVM不相连。
为什么要用Metaspace替代方法区
随着动态类加载的情况越来越多,这块内存变得不太可控,如果设置小了,系统运行过程中就容易出现内存溢出,设置大了又浪费内存。
https://www.jianshu.com/p/a6f19189ec62
运行常量池的变化(编译器生成堆各种字面量和符号的引用)
JDK1.6中在方法区中, JDK1.7在永久代(堆)中. JDK1.8在元空间(本地内存)中, 元空间是对方法区的实现.
元空间(空间大小只受制于机器的内存), 永久代(受制于参数,超出大小会OOM)
因此, 元空间可以不断地放大,而每一次放大都会造成一次full GC
https://blog.csdn.net/rongtaoup/article/details/89142396
常量池的好处:
常量池避免了频繁的创建和销毁对象而影响系统性能
直接内存
直接内存并不是虚拟机运行时数据区的一部分。
在NIO中,引入了一种基于通道和缓冲区的I/O方式,它可以使用native函数直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
-XX:MaxDirectMemorySize设置最大值,默认与java堆最大值一样。
网友评论