内存结构
对于JVM的内存结构,我将按照上面两张图配合来讲。
Java Source就是我们的Java源代码,经过JVM编译器编译之后变为Java Class也称Java字节码文件。当我们运行java命令对字节码文件进行运行的时候,将会创建一个Java虚拟机。
此时虚拟机将会创建一个名字为main的主线程来执行我们的入口方法的代码,也就是它会去找main方法。
而运行代码也是需要内存的,而他的内存从JVM Stacks虚拟机栈中获取,当然不光光是主线程,还有我们在代码中new Thread的线程也是从这里获取内存资源。
当然,想要运行main方法,会发现main方法是在一个类Main中的,而此时还没有加载这个类呢,因此我们需要对这个类进行加载,此时就会通过类加载子系统将类的原始信息加载到Method Area方法区中,加载到这个区中的信息包括类的名字,类的继承关系,类的成员变量,类中引用的其他类,以及类的方法代码等。
而当我们的方法执行到一个new操作的时候,也就是需要创建一个实例对象的时候,对象所占用的内存来自于堆Heap,会先计算这个对象需要占用多大的内存,然后去堆中找到一个这么大的内存来创建这个对象。
而指向这个对象的引用,以及方法参数,他们都是引用值,他们就不是放在堆中了,而是放在虚拟机栈JVM Stacks中。
而当调用update这种我们自己编写的方法,或者调用hashCode这种Java无法实现,必须依靠底层操作系统来实现的代码,我们都放在JVM Stacks中,当然hashCode这种方法一般是放在Native Method Stacks本地方法栈中,不过我们JVM的实现是直接将他们整合在JVM Stacks中了。
而当前线程他不可能一直占用CPU,那么当他交出CPU的使用权的时候,下一次再次轮到它之后如何直到要从哪里开始呢?
这个时候就用到了PC Register,程序计数器,他将会当前这个线程运行到的代码的位置,因此即使当前线程被切换了,那么下次切换回来的时候通过程序计数器也能直到该从哪里继续运行。
那么当我们的对象被设定为null,或者没有引用的时候,那么我们的GC垃圾回收器将会对这些对象进行回收。
同时,我们的CPU其实是不能理解字节码指令的,因此我们还需要JVM提供的解释器对字节码指令进行解释,将其翻译成CPU可以理解的机器码指令,当然对于重复的代码也是重复进行解释的。
而对于一些频繁调用的热点代码,如执行万次的循环,那么就会通过JIT进行优化,加快这些热点代码的执行,从而提高效率。
Method Area(方法区)
(1)方法区是各个线程共享的内存区域,在虚拟机启动时创建
(2)虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非
堆),目的是与Java堆区分开来
(3)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
(4)当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常,此时回看装载阶段的第2步,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,如果这时候把从Class文件到装载的第(1)和(2)步合并起来理解的话,可以画个图
JVM运行时数据区是一种规范,真正的实现在JDK 8中就是Metaspace,在JDK6或7中就是Perm Space
Heap(堆)
(1)Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。
(2)Java对象实例以及数组都在堆上分配。
此时回看装载阶段的第3步,在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法
区中这些数据的访问入口,此时装载(1)(2)(3)的图可以改动一下
Java Virtual Machine Stacks(虚拟机栈)
经过上面的分析,类加载机制的装载过程已经完成,后续的链接,初始化也会相应的生效。
假如目前的阶段是初始化完成了,后续做啥呢?肯定是Use使用咯,不用的话这样折腾来折腾去
有什么意义?那怎样才能被使用到?换句话说里面内容怎样才能被执行?比如通过主函数main调
用其他方法,这种方式实际上是main线程执行之后调用的方法,即要想使用里面的各种内容,得
要以线程为单位,执行相应的方法才行。那一个线程执行的状态如何维护?一个线程可以执行多
少个方法?这样的关系怎么维护呢?
(1)虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程
的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创
建。
(2)每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。
调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
void a(){ b(); } void b(){ c(); } void c(){ }
栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。
每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A
reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。
局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中
局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使
用。
操作数栈:以压栈和出栈的方式存储操作数的
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调
用过程中的动态连接(Dynamic Linking)。
方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇
见异常,并且这个异常没有在方法体内得到处理。
结合字节码指令理解栈帧 javap -c Person.class > Person.txt Compiled from "Person.java" class Person { ... public static int calc(int, int); Code: 0: iconst_3 //将int类型常量3压入[操作数栈] 1: istore_0 //将int类型值存入[局部变量0] 2: iload_0 //从[局部变量0]中装载int类型值入栈 3: iload_1 //从[局部变量1]中装载int类型值入栈 4: iadd //将栈顶元素弹出栈,执行int类型的加法,结果入栈 5: istore_2 //将栈顶int类型值保存到[局部变量2]中 6: iload_2 //从[局部变量2]中装载int类型值入栈 7: ireturn //从方法中返回int类型的数据 ... }
The Program Counter Register(程序计数器)
我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据
CPU调度来的。
假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获
得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置。
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空,Native方法由本地OS提供,如hashCode()。
Native Method Stacks(本地方法栈)
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。
那如果在Java方法执行的时候调用native的方法呢?
内存溢出的产生
上面的结构中,除了程序计数器不会发生内存溢出,其他的都会发生内存溢出。
出现 OutOfMemoryError的情况
- 堆内存耗尽–对象越来越多,又一直在使用,不能被垃圾回收
- 方法区内存耗尽–加载的类越来越多,很多框架都会在运行期间动态产生新的类
- 虚拟机栈累积–每个线程最多会占用1M内存,线程个数越来越多,而又长时间运行不销毁时出现
StackOverflowError的区域
- 虚拟机栈内部–方法调用次数过多,每一次调用方法都会从线程分配到的内存中拿走一点去做函数调用,如果调用太多,那么内存不够了,就会溢出