根据Java虚拟机规范,Java数据区域分为五大数据区域。
其中方法区和堆是所有线程共享的,虚拟机栈、本地方法栈和程序计数器则为线程私有的。
有的博客称方法区是永久代,那是因为前者是JVM的规范,而后者则是JVM规范的一种实现,并且只有HotSpot才有永久代,
JDK8中已经彻底移除了方法区,JDK8中引入了一个新的内存区域叫metaspace(元空间),后边详细介绍。
栈分为虚拟机栈和本地方法栈。
每个方法执行都会创建一个栈帧,用于存放局部变量表,操作栈,动态链接,方法出口等。每个方法从被调用,直到被执行完。对应着一个栈帧在虚拟机中从入栈到出栈的过程。 通常说的栈就是指局部变量表部分,存放编译期间可知的8种基本数据类型及对象引用和指令地址。局部变量表是在编译期间完成分配,当进入一个方法时,这个栈中的局部变量分配内存大小是确定的。
常见的的两种异常StackOverFlowError和OutOfMemoneyError。当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出OutOfMemoneyError。
本地方法栈与虚拟机栈的作用十分类似,不过本地方法是为native方法服务的。部分虚拟机(比如Sun HotSpot虚拟机)直接将本地方法栈与虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StactOverflowError与OutOfMemoryError异常。
当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。
Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。
唯一一块Java虚拟机没有规定任何OutofMemoryError的区块。
方法区/永久代是被所有线程共享区域,用于存放已被虚拟机加载的类信息、常量、静态变量等数据。永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。
在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区,此时hotspot虚拟机对方法区的实现为永久代
在JDK1.7字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区,也就是hotspot中的永久代
在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之,这时候字符串常量池还在堆, 运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace)
永久代在JDK8中被删除,被一个叫做元空间的区域所替代了。这项改动是很有必要的,因为对永久代进行调优是很困难的。永久代中的元数据可能会随着每一次Full GC发生而进行移动。并且为永久代设置空间大小也是很难确定的,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等。
默认情况下,元空间的最大可分配空间就是系统可用内存空间。因此,我们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。最终用户可以为元空间设置一个可用空间最大值,如果不进行设置,JVM会自动根据类的元数据大小动态增加元空间的容量。
注意:永久代的移除并不代表自定义的类加载器泄露问题就解决了。还必须监控内存消耗情况,因为一旦发生泄漏,会占用大量的本地内存,
堆被所有线程共享区域,在虚拟机启动时创建,唯一目的是存放对象实例。
堆区是垃圾回收的主要区域,通常情况下分为两个区块年轻代和老年代。年轻代又分为Eden区(存放新创建对象),From survivor区和To survivor区(两个survivor区保存gc后幸存下的对象)。默认情况下各自占比 8:1:1。
java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。
逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者被其它线程所引用。
在JVM运行时,可以通过配置以下参数改变整个JVM堆的配置比例
-Xms
、-Xmx
-XX:MaxPermSize
、-XXermSize
-XX:MaxDirectMemorySize
参数,或者-XX:MaxDirectMemorySize
-Xss
参数增加线程栈大小。新生代采用复制算法。老年代采用标记/清除算法或标记/整理算法。由于老年代存活率高,没有额外空间给他做担保,必须使用这两种算法。
算法分为2个阶段:
标记算法分为两种:
如图所示,当进行过标记清除算法之后,出现了大量的非连续内存。当java堆需要分配一段连续的内存给一个新对象时,发现虽然内存清理出了很多的空闲,但是仍然需要继续清理以满足“连续空间”的要求。所以说,这种方法比较基础,效率也比较低下。
为了解决效率与内存碎片问题,复制(Copying)算法出现了,它将内存划分为两块相等的大小,每次使用一块,当这一块用完了,就将还存活的对象复制到另外一块内存区域中,然后将当前内存空间一次性清理掉。这样的对整个半区进行回收,分配时按照顺序从内存顶端依次分配,这种实现简单,运行高效。不过这种算法将原有的内存空间减少为实际的一半,代价比较高。
从图中可以看出,整理后的内存十分规整,但是白白浪费一般的内存成本太高。然而这其实是很重要的一个收集算法,因为现在的商业虚拟机都采用这种算法来回收新生代。IBM公司的专门研究表明,新生代中的对象98%都是“朝生夕死”的,所以不需要按照1:1的比例来划分内存。HotSpot虚拟机将Java堆划分为年轻代(Young Generation)、老年代(Tenured Generation),其中年轻代又分为一块Eden和两块Survivor。
所有的新建对象都放在年轻代中,年轻代使用的GC算法就是复制算法。其中Eden与Survivor的内存大小比例为8:2,其中Eden由1大块组成,Survivor由2小块组成。每次使用内存为1Eden+1Survivor,即90%的内存。由于年轻代中的对象生命周期往往很短,所以当需要进行GC的时候就将当前90%中存活的对象复制到另外一块Survivor中,原来的Eden与Survivor将被清空。但是这就有一个问题,我们无法保证每次年轻代GC后存活的对象都不高于10%。所以在当活下来的对象高于10%的时候,这部分对象将由Tenured进行担保,即无法复制到Survivor中的对象将移动到老年代。
复制算法在极端情况下(存活对象较多)效率变得很低,并且需要有额外的空间进行分配担保。所以在老年代中这种情况一般是不适合的。
所以就出现了标记-整理(Mark-Compact)算法。与标记清除算法一样,首先是标记对象,然而第二步是将存货的对象向内存一段移动,整理出一块较大的连续内存空间。
在年轻代(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC。Minor GC当年轻代中eden区分配满的时候触发,只会清理年轻代。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。
full gc是收集整个堆,包括young gen、old gen、perm gen(如果存在的话)、元空间(1.8及以上)等所有部分的模式。
混合GC 收集整个young gen以及部分old gen的GC。只有G1有这个模式
在JDK1.2之前,使用的是引用计数器算法,即当这个类被加载到内存之后,就会产生方法区,堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用时,引用计数器继续+1,而当其中一个引用销毁时,引用计数器-1,当引用计数器减为0的时候,标志着这个对象已经没有引用了,可以回收了!但是这样会有一个问题:当我们的代码出现这样的情况时: ObjA.obj=ObjB ObjB.obj=ObjA 这样的代码会产生如下引用情形ObjA指向ObjB,而ObjB又指向objA,这样当其他所有的引用都消失了之后,ObjA和ObjB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。
可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看做一张图,从一个节点GC Root开始,寻找对应的引用节点,找到这个节点之后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
目前Java中可作为GC Root的对象有:
Object obj=new Object();
SoftReference<Object> sf=new SoftRerence<Object>(obj);
obj=null;
sf.get();//有时会返回null
这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然这个对象被标记为需要回收的对象时,则返回null;
软引用主要用于用户实现类似缓存的功能,在内存不足的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真实的来源查询这些数据。
Object obj=new Object();
WeakReference<Object> wf=new WeakReference<Object>(obj);
obj=null;
wf.get();//有时会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。弱引用主要用于监控对象是否已经被标记为即将回收的垃圾,可以通过弱引用的isEnQueues方法返回对象是否被垃圾回收器标记。
要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。可以通过如下代码实现
import java.lang.ref.ReferenceQueue;
public class Main {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}
}
如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是具体实现。jvm会结合针对不同的场景及用户的配置使用不同的收集器。 年轻代收集器: Serial、ParNew、Parallel Scavenge 老年代收集器: Serial Old、Parallel Old、CMS收集器 特殊收集器: G1收集器(新型,不在年轻、老年代范畴内)
最基本、发展最久的收集器,在jdk3以前是gc收集器的唯一选择,Serial是单线程收集器,Serial收集器只能使用一条线程进行收集工作,在收集的时候必须得停掉其它线程,等待收集工作完成其它线程才可以继续工作。
虽然Serial看起来很坑,需停掉别的线程以完成自己的gc工作,但是也不是完全没用的,比如说Serial在运行在Client模式下优于其它收集器(简单高效,不过一般都是用Server模式,64bit的jvm甚至没Client模式)
串行收集器组合 Serial + Serial Old
优点:对于Client模式下的jvm来说是个好的选择。适用于单核CPU(现在基本都是多核了) 缺点:收集时要暂停其它线程,有点浪费资源,多核下显得。
可以认为是Serial的升级版,因为它支持多线程[GC线程],而且收集算法、Stop The World、回收策略和Serial一样,就是可以有多个GC线程并发运行,它是HotSpot第一个真正意义实现并发的收集器。默认开启线程数和当前cpu数量相同,如果cpu核数很多不想用那么多,可以通过-XX:ParallelGCThreads来控制垃圾收集线程的数量。
优点:
缺点: 在单核下表现不会比Serial好,由于在单核能利用多核的优势,在线程收集过程中可能会出现频繁上下文切换,导致额外的开销。
采用复制算法的收集器,和ParNew一样支持多线程。 但是该收集器重点关心的是吞吐量(吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 如果代码运行100min垃圾收集1min,则为99%) 对于用户界面,适合使用GC停顿时间短,不然因为卡顿导致交互界面卡顿将很影响用户体验。 对于后台高吞吐量可以高效率的利用cpu尽快完成程序运算任务,适合后台运算
并行收集器组合 Parallel Scavenge + Parallel Old
Parallel Scavenge注重吞吐量,所以也成为"吞吐量优先"收集器。
JDK7和8中,作为年轻代默认的收集器
和新生代的Serial一样为单线程,Serial的老年代版本,不过它采用"标记-整理算法",这个模式主要是给Client模式下的JVM使用。 如果是Server模式有两大用途:
支持多线程,Parallel Scavenge的老年版本,jdk6开始出现,采用"标记-整理算法"。 在jdk6以前,新生代的Parallel Scavenge只能和Serial Old配合使用,而且Serial Old为单线程Server模式下无法充分利用多核cpu,这种结合并不能让应用的吞吐量最大化。
Parallel Old的出现结合Parallel Scavenge,真正的形成“吞吐量优先”的收集器组合。 JDK7和8中,作为老年代默认的收集器
CMS收集器(Concurrent Mark Sweep)是以一种获取最短回收停顿时间为目标的收集器。(重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器)启用CMS:-XX:+UseConcMarkSweepGC
正如其名,CMS采用的是"标记-清除"(Mark Sweep)算法,而且是支持并发的。 它的运作分为4个阶段:
以上初始标记和重新标记需要停掉其它运行java线程。之所以说CMS的用户体验好,是因为CMS收集器的内存回收工作是可以和用户线程一起并发执行。 总体上CMS是款优秀的收集器,但是它也有缺点:
Concurrent mode failure:如果CMS回收过程还没有执行完,老年代的剩余空间就用完了,或者,当前老年代空间不能满足一次内存分配请求(可能对象较大),那么此时将触发担保机制,停顿所有用户线程,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间;
浮动垃圾:由于cms支持运行的时候用户线程也在运行,程序运行的时候会产生新的垃圾,这里产生的垃圾就是浮动垃圾,cms无法当次处理,得等下次才可以。
假如有一个对象GC线程没有标记(用户线程之前没在用),然后轮到了用户线程,用户线程说,这个对象我重新又要用了,不要把这个对象GC掉,这个时候怎么办?假如这个时候处理不了,还是GC了,那么程序就直接报错了,这个是不允许的,解决办法可以百度搜索“cms 三色标记法”获取答案
G1(garbage first)收集器是当前最为前沿的收集器之一(1.7以后才开始有),同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。
当G1确定有必要进行垃圾回收时,它会先收集存活数据最少的区域(垃圾优先) g1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器。用到的算法为标记-清理、复制算法
G1是区域化的,它将java堆内存划分为若干个大小相同的区域"region“,JVM可以设置每个region的大小(1-32m,大小得看堆内存大小,必须是2的幂),它会根据当前的堆内存分配合理的region大小。
g1通过并发(并行)标记阶段查找老年代存活对象,通过并行复制压缩存活对象(这样可以省出连续空间供大对象使用)。g1将一组或多组区域中存活对象以增量并行的方式复制到不同区域进行压缩,从而减少堆碎片,目标是尽可能多回收堆空间,且尽可能不超出暂停目标以达到低延迟的目的。g1提供三种垃圾回收模式 young gc、mixed gc 和 full gc,不像其它的收集器,根据区域而不是分代,新生代老年代的对象它都能回收。几个重要的默认值,更多的查看官方文档oracle官方g1中文文档 g1是自适应的回收器,提供了若干个默认值,无需修改就可高效运作
JDK9中,G1作为默认的收集器 JDK7/8,默认关闭的,开启选项 -XX:+UseG1GC
更详细的G1垃圾回收介绍,请查看这篇文章: https://blog.csdn.net/coderlius/article/details/79272773