JVM内存模型

运行时数据区域

java虚拟机规范,java虚拟机管理的内存将分为下面五大区域统称运行时数据区

其中方法区(也叫非堆)、堆线程共享,虚拟机栈、本地方法栈、程序计数器线程私有。

程序计数器

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。对于一个处理器,在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。

注:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。

虚拟机栈(Java栈)

每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程,栈先进后出。栈帧大小由编译期确定,不受运行期数据影响。(Hotspot vm 将虚拟机栈和本地方法中合为一块)

Java虚拟机栈可能出现两种类型的异常:

  • 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError,典型的递归深度过大容易出现。
  • 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

本地方法栈

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件。(Hotspot vm 将虚拟机栈和本地方法中合为一块)

堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。

如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)。

方法区

用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

 

内存溢出

两种内存溢出异常:

  • StackOverFlowError:当请求的栈深度大于虚拟机所允许的最大深度 ,通过-Xss控制。
  • OutOfMemoryError:虚拟机在扩展栈时无法申请到足够的内存空间,可通过-Xmx、-Xms控制。

对象的创建

选择哪种分配方式由java堆决定,而java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。

指针碰撞

假设java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式叫做指针碰撞。应用的收集器:Serial、ParNew。

空闲列表

如果堆中内存不是规整的,已使用的内存和空闲的内存相互交错,那就没办法进行指针碰撞了,虚拟机就必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式叫空闲列表。应用的收集器:CMS。

 

对象的访问

句柄访问:

简单来说就是java堆划出一块内存作为句柄池,引用中存储的是对象的句柄地址,句柄中包含对象实例数据、类型数据的地址信息。 优点:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是非常普遍的行为】只需改变句柄中实例数据的指针,不需要改动引用ref本身。

直接指针访问:

与句柄访问不同的是,引用中存储的直接就是对象的实例数据,但是类型数据跟句柄访问方式一样。 优点:优势很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。(可能是出于Java中对象的访问时十分频繁的,平时我们常用的JVM HotSpot采用此种方式)

GC

判断对象是否存活

1.引用计数算法:

早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。 优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。 缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。

2.可达性分析算法:

目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。 它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。

可作为GC Roots的对象有四种:

  • 虚拟机栈(栈桢中的本地变量表)中的引用的对象。
  • 方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。
  • 方法区中的常量引用的对象。
  • 本地方法栈中JNI(native方法)引用的对象

内存分配机制

这里所说的内存分配,主要指的是在堆上的分配,一般的,对象的内存分配都是在堆上进行,但现代技术也支持将对象拆成标量类型(标量类型即原子类型,表示单个值,可以是基本类型或String等),然后在栈上分配,在栈上分配的很少见,我们这里不考虑。

Java内存分配和回收的机制概括:分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。

年轻代

对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代,用-XX:PretenureSizeThreshold来控制)的Eden区,大部分的对象在创建后很快就不再使用,(IBM的研究表明,98%的对象都是很快消亡的),当Eden区没有足够空间进行分配时,虚拟机将发起一次GC,这个GC机制被称为Minor GC。注意,Minor GC并不代表年轻代内存不足,它只表示在Eden区上的GC。

年轻代可以分为3个区域:Eden区和两个存活区(Survivor 0 、Survivor 1)。在新生代中,使用“标记-复制”算法进行清理,将新生代内存分为2部分,1部分 Eden区较大,1部分Survivor比较小,并被划分为两个等量的部分。每次进行清理时,将Eden区和一个Survivor中仍然存活的对象拷贝到 另一个Survivor中,然后清理掉Eden和刚才的Survivor。

老年代

对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Minor GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC

大对象直接进入老年代:如果对象比较大(比如长字符串或大数组),则会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。

长期存活的对象将进入老年代:当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能够被Survivor容纳的话,将被移动到Suvivor空间中,并且对象的年龄设为1.对象在Suvivor区每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认是15岁,可以用-XX:MaxTenuringThreshold控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值),对象将被晋升到老年代中。
动态对象年龄判定:如果在Survivor空间中相同年龄所用对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到-XX:MaxTenuringThreshold中要求的年龄。

方法区(永久代)

 永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:

  • 类的所有实例都已经被回收
  • 加载类的ClassLoader已经被回收
  • 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)

 永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。HotSpot提供-Xnoclassgc进行控制,使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看类加载和卸载信息

 -verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;-XX:+TraceClassUnLoading需要fastdebug版HotSpot支持


空间分配担保

前面提到过的,为了把Suvivor无法容纳的对象直接进入老年代,在发生Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

垃圾收集算法:

分代收集算法,及对于不通内存区域采用不通的垃圾收集算法。年轻代使用复制算法,老年代使用标记/清除或标记/整理算法。

标记-清除

跟它的名字一样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记阶段:标记的过程其实就是前面介绍的可达性分析算法的过程,遍历所有的GC Roots对象,对从GC Roots对象可达的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象;

清除阶段:清除的过程是对堆内存进行遍历,如果发现某个对象没有被标记为可达对象(通过读取对象header信息),则将其回收

