三者是包含关系,如图所示:
以car类为例
程序计数器是每个线程私有的,是一块较小的内存空间。可以看做是当前线程所执行字节码的行号指示器,此内存区域没有规定任何OutOfMemoryError情况的区域。
在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能 会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选 取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需 要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的, 在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线 程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立 的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私 有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指 令的地址;
如果正在执行的是Native方法,这个计数器值则为空(Undefined)。
== 线程私有的,生命周期与线程相同。==描述的是Java方法执行的内存模型:每个方法在执行的同时 都会创建一个栈帧(Stack Frame[1])用于存储局部变量表、操作数栈、动态链接、方法出口 等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出 栈的过程。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完 全确定了,并且写入到方法表的Code属性之中[2],因此一个栈帧需要分配多少内存,不会受 到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)
public class demo{
public static void main(String[] args){
add();
}
public void add(){
update();
.......
}
public void update(){
}
}
局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位。用于存放方法参数和方法 内部定义的局部变量。存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对 象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress类型(指向了一条字节码指令的地址)。
对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。Java 语言中明确的(reference类型则可能是32位也可能是64位)64位的数据类型只有long和double 两种。其余的数据 类型只占用1个。==局部变量表所需的内存空间在编译期间完成分配,==当进入一个方法时,这 个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变 量表的大小。
对这个区域规定了两种异常状况:
如果线程请求的栈深度大于虚 拟机所允许的深度,将抛出StackOverflowError异常;
如果虚拟机栈可以动态扩展(当前大部 分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如 果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
与虚拟机栈一样,本地方法 栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就 是存放对象实例,所有的对象实例以及数组都要在堆上分配。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”
,但是随着JIT编译器的发展与逃逸分析技 术逐渐成熟,栈上分配、标量替换[2]优化技术将会导致一些微妙的变化发生,所有的对象都 分配在堆上也渐渐变得不是那么“绝对”了。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上 是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是 可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的==(通过-Xmx和-Xms控制)。如 果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异 常==。
堆内存细分为新生区,养老区,永久区(jdk1.6和jdk1.7方法区可以理解为永久区,JDK1.8之后已经将方法区取消,替代的是元数据区,jdk8真正开始废弃永久代,而使用元空间(Metaspace),元数据区可以使用参数-XX:MaxMetaspaceSzie设定大小,这是一块堆外的直接内存,与永久区不同,如果不指定大小,默认情况下,虚拟机会耗尽可用系统内存
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚 拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规 范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。
相对而言,垃圾收集行为在这个 区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区 域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回 收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确 实是必要的。
当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError异常。
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版 本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于 存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常 量池中存放。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申 请到内存时会抛出OutOfMemoryError异常。
除了程序计数器外,虚拟机内存的其他几个运行时区域都 有发生OutOfMemoryError(称OOM)异常的可能
1.将堆的最小值-Xms参数与最 大值-Xmx参数设置为一样即可避免堆自动扩展
2.通过参数-XX: +HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆 转储快照以便事后进行分析
3 然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定
4.DirectMemory容量可通过-XX:MaxDirectMemorySize指定
Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达 路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生 内存溢出异常。
解决办法:
要解决这个区域的异常,一般的手段是先通过内存映像分析工具,对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也 就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到 泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握 了泄露对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄露代码的位 置。
如果不存在泄露,就是内存中的对象确实都还必须存活着,那就应当检查虚 拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是 否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消 耗。
关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无 法分配的时候,虚拟机抛出的都是StackOverflowError异常。
不断地建立线程,可以产生内存溢出异常。。每个线程分配到的栈容量越大,可以 建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
出现StackOverflowError异常时有错误 堆栈可以阅读,相对来说,比较容易找到问题的所在。
但是,如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚 拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是 比较苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景除 了上面提到的程序使用了CGLib字节码增强和动态语言之外,常见的还有:大量JSP或动态产 生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个 类文件,被不同的加载器加载也会视为不同的类)等。
由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显 的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就 可以考虑检查一下是不是这方面的原因。
======================================
1.尝试扩大堆内存,看结果,如果依然溢出,尝试2
2.则分析内存,看那一块出现问题
1.(使用专业工具)能够看到代码第几行出错,内存快照分析工具, MAT,JProfiler
2.Dubug 一行一行分析代码
为什么要了解GC和内存分配呢?答案很简单:当需 要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,就需要对这些“自动化”的技术实施必要的监控和调节。
从以下问题出发
垃圾回收主要是对 堆和方法区进行的
程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随 线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由JIT编译器 进行一些优化,,先忽略,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问 题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆和方法区则不一 样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也 可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配 和回收都是动态的,垃圾收集器所关注的是这部分内存Java堆是GC回收的“重点区域”。堆中基本存放着所有对象实例,gc进行回收前,第一件事就是确认哪些对象存活,哪些死去[即不可能再被引用]
当一个对象死了,不再被引用了,就可以对其进行回收了。
判断对象是否存活算法
1.引用计数算法 给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。
优点: 实现简单效率高,被广泛使用与如python何游戏脚本语言上。
缺点: 难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。
== 2.可达性分析算法 == 目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。 它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接,则证明对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处 于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。不建议使用finalize()方法,
finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及时。
javaGC泛指java的垃圾回收机制,该机制是java与C/C++的主要区别之一,写java代码一般都不需要编写内存回收或者垃圾清理的代码,也不需要像C/C++那样做类似delete/free的操作。内存会出现泄露,所以需要GC对内存进行回收,
GC分两种:
分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟 机中与内存相关的参数的设置。
判断对象是否存活算法 解决了哪些对象可以回收的问题,垃圾收集算法则关心怎么回收。
- 标记/清除算法【最基础】
首先标记出所有需要回收的对象,在标记完成后统一回收所有 被标记的对象
缺点:
- 复制算法
它将可用内存按容 量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是 对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况
缺点:空间代价大,内存利用率太低
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生 代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存 分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[1]。 当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最 后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10% 的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每 次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里 指老年代)进行分配担保(Handle Promotion)
- 标记/整理算法
总结:
内存效率: 复制>标记清除>标记压缩
内存整齐度: 复制=标记压缩>标记清除
内存利用率: 标记压缩=标记清除>复制
java jvm采用
分代收集算法
,对不同区域采用不同的回收算法:在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
新生代采用复制算法
绝大多数最新被创建的对象都会被分配到这里,由于大部分在创建后很快变得不可达,很多对象被创建在新生代,然后“消失”。对象从这个区域“消失”的过程我们称之为:Minor GC 。使用复制算法【复制算法比较适合用于存活率低的内存区域】。它优化了标记/清除算法的效率和内存碎片问题,将内存分为一块Eden空间和From Survivor、To Survivor【保留空间】,三者默认比例为8:1:1,优先使用Eden区,若Eden区满,则将对象复制到第二块内存区上。但是不能保证每次回收都只有不多于10%的对象存货,所以Survivor区不够的话,则会依赖老年代年存进行分配】。
GC开始时,对象只会存于Eden和From Survivor区域,To Survivor【保留空间】为空。
GC进行时,Eden区所有存活的对象都被复制到To Survivor区,而From
Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阈值(默认15是因为对象头中年龄战4bit,新生代每熬过一次垃圾回收,年龄+1),则移到老年代,没有达到则复制到To
Survivor。
老年代采用标记/清除算法或标记/整理算法
对象没有变得不可达,并且从新生代周期中存活了下来,会被拷贝到这里。其区域分配的空间要比新生代多。也正由于其相对大的空间,发生在老年代的GC次数要比新生代少得多。对象从老年代中消失的过程,称之为:Major GC 或者 Full GC。
由于老年代存活率高,没有额外空间给他做担保,必须使用这两种算法。
内存回收如何进行是由虚拟机所采用的GC收集 器决定的,而通常虚拟机中往往不止有一种GC收集器。
Serial收集器(-XX:+UseSerialGC)
Serial收集器是单线程的,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的 选择。
ParNew收集器(-XX:+UseParNewGC)
ParNew收集器其实就是Serial收集器的多线程版本,是许多运行在Server模式下的虚拟机中首选的新生代收集器
Parallel Scavenge收集器(-XX:+UseParallelGC)
Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总
消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
SerialOld 收集器(-XX:+UseSerialOldGC)
ParallelOld 收集器(-XX:+UseParallelOldGC)
CMS 收集器(-XX:+UseConcMarkSweepGC)
是HotSpot虚 拟机中第一款真正意义上的并发(Concurrent)收集器,以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记—清除”算法实现 的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:
G1 收集器
G1是一款面向服务端应用的垃圾收集器,具有以下特点: