(重要)1 - JVM随笔分类(java虚拟机的内存区域分配(一个不断记录和推翻以及再记录的一个过程))

原文内容来自于LZ(楼主)的印象笔记,如出现排版异常或图片丢失等问题,可查看当前链接:https://app.yinxiang.com/fx/15979358-5f36-434a-893d-65d1bb1e4737
 

 

java虚拟机的内存区域分配

 
在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中八种基本类型的包装类的大部分都实现了常量池技术)
http://chenzehe.iteye.com/blog/1727062 Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值在-128到127时才可使用到对象池
如类和接口的常量,编译器生成的各种字面量也是放置到常量池中,java中常量变量在
编译器时则便会进行自动运算后赋值, 几张图轻松理解String.intern() - CSDN博客,除此之外,javac在编译器还存在代码的标注检查,注解处理器,
自动装箱,条件编译,解析于填充符号表等,具体可详细看下javac编译期间所做的具体操作,当一个类中的成员变量(static 变量)和成员方法(static 方法)被引用的时候,JVM
也是通过常量池中的引用来查找成员变量和成员方法在内存中的实际地址,然后返回引用地址。
 
回顾方法区和常量池:方法区主要包括 类的基本信息,其中包括(类的访问标志,这个class是否是类还是接口,是否定义public类型,是否定义abstrace类型,是否声明了final等,
以及记录当前该class类的索引关系,父类索引和接口索引接口的关系;字段表集合,记录当前接口或类中声明的变量,包括类级别变量和实例级变量,字段的作用域,是否是实例变量(static)或类变量,
可变性,并发可见性(volatile)等,方法表集合,包括访问标志,名称索引,等于字段表的描述是一致的,父类方法没有在子类中重新,则方法集合中不会出现来自父类的方法信息,)https://blog.csdn.net/u011116672/article/details/49865023
常量,静态变量,以及编译器编译后代码等,其中,常量池属于方法区,但常量池中记录的一般为,常量数据,和符号引用数据等,
 
 
假设此时,在一个方法中,Object obj = new Object(),这样一个代码出现在方法体中,也会涉及到内存的分配的操作,
此时,在Object obj,则被分配至栈内存空间中的局部变量表中,作为一个引用类型出现,reference类型,而此时所执行的方法中的其余的
变量信息则也是同时记录在此时的方法栈的局部变量表中,而此时的 new Object()则会被分配至 堆内存中,形成一块存储了Object类型所有
实例数据值(对象中各个实例字段的数据(即类的非静态变量,存储在堆中,作用于整个类中))的结构化内存,
,其中栈内存的局部变量表中的reference类型将记录 该实例对象在堆内存的具体地址,除此之外,堆中的Object()实例,
也同时必须能够找到此对象的类型数据(对象类型,父类,实现的接口,方法)等基本的class类的信息,这些类的基本毫无疑问则记录在
方法区中,其中 new Obejct()中的成员变量,成员方法(静态方法)等信息则是记录在方法区中的常量池中,实例方法等信息则还是记录在方法区中,
并不在常量池中记录,
 
关于堆中的空间分配:
 
JVM中GC垃圾收集器常用的-->几种垃圾收集算法
  1. 引用计数算法,对于互相引用且没有被其他引用的对象无法处理收集,JVM实际上也并未采用
  2. 根搜索算法(JVM其余几个算法的实现基础),设立若干的根对象,当任何一个根对象到某一个对象的均不可达时,则认为该对象是可以回收的,根对象又叫做(GC ROOTS),
