在JVM运行时,类加载器ClassLoader在加载到类的字节码后,交由jvm的执行引擎处理,
执行过程当中须要空间来存储数据
(相似于Cpu及主存),此时的这段空间的分配和释放过程是
此处须要关心和理解的,暂能够称为运行时的数据的内存区的分配,
首先运行时的数据区包括,程序计数器,以及Stack(虚拟机
栈),以及虚拟机堆,方法区,本地方法栈,
虽然运行时区域分配只要包含上述的描述组件,但实际运行中,程序计数器外,应该再加一个寄存器,
目前先描述上面5个,寄存器后面一并写入,
程序计数器:
java中的多线程是经过线程轮流切换并分配处理器执行时间来实现的,再任何一个肯定的时刻,一个处理器只会处理一条线程中的指令
,所以,为了线程切换后能恢复到正确的执行位置,
每条线程都须要有一个独立的程序计数器,各条线程之间的计数器互不影响,
独立存储,咱们称这一类内存区域为“线程私有”的内存区域,而程序计数器则是一块较小的内存,它的做用即是记录当前线程
所执行的字节码的行号指示器,因此也能够称做为“线程私有的内存区域的一种”,除了程序计数器为线程私有的内存区域外,
虚拟机中的“栈”也是能够称做为“线程私有的”内存区域的一种。
除此以外,程序计数器(ProgramCounter)也被称做为PC寄存器,是在线程启动的时候会建立该PC寄存器,即程序计数器,用于记录,
当前正在执行的JVM指令的地址,用于线程切换后能够执行到正确的位置,那么除了PC寄存器外,在JVM中还有最经常使用的另外三个寄存器,
分别是,optop操做数栈顶指针,frame当前执行环境指针,vars指向当前执行环境中第一个局部变量的指针,全部的寄存器均为32位,
除了PC使用与记录程序的执行位置外,
optop,frame,vars则是用于记录指向Java栈区的指针,
(注:PC寄存器内存区域是JVM虚拟机中惟一一个没有规定任何OutOfMemoryError状况的区域)
Java虚拟机栈
Java栈的内存区域很小,默认状况下JVM设置栈的内存为1M,于程序计数器同样,“栈”也是线程私有的内存区域,每一个栈中的数据,
都是线程私有的,其它栈不能访问,它的生命周期和线程相同,“栈”描述的是
Java
方法执行时
的内存模型,
每一个方法被执行时都会同时建立一个“栈帧
”
用于存储局部变量表,操做栈,
动态连接,方法出口等信息,每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈和出栈的过程。
局部变量表 存放了
编译期可知的各类基本数据类型,(byte,short,char,int,float,long,double,boolean),对象引用(
reference类型,对象引用地址),和returnAddress类型,(指向了一条字节码指令的地址),其中64位长度的long和double会占用2个局部变量空间外,
其他数据类型均占用1个(对象引用和returnAddress也属于占用1个空间的数据类型),
局部变量表所需的内存空间是在
编译器间
完成分配的,当线程执行一个方法时,该方法在栈帧中所需分配多大的内存空间是彻底肯定的,
在方法的运行期间是不会改变局部变量表的大小的,
“栈”是存放线程调用方法时,存储局部变量表,操做,方法出口等于方法执行相关的信息,
Java栈每次所建立
内存的大小是由Xss来调节的,方法层次太多会撑爆这个内存区域,若不够时将会抛出StackOverflowError的异常信息,
通常状况下Xss设置的大小是设置当前线程栈的空间大小,若线程栈的空间大小设置的过大,
则会致使linux服务器的内存可建立线程数将会较少,由于“栈”是一个线程的私有区域,每个方法被调用直至完成的过程,
就对应这一个线程栈的入栈和出栈的过程,若是一个线程在访问一个方法时的栈大小建立过大,假设Xss为10M,那么每个
线程在访问方法时,线程栈则都会建立10M的栈内存空间,若是此时服务器剩余的内存空间为100M,则此时最大可建立线程数
则为10个,这显然是不符合整个项目的应用的,除非扩大服务器的内存空间(线程栈的建立是使用的服务器剩余的内存空间进行建立的)
或者缩小每个线程所使用的建立栈的内存大小,因此通常内存栈的空间大小Xss设置为256K通常便可,若是一个方法的层次太多,撑爆
了256K的线程内存区域,则会抛出异常便可,但通常较小的内存栈空间,则意味着在相同的服务器内存环境中,能够建立更多的线程数据。
上面也已经提到“栈帧”是用来存储局部变量表,操做数堆栈,动态连接,方法出口等信息的,
总体来讲,虚拟机栈中能够分为三个部分,
局部变量,执行环境,和操做数堆栈(即上述提到的操做栈),其中执行变量部分包含了当前方法调用所使用的全部局部变量,vars寄存器指向这一点,执行环境
用于维护堆栈自己的操做,frame(帧)寄存器,指向它,操做数堆栈经过字节码指令用做工做空间,在此到处置字节码指令的参数,并找到
字节码指令的结果,操做数堆栈的顶部则由optop寄存器指向,
执行环境一般夹在局部变量表和操做数堆栈之间,
当前正在执行的方法的操做数堆栈始终是最顶层的堆栈部分,所以optop寄存器指向整个
Java堆栈的顶部、
通常状况下当一个方法嵌套另外的方法同时执行时,
首先理解下上面所提到的关于栈的概念,能够知道,每个方法被
线程
执行的时候都会建立一个栈帧,用于存储局部变量表,操做数堆栈,和执行环境(动态连接,方法出口等)信息
,每个方法被线程执行的时候,都会建立一个栈帧,而栈帧的内存大小是由Xss来设置的,好比200KB,则表示每个线程在执行的过程中都具有了一个200KB的栈内存空间分配的大小,
每个方法在被执行的时候都会建立一个栈帧,若是是当前方法的嵌套层次太多时,则在当前方法中调用其余方法时,则也会建立所嵌套方法的堆栈,可是当前线程所执行的方法体的整个的
方法的嵌套层次所建立的堆栈不能超过Xss所设置的值,若是方法所执行的层次太多,则可能会致使栈溢出。( 线程在执行一个方法时,建立的是一个栈的内存空间大小,随着栈的深度愈来愈大,
每一次所执行的嵌套的方法都相似于建立一个帧,即当前栈上面的一个栈帧,也包含了一个,
卧槽,我知道了!!!看下面)
栈和栈帧是不同的!!使用Xss参数来设置栈的大小,但每一个方法在被执行的时候,都会建立一个栈帧!!用于存储局部变量表等等的信息,这和上面所写到的是同样的!,
可是栈是后入先出的一个数据结构,而栈帧则是栈上面的一个实体!,相似于这样的一个效果,
全部的栈帧组成一块儿造成栈!!,而栈帧所作的操做则如同上面所提到的,记录当前方法的局部变量表,方法的出入口,执行环境等信息,每个方法在被嵌套执行的时候,都会建立一个栈帧,方法的嵌套层次越多,
则建立的当前栈的栈帧越多,而随着栈帧建立的深度越大,一旦超出了所设置栈的空间大小,则会出现所对应的StackOverflowError栈内存溢出的异常。!这样的理解彷佛是很对,而且很正确的,!,
这也是为何会说,当前方法所执行的方法深度过深时,会出现栈溢出异常的问题所在!。
因此:有一句话是,每个方法从调用开始到执行完成的过程,则就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程,(请详细知道和明白栈和栈帧的不一样,再好好体验一下这句话,!是很正确的!)
那么则再好好回顾一下上面所提到的针对栈的寄存器,分别是optop,frame,vars寄存器,
StackOverflowError深度:若是使用jvm默认设置(JVM默认设置线程建立栈的大小为1M),栈的深度大多数状况下可达到1000~2000,足以在平常开发中使用。注意避免代码中存在超过1000的方法嵌套。每一个方法嵌套对应一个栈帧。
本地方法栈
本地方法栈于虚拟机栈所发挥的做用是很是类似的,其区别则是虚拟机栈执行Java(也就是字节码)服务,而本地方法栈则是
为虚拟机使用到的Native方法服务,保存native方法进入区域的地址,本地方法栈一样会抛出StackOverflowError于OutofMemory
Error异常。
Java堆
Java堆(Heap)是虚拟机中所管理的内存最大的一块,由于Java堆是全部线程共享的一块内存区域,在虚拟机启动时建立,此内存区域
做用则是存放对象实例,全部的对象实例以及数组都要在堆上分配,Java堆也是垃圾收集器管理的主要区域,从
内存回收的角度来看,
如今JVM的收集器基本都是采用
分代收集算法,因此Java堆还能够细分为,新生代和老年代,以及Eden区,From Survivor 和 To Survivor
区域等,以及从
内存分配的角度来看,Java堆中还可能划分出多个线程私有的分配缓冲区(ThreadLocal,AppocationBuffer,TLAP)等
内存区域空间,可是不管如何划分,都是在方便JVM收集器GC收集的过程当中,或者线程分配的方式来分配的堆内存区域,不管如何分配,
都与存放内容无关,不管任何区域,存放的都是对象实例,
相比于栈来讲,堆内存最不一样的地方在于编辑器是没法知道要从堆内存中分配多少存储空间,也没法得知存储的数据要在堆中存储多长时间,
所以,用堆保存的数据会具有更大的灵活性,在程序的执行过程当中,会在堆里自动进行对象和数据的存储,堆所占用
内存的大小由-Xmx和Xms指令来调节,
随意使用一个Main方法运行一个无限循环的new Object()的程序,使用
-verbos:gc -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:SurvivorRatio=9
-XX+HeapDumpOnOutOfMemoryError,便能很快报错OOM(内存溢出异常),并能自动生成DUMP文件,
由于只给堆内存分配了10M的空间而已,:(此处引述该段话的意义在于,提供了很好的内存溢出的测试思路,
能够经过配置较低的堆内存以及栈等内存,来调试对JVM的深刻理解的一种方式,经过这种方式,能够测试DUMP的
打印路径,DUMP文件的分析等等,以及能够测试JVM的GC收集算法等啦(分代收集等等啦),没必要每次都在生产或真实环境中进行数据的测试了。)
方法区
method方法区又称做
静态区,它用于存储已被
虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,
注:(方法区存储的是在编译期间已经被虚拟机加载的一些内存信息,在编译期间被加载的信息通常则为静态数据,常量,类的基本信息等,而非编译时JVM加载的数据,如非静态变量等,则是在实例建立后存储在堆中)
如:存放全部的类信息,静态变量,静态方法,常量和
成员方法,
方法区于Java堆同样,也是各个线程共享的一个内存区域,在HotSport虚拟机上,不少人愿意把方法区称为“永久代”
实际上二者并不等价,仅仅是由于HotSport虚拟机把GC的分代手机扩展到了方法区,或者说使用永久代来实现方法区而已,
但对于其它按照Java虚拟机规范实现的虚拟机(BEA,IMB J9)等是不存在永久代的概念的,
相比于Java堆而言,方法区的内存回收主要是针对常量池的回收和对类型的卸载,但该区域的内存回收机制相比于堆的GC内存回收机制
来讲“成绩”难以让人满意,尤为是类型的卸载等,回收条件较为苟刻,但该部分区域的回收是存在必要的,当方法区没法知足内存分配
需求时,则将抛出OutOfMemoryError异常,
方法区的大小由-XX:PermSize和-XX:MaxPermSize来调节,类太多可能会撑爆方法区,静态变量或常量也有可能撑爆方法区。
运行时常量池
该区域属于方法区,除了方法区中记录类的版本,字段,方法,接口等信息外,还有一项则是常量池,能够将常量池理解为 方法区的 资源从库,
主要用于存放编译期生成的各类字面量,
和符号引用(字面量即常量,符号引用即
类和接口的全限定名, 字段的名称和描述符, 方法的名称和描述符, Java中八种基本类型的包装类的大部分都实现了常量池技术
),
如类和接口的常量,编译器生成的各类字面量也是放置到常量池中,java中常量变量在
自动装箱,条件编译,解析于填充符号表等,具体可详细看下javac编译期间所作的具体操做,当一个类中的成员变量(static 变量)和成员方法(static 方法)被引用的时候,JVM
则
也是经过常量池中的引用来查找成员变量和成员方法在内存中的实际地址,而后返回引用地址。
回顾方法区和常量池:方法区主要包括 类的基本信息,其中包括(
类的访问标志,这个class是不是类仍是接口,是否认义public类型,是否认义abstrace类型,是否声明了final等,
以及记录当前该
class类的索引关系,父类索引和接口索引接口的关系;
字段表集合,记录当前接口或类中声明的变量,包括类级别变量和实例级变量,字段的做用域,是不是实例变量(static)或类变量,
常量,静态变量,以及编译器编译后代码等,其中,常量池属于方法区,但常量池中记录的通常为,常量数据,和符号引用数据等,
假设此时,在一个方法中,Object obj = new Object(),这样一个代码出如今方法体中,也会涉及到内存的分配的操做,
此时,在Object obj,则被分配至栈内存空间中的局部变量表中,做为一个引用类型出现,reference类型,而此时所执行的方法中的其他的
变量信息则也是同时记录在此时的方法栈的局部变量表中,而此时的 new Object()则会被分配至 堆内存中,造成一块存储了Object类型全部
实例数据值(对象中各个实例字段的数据(即类的非静态变量,存储在堆中,做用于整个类中))的结构化内存,
,其中栈内存的局部变量表中的reference类型将记录 该实例对象在堆内存的具体地址,除此以外,堆中的Object()实例,
也同时必须可以找到此对象的类型数据(对象类型,父类,实现的接口,方法)等基本的class类的信息,这些类的基本毫无疑问则记录在
方法区中,其中 new Obejct()中的成员变量,成员方法(静态方法)等信息则是记录在方法区中的常量池中,实例方法等信息则仍是记录在方法区中,
并不在常量池中记录,
关于堆中的空间分配:
JVM中GC垃圾收集器经常使用的-->几种垃圾收集算法
-
引用计数算法,对于互相引用且没有被其余引用的对象没法处理收集,JVM实际上也并未采用
-
根搜索算法(JVM其他几个算法的实现基础),设立若干的根对象,当任何一个
根对象到某一个对象的均不可达时,则认为该对象是能够回收的,根对象又叫作(GC ROOTS),
JAVA中扮演GC ROOTS根对象的主要包括如下四种对象,分别是:
1. 虚拟机栈中的引用对象(即虚拟机栈帧中的局部变量表中所引用的对象),2. 方法区中的类静态属性引用的对象,3. 方法区的常量引用对象,4. 本地方法栈的JNI引用对象, 以上四中对象扮演GC ROOTS的角色,只要是某一个对象到任何一个根对象皆不可到达时,则表示该对象为可回收对象。
根搜索算法解决了能够判断哪些对象是能够被回收的,哪些对象是不能够回收的问题,可是在JVM垃圾回收的过程当中,还须要解决的另外两个问题则是,何时能够回收这些内存垃圾,以及如何回收这些垃圾内存,在根搜索算法的基础上,
现代虚拟机中垃圾搜索的实现当中
,
主要存在的则是以下三种
,
分别是:
标记-清除算法,复制算法,标记-整理算法,还有一个则是 分代收集算法,前三种算法均是扩展于根搜索算法,对于分代搜索算法,也会在下面作详细的相关介绍。
-
标记清除算法,堆的有效内存空间快被耗尽后,遍历全部的GC ROOTS,将全部GC ROOTS可达的对象标记为存活对象,而后清除全部GC ROOTS不可达的对象,(缺点:须要遍历全部的堆对象,判断是否和GC ROOTS 可达,效率低下,且在标记清除算法执行过程当中,须要中止程序应用程序,第二:标记清除所清理出的内存是不连续的内存空间,由于被清除的对象是出如今内存的各个角落的,因此致使内存空间布局很乱,连续空间较为难找,则在从新分配数组对象时,寻找不到一个连续的内存空间,将会出现莫名的问题(数组的内存空间是连续的内存集合))
-
复制算法(又能够叫作,标记/复制/清除算法),复制算法与标记清除算法不一样的是,复制算法将会把内存划分为两个不一样的区间,分别是活动内存区间,和空闲内存区间,在任意的时间点,只会有一个内存区间被使用,当活动内存区间的有效内存被消耗完的时候,JVM则暂停程序运行,将活动区间中的存活对象所有复制到
空闲空间中,且严格按照内存地址进行依次排列,于此同时,GC线程将更新后的存活对象指向新的内存地址,且清空活动内存中所剩余的垃圾对象。
能够看出的是,尽管复制算法也是用了GC ROOTS的遍历方式获得存活对象,而后所有复制到空间内存空间中,但复制算法弥补了 标记/清除算法中,内存混乱的问题,但缺点是:须要浪费一半的内存,作空闲内存,这是不可忽略的特色。
-
标记整理算法(又能够叫作,标记/整理/清除算法),于标记/清除算法不一样的是,标记整理算法,1. 也是先遍历全部的GC ROOTS,将后将存活对象进行标记,2. 移动所对应的存活对象,且严格按照内存的空间地址进行移动,而后将末端的内存地址进行回收,
相比于标记清除算法,解决了内存分散的特色,相比于复制算法,则消除了内存减半的代价,可是其惟一的缺点即是:标记整理的执行效率不高,既要标记又要整理对应的存活对象的引用地址,相对来讲,总体的执行效率要低于复制算法。
上述三种GC的实现算法共同点和不一样点分别是:(总结:):
-
共同点:都须要先暂停应用的执行,(由于都存在标记阶段,经过遍历GC ROOTS来判断获得存活的对象,即经过根搜索算法进行标记获得可回收对象,)
-
执行效率上:
复制算法>标记/整理算法>标记/清除算法(此处的效率只是简单的对比时间复杂度,实际状况不必定如此)。
-
内存整齐度上:复制算法=标记整理算法>标记清除算法
-
内存利用率上:标记整理算法=标记清除算法>复制算法
能够看到标记清除算法,算是一个比较落后的算法,可是上述的算法在执行过程当中,都存在或多或少的优缺点,因此在特定的场合或者说是内存结构中,使用特定的算法,都将会特别有效果,根据不一样的内存状况来选择对一个的垃圾清理算法,是较为合适的一种行为;
问?为何上述的三种算法在执行过程中,都须要暂停应用程序的执行,?
答:首先上述三种算法都存在对象的标记行为,以此来判断出那些是可回收对象,那些是活跃对象,即根据GC ROOTS是否可达进行搜索标记,即上述所提到的根搜索算法,那么在经过根搜索算法,标记的过程中,假设此时A对象被标记为了可回收对象,那么因为应用程序是在可执行状态下,此时又建立了B对象,且B对象引用A对象,是可达的,可是因为B对象的建立和引用是在标记以后,此时则会出现了A对象进行了垃圾回收,而B对象则经过,整理也好,复制也好,被保留了下来,那么此时程序再次经过B对象获取A对象时,则会出现A对象为null的状况,那么这必然是在程序的过程中不容许出现的状况,因此在涉及到 标记 再最终有一个过程是清除过程的这样的算法中,必然是先执行对应的 垃圾清理算法,而后再算法执行过程后,通知唤醒对应的应用程序中线程,而后继续执行相关的应用程序的任务。(GC 的线程在执行的过程当中,必然是和应用程序的线程相互配合,才能达到垃圾清理后,也不影响程序的正常运行的效果,好比此处的暂停应用程序的线程执行,先优先执行GC的垃圾回收的线程)
分代收集算法:是
JVM中GC垃圾收集器-->经常使用的几种收集器即各收集器的使用区别;