JVM的生命周期
“JVM实例和JVM执行引擎实例,JVM实例对应了一个独立运行的Java程序,而JVM执行引擎实例对应了属于用户运行程序的线程;也就是JVM实例是进程级别,而执行引擎是线程级别的”。
JVM实例的诞生:启动一个带有main方法的Java程序时,一个JVM实例就产生了,此时类加载器就会以main方法为切入点来开始程序的执行。
JVM实例的运行:main()作为该程序初始线程的起点,所有的任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,Java程序也可以标明自己创建的线程是守护线程。
JVM实例的消亡:当程序中的所有非守护线程都终止时,JVM才退出
JVM的体系结构
JVM的内部体系结构分为三部分:类装载器子系统,运行时数据区和执行引擎。
1、类装载器,所有的类的加载都是由类加载器来完成的,并且Java中的类加载器是动态加载的,只有使用的时候才会主动去加载。
当执行 java MyFirstClass命令时候,它的简单的类加载过程如下:
1) 寻找jre目录,找到jvm.dll,根据它来初始化一个JVM;
2) 产生一个Bootstrap Loader(启动类加载器);
3) Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为Bootstrap Loader。
4) Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。
5)最后由AppClass Loader加载MyFirstClass类。
2、执行引擎:它主要是执行字节码或者本地方法
执行引擎定义了一个指令集,每一条指令包含一个单字节的操作码,后面跟0个或者多个操作数。这些就是主要用来执行经过编译后的字节码指令。
3、运行时数据区:方法区,堆,虚拟机栈,PC寄存器,本地方法栈
(1)方法区和堆由所有线程共享
堆:存放所有程序在运行时创建的对象
方法区:当JVM的类装载器加载.class文件,并进行解析,把解析的类型信息放入方法区。
(2)Java栈和PC寄存器和本地方法栈由线程独享,在新线程创建时间里
JVM中的数据类型:包括基本类型和引用类型,
(1)基本类型:8种基本数值类型和returnAddress(JVM的内部类型,用来实现finally子句)。
(2)引用类型:数组类型,类类型,接口类型。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。在Java中,Main函数就是栈的起始点,也是程序的起始点。
程序计数器:
程序计数器,一般占据一块较小的空间,主要就是作为当前线程执行字节码时候当前行号的指示器。字节码解释器就是通过改变计数器值来执行下一条字节码指令。之所以是由线程私有的,是因为CPU会轮流的交替执行各个线程的任务,而为了使得切换后的线程能正确恢复到执行位置,此时就需要为每个线程记录每次执行字节码的记录数。一般执行JAVA方法,该计数器记录是字节码指令的地址,而如果执行native方法,这个计数器值为空。这个区域是唯一一个不会出OutOfMemoryError的区域。
虚拟机栈:
虚拟机栈即Java栈,它也是线程私有的,该栈的生命周期同线程一起生一起亡,它所描述的就是Java的方法在执行中的内存模型:每个线程在执行方法的时候都会创建一个栈帧,它主要用于存储局部变量表,操作数栈,动态链接和方法出口等信息。该栈帧有两种操作,即帧的压栈和出栈。
每个栈帧代表一个方法,Java方法有两种返回方式,return和抛出异常,两种方式都会导致该方法对应的帧出栈和释放内存。
对于局部变量表存放编译期可知的8种基本数据类型,对象引用类型,和returnAddress类型,它所需要的内存大小在编译期间都完成分配了。
本地方法栈:
本地方法栈是为虚拟机在使用native方法的时候提供服务,而虚拟机站是为虚拟机执行java方法(字节码)提供服务。本地方法栈主要依赖于本地方法的实现,如某个JVM实现的本地方法接口使用C连接模型,则本地方法栈就是C栈,可以说某线程在调用本地方法时,就进入了一个不受JVM限制的领域,也就是JVM可以利用本地方法来动态扩展本身。
在HotSpot虚拟机上,本地方法栈和虚拟机栈合二为一。
堆:
在Java中“一切皆对象”,而存储这些Java对象的实例变量就是在堆中,所有的对象实例以及数组都在堆上分配。该堆中存放的数据主要有对象实例变量(包括自己所属的类和其父类声明的)以及指向方法区中对象类型数据的指针,指向方法表的指针。
在方法中利用Object obj=new Object();
Object obj会反映到Java栈的本地变量表中,它作为一个reference类型存储的。new Object()则是反映到Java堆中,直接在堆中分配,凡是利用new实例化对象,都是在堆中分配内存的。一个在Java栈中,一个在Java堆中,该reference类型所存储的信息就是为了要关联堆中实例对象的地址与栈中的变量地址。reference类型它只是用来作为一个指向对象的引用,它直接指向了该Java堆中对象实例数据的地址,该地址中包含了该对象类型数据的指针。之所以需要对象类型数据的指针的时候,是因为在java中对象转换过程中需要判断对象类型,以及java利用多态进行处理的时候,动态绑定在运行的过程中需要知道具体的对象类型是什么。
一般将堆划分为年轻代和老年代。
方法区:
在我们编程中,利用getClass来获取类型信息,而这个类型信息就是存放在方法区的,所以方法区存储了类的基本信息即类型信息,常量,静态变量,即使编译器编译后的代码。方法区在HotSpot中也叫永久代。那么类型信息在Java中是利用大端序的(即低字节的数据存储在高位内存上)。
类型信息:包括class的全限定名,class的直接父类,类类型还是接口类型,类的修饰符,所有直接父接口的列表,Class对象提供了访问这些信息的窗口
指向ClassLoader类的引用:在动态连接时装载该类中引用的其他类
指向Class类的引用:
在方法区中,有一块是称为运行时常量池,主要用来存放编译期生成的各种字面量和符号引用。
常量池:包括直接常量(String,integer和floatpoint常量)以及对其他类型、字段和方法的符号引用,由于这些符号引用,使得常量池成为Java程序动态连接中至关重要的部分
字段信息:普通意义上的类型中声明的字段
方法信息:类型中各个方法的信息
编译期常量:指用final声明或者用编译时已知的值初始化的类变量
class将所有的常量复制至其常量池或者其字节码流中。
方法表:一个数组,包括所有它的实例可能调用的实例方法的直接引用(包括从父类中继承来的)
除此之外,若某个类不是抽象和本地的,还要保存方法的字节码,操作数栈和该方法的栈帧,异常表。
为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
第一,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
第二,堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。
第三,栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
第四,面向对象就是堆和栈的完美结合。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。
后记:
从大的方面来说,可以将JVM管理内存分为两种类型:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给 自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。
在设置JVM参数的过程中要注意,-Xms -Xmx这两个指的就是年轻代与老年代的大小之和的堆大小,指的也就是我们逻辑上所说的堆的大小是多大,这里-Xmx指的是堆的大小,但是实际上它仅仅指的是年轻代和老年代总和的大小,-Xmn指的是年轻代大小,则利用Xmx-Xmn就可以得出老年代的大小了。而-XX:PermSize -XX:MaxPermSize这两个指的就是持久代的大小,严格意义上-Xmx和-XX:MaxPermSize才是JVM中堆的完整大小,-Xss是主要指的线程栈的大小,当在多线程中注意分配该大小。
JVM内存主要由5大部分组成,JVM的完整大小,可以通过以下参数设置,这里将年轻代与老年大的大小称为堆大小
-Xms(年轻代与老年代的初始大小最小大小) 如-Xms20M
-Xmx(堆的最大值)
-Xmn(堆中年轻代的大小)
-XX:SurvivorRatio(堆中年轻代的Eden区与一个Survivor的区大小比例) 如-XX:SurvivorRatio=8
-XX:MaxPermSize(持久代的大小)
-Xss(线程栈的大小)
这几个参数来完整的布局好。这里设定了年轻代的大小,老年大的大小,年轻代中Eden与一个Survivor区域的比例大小,持久代的大小,以及栈的大小,这样整个JVM的内存布局分配完毕了。