JAVA中扮演GC ROOTS根对象的主要包括以下四种对象,分别是:1. 虚拟机栈中的引用对象(即虚拟机栈帧中的局部变量表中所引用的对象),2. 方法区中的类静态属性引用的对象,3. 方法区的常量引用对象,4. 本地方法栈的JNI引用对象,    以上四中对象扮演GC ROOTS的角色,只要是某一个对象到任何一个根对象皆不可到达时,则表示该对象为可回收对象
            根搜索算法解决了可以判断哪些对象是可以被回收的,哪些对象是不可以回收的问题,但是在JVM垃圾回收的过程中,还需要解决的另外两个问题则是,什么时候可以回收这些内存垃圾,以及如何回收这些垃圾内存,在根搜索算法的基础上,现代虚拟机中垃圾搜索的实现当中主要存在的则是如下三种分别是标记-清除算法,复制算法,标记-整理算法,还有一个则是 分代收集算法,前三种算法均是扩展于根搜索算法,对于分代搜索算法,也会在下面做详细的相关介绍。
  1. 标记清除算法,堆的有效内存空间快被耗尽后,遍历所有的GC ROOTS,将所有GC ROOTS可达的对象标记为存活对象,然后清除所有GC ROOTS不可达的对象,(缺点:需要遍历所有的堆对象,判断是否和GC ROOTS 可达,效率低下,且在标记清除算法执行过程中,需要停止程序应用程序,第二:标记清除所清理出的内存是不连续的内存空间,因为被清除的对象是出现在内存的各个角落的,所以导致内存空间布局很乱,连续空间较为难找,则在重新分配数组对象时,寻找不到一个连续的内存空间,将会出现莫名的问题(数组的内存空间是连续的内存集合))
  2. 复制算法(又可以叫做,标记/复制/清除算法),复制算法与标记清除算法不同的是,复制算法将会把内存划分为两个不同的区间,分别是活动内存区间,和空闲内存区间,在任意的时间点,只会有一个内存区间被使用,当活动内存区间的有效内存被消耗完的时候,JVM则暂停程序运行,将活动区间中的存活对象全部复制到 空闲空间中,且严格按照内存地址进行依次排列,于此同时,GC线程将更新后的存活对象指向新的内存地址,且清空活动内存中所剩余的垃圾对象。可以看出的是,尽管复制算法也是用了GC ROOTS的遍历方式得到存活对象,然后全部复制到空间内存空间中,但复制算法弥补了 标记/清除算法中,内存混乱的问题,但缺点是:需要浪费一半的内存,做空闲内存,这是不可忽略的特点。
  3. 标记整理算法(又可以叫做,标记/整理/清除算法),于标记/清除算法不同的是,标记整理算法,1. 也是先遍历所有的GC ROOTS,将后将存活对象进行标记,2. 移动所对应的存活对象,且严格按照内存的空间地址进行移动,然后将末端的内存地址进行回收,相比于标记清除算法,解决了内存分散的特点,相比于复制算法,则消除了内存减半的代价,但是其唯一的缺点便是:标记整理的执行效率不高,既要标记又要整理对应的存活对象的引用地址,相对来说,整体的执行效率要低于复制算法。
 
上述三种GC的实现算法共同点和不同点分别是:(总结:):
  1. 共同点:都需要先暂停应用的执行,(因为都存在标记阶段,通过遍历GC ROOTS来判断得到存活的对象,即通过根搜索算法进行标记得到可回收对象,)
  2. 执行效率上:复制算法>标记/整理算法>标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
  3. 内存整齐度上:复制算法=标记整理算法>标记清除算法
  4. 内存利用率上:标记整理算法=标记清除算法>复制算法
可以看到标记清除算法,算是一个比较落后的算法,但是上述的算法在执行过程中,都存在或多或少的优缺点,所以在特定的场合或者说是内存结构中,使用特定的算法,都将会特别有效果,根据不同的内存情况来选择对一个的垃圾清理算法,是较为合适的一种行为;
 
问?为什么上述的三种算法在执行过程当中,都需要暂停应用程序的执行,?
答:首先上述三种算法都存在对象的标记行为,以此来判断出那些是可回收对象,那些是活跃对象,即根据GC ROOTS是否可达进行搜索标记,即上述所提到的根搜索算法,那么在通过根搜索算法,标记的过程当中,假设此时A对象被标记为了可回收对象,那么由于应用程序是在可执行状态下,此时又创建了B对象,且B对象引用A对象,是可达的,但是由于B对象的创建和引用是在标记之后,此时则会出现了A对象进行了垃圾回收,而B对象则通过,整理也好,复制也好,被保留了下来,那么此时程序再次通过B对象获取A对象时,则会出现A对象为null的情况,那么这必然是在程序的过程当中不允许出现的情况,所以在涉及到 标记 再最终有一个过程是清除过程的这样的算法中,必然是先执行对应的 垃圾清理算法,然后再算法执行过程后,通知唤醒对应的应用程序中线程,然后继续执行相关的应用程序的任务。(GC 的线程在执行的过程中,必然是和应用程序的线程相互配合,才能达到垃圾清理后,也不影响程序的正常运行的效果,比如此处的暂停应用程序的线程执行,先优先执行GC的垃圾回收的线程)
 
分代收集算法:
 
JVM中GC垃圾收集器-->常用的几种收集器即各收集器的使用区别;
 

 

 

 

posted @ 2019-03-09 09:33  陈咬金  阅读(444)  评论(0编辑  收藏  举报