以下信息摘录自:深入理解JVM的内存结构及GC机制
JVM内存管理
JVM虚拟机常见面试题
https://mp.weixin.qq.com/s/eULjdiqj0RnWerruzWnDJA
根据JVM规范,JVM把内存区域划分成了以下几个区域:
1.方法区(Method Area)
2.堆区(Heap)
3.虚拟机栈(VM Stack)
4.本地方法栈(Native Method Stack)
5.程序计数器(Program Counter Register)
其中方法区和堆是所有线程共享的
方法区(Method Area)
方法区存放的是要加载的类的信息(包括类型、修饰符等)、静态变量(关于静态变量和静态方法的存储请参考:# where is a static method and a static variable stored in java. In heap or in stack memory)、构造函数、final定义的常亮、类中的字段和方法等字符串信息。方法区是全局共享的,在一定条件下也会被GC。当方法区超过允许的大小时,就会抛出OutOfMemory:PermGen Space异常。
在Hotspot虚拟机中,这块区域对应持久代(Permanent Generation),一般来说,在方法区上执行GC的情况很少,这也就是方法区被称作持久代的原因之一,但这并不代表方法区上就完全没有被GC的可能,方法区中的GC主要针对常量池的回收和已加载类的卸载。在方法区上进行GC,条件相当苛刻而且困难。
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译器生成的常亮和引用。一般来说,常量的分配在编译时就能确定,但也不全是,也可以存储在运行时产生的常量。比如String类的intern()方法,作用是String类维护了一个常量池,如果调用的字符"Hello"已经在常量池中,则直接返回常量池的地址,否则新建一个常量放入常量池中,并返回地址。
堆区(Heap)
堆区是GC发生最频繁的地方,也是理解GC机制最重要的区域。堆区是由所有的线程共享的,在虚拟机启动的时候创建。堆区主要用于存放对象实例及数组,凡是通过new生成的对象都存放在堆中,对于堆中的对象生命周期的管理由Java虚拟机的垃圾回收机制GC进行回收和统一管理。类的非静态成员变量也放在堆区,其中基本数据类型是直接保存值,而复杂类型是保存指向对象的引用,非静态成员变量在类的实例化时开辟空间并且初始化。所以你要知道类的几个时机,加载-连接-初始化-实例化。
虚拟机栈(VM Stack)
虚拟机栈占用的操作系统内存,每个线程对应一个虚拟机栈,他是线程私有的,生命周期和线程一样,每个方法被执行时产生一个栈桢(Statck Frame),栈桢用于存储局部变量表、动态链接、操作数和方法返回值等信息,当方法被调用时,栈桢入栈,当方法调用结束时,栈桢出栈。
局部变量表中存储着方法相关的局部变量,包括各种基本数据类型及对象的引用地址等,因此他有个特点:内存空间可以在编译时间就确定,运行时不再改变。
虚拟机栈定义了两种异常类型:StackOverFlowError(栈溢出)和OutOfMemoryError(内存溢出)。如果线程调用的栈深度大于虚拟机允许的最大深度(比如递归调用),则会抛出StackOverFlowError,不过大多数虚拟机都允许动态扩展虚拟机栈的大小,所以线程可以一直申请栈内存,知道内存不足时,抛出OutOfMemoryError。
本地方法栈(Native Method Stack)
本地方法栈用于支持native方法的执行,存储了每个native方法的执行状态。本地方法栈和虚拟机栈他们的运行的方式一致,唯一得到区别就是:虚拟机栈执行jaba方法,本地方法栈执行native方法。在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将虚拟机栈和本地方法栈一起使用。
程序计数器(Program Counter Register)
程序计数器是一个很小的内存区域,不在RAM上,而是直接划分在CPU上,程序员无法操作它,它的作用是:JVM在解释字节码(.class)文件时,存储当前线程执行的字节码行号,只是一种概念类型,各种JVM所采用的的方式不一样。字节码解释器工作时,就是通过改变程序计数器的值来取下一条要执行的指令,分支、循环、跳转等基础功能都是依赖此技术区来完成的。
每个程序计数器只能记录一个线程的行号,因此它是线程私有的。
如果程序当前正在执行的是一个Java方法,则程序计数器记录的是正在执行的虚拟机字节指令的地址,如果执行的是native方法,则计数器的值为空,此内存区是唯一不会抛出OutMemoryError的区域。
GC机制
随着程序的运行,内存中的实例对象、变量等占据的内存越来越多,如果不及时的进行回收,会降低程序的运行效率,甚至引发系统异常。
在上面分析得五个内存区域中,有3个是不需要进行内存回收的:本地方法栈、程序计数器、虚拟机栈。因为他们的生命周期是和线程同步的,随着线程的销毁,他们占用的内存会自动释放。所以,只有方法区和堆区需要进行垃圾回收,回收的对象就是那些不存在任何引用的对象。
查找算法
经典的引用计数算法,每个对象添加到引用计数器,每被引用一次,计数器+1,失去莹莹,计数器-1,当计数器在一段时间内为0时,即认为该对象可以被回收了。但是这个算法有一个明显的缺陷,当存在两个对象相互引用时,但这两个对象都没被其他对象引用时,理论上应该把二者都回收掉,但是由于他们自己相互引用造成计数器不为0,不符合垃圾回收的条件,所以就导致无法回收这一块内存区域。因此,sun的JVM并没有采用这种算法,而是采用一个叫做根搜索算法,如图:
基本思想是:从一个叫做GC的根节点出发,向下搜索,如果一个对象不能达到GC ROOTS的时候,说明该对象不再被引用,可以被回收。如上图中Object5、Object6、Object7,虽然他们三个依赖相互引用,但是他们其实已经没有作用了,这样就解决了引用计数算法的缺陷。
补充概念,在JDK1.2之后引入了四个概念:强引用、软引用、弱引用、虚引用。
强引用:只要是直接new出来的对象都是强引用,当对象还被其他变量引用的时候GC无论如何都不会回收,即使抛出OOM异常。
软引用:只有当JVM内存不足时才会被回收。
弱引用:只要GC,就会立马被回收,不管内存时候充足。
虚引用:可以忽略不计,JVM完全不在乎虚引用,你可以理解它是用来凑数的。它唯一的作用就是做一些跟踪记录,辅助finalize函数的使用。
最后总结一下,什么样的对象需要被回收:
1.该类的所有实例都已经被回收;
2.加载该类的ClassLoad已经被回收;
3.该类对应的反射类java.lang.Class对象没有被任何实例引用。
内存分区
堆内存主要被分为三块:新生代(Youn Generation)、旧生代(Old Generation)、持久代(Permanent Generation)。三代的特点不同,造就了他们使用的GC算法不同,新生代适合生命周期较短,快速创建和销毁的对象,旧生代适合生命周期较长的对象,持久代在Sun Hotpot虚拟机中就是指方法区(有些JVM根本就没有持久代这一说法)。
新生代(Youn Generation):大致分为Eden区和Survivor区,Survivor区又分为大小相同的两部分:FromSpace和ToSpace。新建的对象都是从新生代分配内存的,Eden区不足时会把存活的对象转移到Survivor区。当新生代进行垃圾回收时会发出Minor GC(也称作Youn GC)。
旧生代(Old Generation):旧生代用于存储新生代多次回收依然存活的对象,如缓存对象。当旧生代满了的时候就需要对旧生代进行回收,旧生代的垃圾回收称作Major GC(也叫做Full GC)。
持久代(Permanent Generation):在Sun 的JVM中就是指方法区的意思,尽管大多数JVM没有持久代。
GC算法
常见的GC算法:复制、标记-清除和标记-压缩
复制:复制算法采用的方式为从根集合进行扫描,将存活的对象移动到一块空闲得到区域,如图所示:
当存活的对象较少时,复制算法会比较搞笑(新生代的Eden区就是采用这种算法),其带来的成本就是需要一块额外的空闲控件和对象的移动。
标记-清除:该算法采用的方式是从根集合开始扫描,对存活的对象进行标记,标记完毕后,在扫描整个空间中未被标记的对象,并进行清除。标记-清除的过程如下:
上图中蓝色部分是有被引用的对象,黄色部分是没有被引用的对象。在标记阶段,需要进行全盘扫描,这个过程是比较耗时的。
清除阶段清理的是没有被引用的对象,存活的对象呗保留。
标记-清除动作不需要移动对象,且仅对不存活的对象进行清理,在空间中存活对象较多时比较高效,但由于只是清除,没有重新整理,当未被引用对象较多时,会造成内存碎片。
标记-压缩:该算法与标记-清除算法类似,都是想对存活的对象进行标记,但是在清除后会把活的对象向左端空间空间进行移动,然后再更新其引用对象的指针,如下图所示:
由于进行了移动规整动作,改算法避免了标记-清除算法的内存碎片问题,但由于需要进行移动,因此成本也增加了。(该算法适用于旧生代)
垃圾回收器
在JVM中,GC是由垃圾回收器来执行,所以,在实际应用场景中,我们需要选择合适的垃圾收集器,下面我们介绍一下垃圾收集器。
Serial GC(串行收集器)
serial GC是最古老也是最基本的收集器,但是现在依然被广泛使用,JAVA SE5和JAVA SE6中客户端虚拟机采用的是默认的配置。比较适合于只有一个处理器的系统。在串行处理器中minor和major GC过程都是用一个线程进行回收的。它的最大特点就是在进行垃圾回收时,需要对所有正在执行的线程暂停(stop the world),对于有些应用是难以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在指定范围之内,大多数应用还是可以接受的,而且,事实上它并没有让我们失望,几十毫秒的停顿,对于我们客户端是完全可以接受的,该收集器适用于单CPU、新生代控件较小且对暂停时间要求不是特别高的应用上,是client级别的默认GC方式。
ParNew GC(新式GC)
基本上和serial GC一样,单本质却别就是加入了多线程机制,提高了效率,这样它就可以被用于服务端上,同时它可以与CMS GC配合,所以,更加有理由将他用于server端。
Parallel Scavenge GC(并行清除GC)
在整个扫描和复制过程采用多线程的方式进行,适用于多CPU、对暂停时间要求较短的应用,是server级别的默认GC方式。
CMS (Concurrent Mark Sweep)GC
该收集器的目标是解决Serial GC停顿的问题,以达到最短回收时间。常见的B/S架构的应用就适合这种收集器,因为其高并发、高响应的特点,CMS是基于标记-清除算法实现的。
CMS收集器的优点:并发收集、低停顿,但远没有达到完美;
CMS收集器的缺点:
1.CMS收集器对CPU资源非常敏感,在并发阶段虽然不会导致用户停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降。
2.CMS收集器无法处理浮动垃圾,可能出现“Concurrnet Mode Failure”,失败而导致另一次的Full GC。
3.CMS收集器是基于标记-清除算法的实现,因此也会产生碎片。
G1收集器
相比CMS收集器有不少改进,首先,基于标记-压缩算法,不会产生内存碎片,其次可以比较精确的控制停顿。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
RTSJ垃圾收集器
RTSJ垃圾收集器,用于Java实时编程