参考文章:html
http://gityuan.com/2016/01/09/java-memory/ (推荐牛人的博客)java
https://www.guru99.com/java-stack-heap.htmlgit
https://www.zhihu.com/question/21539353bootstrap
http://www.javashuo.com/article/p-wkgtrwav-gc.html多线程
深刻Java虚拟机函数
Java虚拟机在执行java程序的过程当中会把其所管理的内存区域划分为若干个不一样的数据区域。 详情以下图所示:操作系统
程序计数器是一块较小的空间,能够当作当前线程所执行的字节码的行号指示器,在虚拟机的概念模型中,字节码解释器工做经过改变程序计数器的值来选取下一条须要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能须要依赖这个计数器完成。.net
因为虚拟机的多线程是经过线程轮流切换分配处理器执行时间的方式来实现的,因此任何一个时刻,一个处理器都会执行一条线程中的指令。所以,为了切换线程能恢复到正确的执行位置,每条线程都须要一个独立的程序计数器,各个线程之间互不影响,独立存储,是一个线程私有的内存。插件
若是线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;若是执行Natvie方法,这个计数器值则为空。线程
这个内存区域是惟一一个在Java虚拟机规范中没有规定任何Out Of Memory Erorr状况的区域。
Java栈,即Java虚拟机栈是线程私有的,他的生命周期和线程相同。Java栈描述的是Java方法执行的内存模型:每一个方法在执行的同时都会建立一个栈帧用于存储局部变量表,操做数栈,动态连接,方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程.
局部变量表:存放了编译器可知的各类基本数据类型,对象引用类型(reference类型,不一样于对象自己,他多是一个指向对象起始地址的引用指针,也多是指向一个表明对象句柄或者其余于此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
局部变量表的所需内存空间在编译期间完成分配,当进入一个方法时候,这个方法所须要分配的局部变量空间是彻底肯定的,方法运行时不会改变局部变量表的大小。
Java栈是为虚拟机执行Java方法(即字节码)服务。
本地方法栈执行的是Native方法服务
Java堆是被全部线程共享的一块内存区域。 堆的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配内存。 Java堆也是垃圾收集器管理的主要区域,即"GC堆"。
Java堆能够出于物理上不连续的内存空间中,逻辑上连续便可。
方法区与Java堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
存储内容:类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息
运行时常量池 是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class常量池来讲,更具备动态性,Java语言不要求常量必定只有在编译期才能产生,也就是并不是预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入常量池,如String.intern()方法。
关于各个部分的联系,能够查看下图(虽然这张图讲的是GC Root的,可是我以为对于理解JMM挺有用的):
这里首先总结一下对象的建立过程:
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终造成能够被虚拟机直接使用的Java类型,这一个过程为类加载机制。
类从被加载到虚拟机内存开始,到卸载出内存为止,他的整个生命周期包括以下阶段:
加载,验证,准备,初始化和卸载阶段的顺序是肯定的,而解析阶段则不必定了,他在某些状况下可在初始化完成后在开始,这是为了支持Java语言的运行时绑定。
关于何时开始加载的过程,Java虚拟机规范中没有进行强制约束,可由虚拟机自由把握。可是对于初始化阶段,虚拟机严格规定了有且只有5种状况必须当即对类进行"初始化"(加载,验证,准备天然须要在此以前开始)。
java.lang.invoke.MethodHandle
实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokestatic的方法的句柄,而且这个方法句柄对应的类没有初始化,则触发初始化。加载
加载阶段,虚拟机须要完成三件事情:
在加载阶段可使用系统提供的引导类加载器来完成,也能够由用户自定的类加载器完成(如Android的插件化技术)。
详细可参考IBM这篇文章
引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。通常来讲,Java 应用的类都是由它来完成加载的。能够经过ClassLoader.getSystemClassLoader()来获取它。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区里,方法区中的数据存储格式由虚拟机自行定义。而后在内存中实例化一个java.lang.Class对象,这个对象做为程序访问方法区中这些类型数据的外部接口。
加载阶段和链接阶段的部份内容(如一部分字节码文件格式验证动做)是交叉进行的,加载阶段还没有完成,链接阶段可能已经开始了,但这些夹在加载阶段之中进行的动做,仍属于链接阶段的内容。
验证
验证是链接的第一步,目的是为了确保Class文件字节流中包含的信息符合虚拟机要求。
验证阶段包含4个阶段的检验动做:
(如验证是否以魔数开头,主次版本号是否在虚拟机处理范围以内等等)
如这个类是否有父类?
这个类的父类是否继承了不被容许继承的类(final关键字)
...
符号引用中经过字符串描述的全限定名是否能找到对应的类。
再指定类中是否存在符合方法字段描述符以及简单名称所描述的方法和字段。
符号引用中的类,字段,方法的访问性(private,protected,public,default)是否能够被当前类访问.。
准备
准备阶段是正式将类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
须要注意的是,这个时候进行内存分配得仅仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量会在对象实例化时候随着对象一块儿分配在Java堆中。其次,上述所说的设置类变量初始值是指的0值,假设类变量定义以下:
public static final num=3;
则num在准备阶段初始化的值为0而不是3,由于这个时候还没有开始执行任何Java方法,而把num指定为3的putstatic指令是在程序被编译后,存放于类构造器<clinit>()方法中的,因此num=3的动做在初始化阶段才会执行。
But,若是上述代码加了一个final字段,那就不同了:
public static final final num=3;
这时候Javac将会为num生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue的设置将num赋值为3。
public class Test { private static int num=3; private static final int finalnum=3; public static void main(String[] args) { } } //使用javap反编译查看Test.class,省略无关部分 //javap -v -p Test.class private static int num; descriptor: I flags: ACC_PRIVATE, ACC_STATIC private static final int finalnum; descriptor: I flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL ConstantValue: int 3
这里顺便记录一下<init>和<clinit>的区别,能够看这个:
<init> is the (or one of the) constructor(s) for the instance, and non-static field initialization.
<clinit> are the static initialization blocks for the class, and static field initialization.
class X { static Log log = LogFactory.getLog(); // <clinit> private int x = 1; // <init> X(){ // <init> } static { // <clinit> } }
解析
解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程,在上面的符号引用验证中提到过。
符号引用替换成直接引用的过程有以下几种状况(详细内容见深刻理解Java虚拟机P223):
上述阶段经过了,则说明解析的工做就完成了。
初始化
初始化工做是类加载过程的最后一步,前面介绍的类加载过程当中,除了在加载阶段用户能够经过自定义ClassLoader参与外,其他的皆有虚拟机主导完成。到初始化阶段,才真正执行类定义中的Java代码。
init是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init方法
在准备阶段,系统已经默认给类变量(被static修饰的变量)赋值过一次初始值(由系统决定)了,初始化阶段就是执行程序猿赋值的过程。或者说初始化阶段是执行<clinit>方法的过程。
clinit方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块中的语句合并产生的。 编译器收集的顺序是由语句在源文件中出现的顺序决定静态语句块只能访问到定义在静态语句块以前>的变量,定义在他以后的变量,只能赋值,不能访问[1]
clinit方法不须要显示的调用父类构造器,虚拟机会保证在子类的clinit方法执行以前,父类的>clinit已经执行完毕。第一个执行clinit方法的确定是java.lang.Object。
因为父类的clinit方法先执行,也就意味着父类中定义的静态语句块优于子类变量赋值操做。[2]
clinit方法对类或者接口来讲不是必须的,若是一个类没有静态语句块,也没有对static变量赋值的操做,那么编译器能够不为这个类生成clinit方法。
接口中也会有clinit方法,惟一与类中执行不一样的是,执行接口clinit方法不须要先执行父接口的>clinit方法,只有当父接口中的变量使用时候,父接口才会初始化。
虚拟机保证一个类的clinit方法只会执行一次。
[1]的验证:
static { num=2323;//能够赋值 System.out.print(num+"");//IDE会提示不能访问 } private static int num=3; //----------------------------------------下面状况经过 private static int num=3; static { num=2323; System.out.print(num+""); }
[2]的验证:
public static void main(String[] args) { Child child = new Child(); System.out.print(child.K+""); } static class Parent{ public static int lll=23; static { lll=666; } } static class Child extends Parent{ public static int K=lll; } //输出666
恰好整理一下static在代码中的执行顺序
若是类尚未被加载:
先执行父类的静态代码块和静态变量初始化,而且静态代码块和静态变量的执行顺序只跟代码中出现的顺序有关。
执行子类的静态代码块和静态变量初始化。
执行父类的实例变量初始化
执行父类的构造函数
执行子类的实例变量初始化
执行子类的构造函数
若是类已经被加载:
.class文件是二进制字节流形式
随着虚拟机的不断发展,不少程序语言开始选择与操做系统和机器指令集无关的格式做为编译后的存储格式(Class文件),从而实现”Write Once, Run Anywhere”。 Java设计之初,考虑后期能让Java虚拟机运行其余语言,目前有愈来愈多的其余语言均可以直接须要在Java虚拟机,虚拟机只能识别Class文件,至因而由何种语言编译而来的,虚拟机并不关心。
类卸载的过程及触发条件
在类使用完以后,知足下面的情形,会被卸载:
该类在堆中的全部实例都已被回收,即在堆中不存在该类的实例对象。
加载该类的classLoader已经被回收。
该类对应的Class对象没有任何地方能够被引用,经过反射访问不到该Class对象。
若是类知足卸载条件,JVM就在GC的时候,对类进行卸载,即在方法区清除类的信息。