虚拟机栈
描述的是方法执行时的运行模型,线程私有,与线程的生命周期相同。栈中的数据大小和生存期是确定的,缺乏灵活性,但存取速度比堆快。虚拟机栈是由一个个栈帧组成的,每个栈帧都拥有局部变量表(基本数据类型、对象引用)、操作数栈、动态链接、方法出口信息。方法执行时入栈,执行完出栈,出栈就相当于清空了数据。入栈和出栈的时机很明确,不需要进行垃圾回收
本地方法栈
线程私有。与虚拟机栈类似,区别是虚拟机栈为Java方法服务,而本地方法栈为虚拟机使用到的Native方法服务。同样不需要垃圾回收
程序计数器
线程私有的资源,JVM会给每个线程创建单独的程序计数器,可以看作是当前线程执行的字节码的行号指示器,解释器的工作原理就是通过改变这个计数器的值来确定下一条需要被执行的字节码指令。程序计数器与线程的生命周期相同
堆
线程共享。存放的是对象,通过new操作创建出来的对象的实例都存在堆空间中。不必关注堆内存需要开辟多少空间。堆内存中内存的释放是由GC完成的。当栈内存中不存在对象的引用时,GC会回收堆内存
方法区
线程共享。存放类信息、静态变量、常量、成员方法、编译器编译后的代码等数据。在类加载器加载class文件的时候,这些信息将会被提取出来,并存储到方法区中。方法区是所有线程共享的,因此被设计为线程安全的。
在这种算法中,假设堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并且初始化赋值后,该对象的计数器的值就设置为 1,每当有一个地方引用它时,计数器的值就加 1。例如将对象 b 赋值给对象 a,那么 b 被引用,则将 b 引用对象的计数器累加 1。
反之,当引用失效时,例如一个对象的某个引用超过了生命周期或者被设置为一个新值时,则之前被引用的对象的计数器的值就减 1。而那些引用计数为 0 的对象,就可以称之为垃圾,可以被收集。
特别地,当一个对象被当做垃圾收集时,它引用的任何对象的计数器的值都减 1。
优点:引用计数法实现起来比较简单,对程序不被长时间打断的实时环境比较有利。
缺点:需要额外的空间来存储计数器;难以检测出对象之间的循环引用,如果两个对象相互引用,即使被赋值为null,由于它们的引用计数不为零,不会被作为垃圾收集。现代虚拟机都不采用引用计数法判断对象是否该回收
可达性分析法也被称之为根搜索法,现代虚拟机基本都是采用这种算法来判断对象是否存活。可达性是指,如果一个对象会被至少一个在程序中的变量通过直接或间接的方式被其他可达的对象引用,则称该对象就是可达的。更准确的说,一个对象只有满足下述两个条件之一,就会被判断为可达的:
1.对象是属于根集中的对象
2对象被一个可达的对象引用
根集中的对象称之为GC Roots,也就是根对象。可达性分析法的基本思路是:将一系列的根对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,如果一个对象到根对象没有任何引用链相连,那么这个对象就不是可达的,也称之为不可达对象。
在这里,我们引出了一个专有名词,即根集,其是指正在执行的 Java 程序可以访问的引用变量(注意,不是对象)的集合,程序可以使用引用变量访问对象的属性和调用对象的方法。在 JVM 中,会将以下对象标记为根集中的对象,具体包括:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中的常量引用的对象
方法区中的类静态属性引用的对象
本地方法栈中的引用对象
活跃线程(已启动且未停止的 Java 线程)
优点:可以解决循环引用的问题,不需要占用额外的空间
缺点:多线程场景下,其他线程可能会更新已经访问过的对象的引用
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行。那些没有被任何引用的对象会被添加到要回收的集合中,进行回收。
标记无用对象,然后进行清除回收。后面的垃圾收集算法都是在此算法的基础上进行改进的。
标记-清除算法将垃圾收集分为两个阶段:
标记阶段:标记出可以回收的对象。
清除阶段:回收被标记的对象所占用的空间。
优点:实现简单,不需要对象进行移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。
标记整理算法标记的过程与标记清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于标记整理算法的收集器的实现中,一般增加句柄和句柄表。
优点:经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片的问题了。
缺点:GC 暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。
复制(Copying Collector)算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。
复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如“标记-整理”算法。
优点:标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。
缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。
当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
新生代和老年代的默认比例是1:2。新生代又分为Eden区、S0区和S1区,比例为8:1:1。新生代发生的GC称为Minor GC,老年代发生的GC称为Full GC
Minor GC
当Eden区将满时,触发 Minor GC,大部分对象被回收,少部分对象存活,这部分对象会被移动到S0区,同时对象年龄+1,最后将Eden区对象全部清理以释放空间。当触发下一次Minor GC时,将Eden区和S0区的存活对象移到S1区,同时清空S0区和S1区的空间。若再次触发Minor GC,S0区和S1区角色互换,重复前一个过程
对象晋升老年代的条件
1.当对象的年龄达到了设定的阈值,就会从S0或S1晋升老年代
2.当某个对象分配需要大量的连续内存时,该对象的创建不会分配在Eden区,会直接分配到老年代,防止移动时开销过大
3.S0或S1区中,相同年龄的对象大小之和大于空间一半以上时,年龄大于等于改该年龄的对象也会晋升到老年代
空间分配担保
在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,那么Minor GC 可以确保是安全的,如果不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行 Minor GC,否则可能进行一次 Full GC。