在垃圾收集器进行GC时,必须停止所有Java执行线程(也称"Stop The World"),原因是在标记阶段进行可达性分析时,不可以出现分析过程中对象引用关系还在不断变化的情况,否则的话可达性分析结果的准确性就无法得到保证。在等待标记清除结束后,应用线程才会恢复运行。

缺点:

1、效率问题。标记和清除两个阶段的效率都不高,因为这两个阶段都需要遍历内存中的对象,很多时候内存中的对象实例数量是非常庞大的,这无疑很耗费时间,而且GC时需要停止应用程序,这会导致非常差的用户体验。

2、空间问题。标记清除之后会产生大量不连续的内存碎片(从上图可以看出),内存空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

复制算法

为了解决效率问题,复制算法出现了。复制算法的原理是:将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块内存上,然后把这一块内存所有的对象一次性清理掉。

回收前:

回收后:

缺点

1、将内存缩小为原来的一半,浪费了一半的内存空间,代价太高;

 2、如果对象的存活率很高,极端一点的情况假设对象存活率为100%,那么我们需要将所有存活的对象复制一遍,耗费的时间代价也是不可忽视的

标记-整理

这种算法与标记/清除算法很像,事实上,标记/整理算法的标记过程任然与标记/清除算法一样,但后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象都向一端移动,然后直接清理掉端边线以外的内存。标记/整理算法不仅弥补了标记/清除算法存在内存碎片的问题,也消除了复制算法内存减半的高额代价,可谓一举两得。但任何算法都有缺点,就像人无完人,标记/整理算法的缺点就是效率也不高,不仅要标记存活对象,还要整理所有存活对象的引用地址,在效率上不如复制算法。

回收前:

回收后:

效率:复制算法 > 标记/整理算法 > 标记/清除算法(标记/清除算法有内存碎片问题,给大对象分配内存时可能会触发新一轮垃圾回收)

内存整齐率:复制算法 = 标记/整理算法 > 标记/清除算法

内存利用率:标记/整理算法 = 标记/清除算法 > 复制算法

垃圾收集器

年轻代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:Serial Old、Parallel Old、CMS 

特殊收集器:G1收集器[新型,不在年轻、老年代范畴内]

Serial

Serial收集器是最基本、发展历史最悠久的收集器。是新生代收集器,是单线程的收集器,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成(“Stop The World”)。

优点:对于Client模式下的jvm来说是个好的选择。适用于单核CPU【现在基本都是多核了】
缺点:收集时要暂停其它线程,有点浪费资源,多核下显得。

ParNew

ParNew收集器其实就是Serial收集器的多线程版本,新生代收集器,使用复制算法。收集算法、Stop The World、回收策略和Serial一样,就是可以有多个GC线程并发运行,它是HotSpot第一个真正意义实现并发的收集器。默认开启线程数和当前cpu数量相同,如果cpu核数很多不想用那么多,可以通过-XX:ParallelGCThreads来控制垃圾收集线程的数量。多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

优点:
1.支持多线程,多核CPU下可以充分的利用CPU资源 
2.运行在Server模式下新生代首选的收集器【重点是因为新生代的这几个收集器只有它和Serial可以配合CMS收集器一起使用】
缺点:在单核下表现不会比Serial好,由于在单核能利用多核的优势,在线程收集过程中可能会出现频繁上下文切换,导致额外的开销。

Parallel Scavenge

新生代收集器,使用复制算法,是并行的多线程收集器,关注CPU吞吐量(Throughput),即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算。Parallel Scavenge收集器提供了一些参数用于精确控制吞吐量。使用-XX:+UseParallelGC开关控制使用Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效),用开关参数-XX:+UseAdaptiveSizePolicy可以进行动态控制,如自动调整Eden/Survivor比例,老年代对象年龄,新生代大小等,这个参数在ParNew下没有。
 

Serial Old

Serial Old是Serial收集器的老年代版本,老年代收集器,单线程收集器,使用“标记-整理”算法。和新生代的Serial一样为单线程,Serial的老年代版本,不过它采用"标记-整理算法",这个模式主要是给Client模式下的JVM使用。
如果是Server模式有两大用途:
1.jdk5前和Parallel Scavenge搭配使用,jdk5前也只有这个老年代收集器可以和它搭配。
2.作为CMS收集器的后备。

Parallel Old

老年代收集器,多线程并行,使用“标记-整理”算法。在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。

CMS

CMS(Concurrent Mark Sweep)收集器一种以获取最短回收停顿时间为目标的收集器,并发收集、低停顿。采用"标记-清除" 可以设置参数XX:+UseConcMarkSweepGC启用。CMS收集器的内存回收工作是可以和用户线程一起并发执行。

的运作过程相对前面几种收集器来说更复杂一些分为4个步骤:初始标记、并发标记、重新标记、并发清除。其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。

G1

G1(garbage first:尽可能多收垃圾,避免full gc)收集器是当前最为前沿的收集器之一(1.7以后才开始有),同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。使用标记-清理复制算法,通过参数-XX:+UseG1GC开启。

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值