- 部分转载至其他博客,但当时为总结笔记在云笔记中,找不到原文地址,见谅。
- JVM运行时数据区域
分为 方法区、堆、虚拟机栈、本地方法栈、程序计数器
其中方法区和堆为所有线程共享的数据区,其余的虚拟机栈、本地方法栈、程序计数器为线程隔离的数据区
即每个线程有单独的虚拟机栈、本地方法栈、程序计数器。
关于他们的作用如下:
程序计数器:一块很小的内存空间,当前线程所执行的字节码的行号指示器(由于JAVA虚拟机的多线程是通过线程轮流切换并分配处理器执行时间来实现的,在任意时刻,一个处理器内核都只会执行一条线程中的指令,因此为了线程切换后能恢复到正确的执行位置,每个线程都要有单独的程序计数器),若线程执行一个JAVA方法,那该计数器记录的是正在执行的虚拟机字节码指令地址。若程序执行的是Native方法(下面会介绍),这个计数器值则为空。该区域为JAVA虚拟机区域没有规定任何OutOfMemoryError(内存溢出导致的错误)情况的区域。
JAVA虚拟机栈:
该区域也是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是JAVA方法执行的内存模型,即每个方法在执行期间时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。人们口头常说的栈,其实为虚拟机栈用的局部变量表部分,局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(refernece类型)和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型会占用2个局部变量空间,其余的数据类型占用1个。局部变量表所需的内存空间在编译器间分配完成,即进入一个方法时,该方法的局部变量空间是固定的,在方法运行期间不会改变。
该区域会有两种异常情况:若线程请求深度大于虚拟机栈中所允许的深度,则抛出StackOverflowError异常;若虚拟机栈可以动态扩展,那么在扩展时无法申请到足够内存时,则抛出OutOfMemoryError异常。需要注意的是,在单线程环境下,虚拟机栈都只会发生StackOverflowError异常,因为当栈空间无法分配时,无法确定是内存不足还是已使用栈空间太大,本质上是对一件事情的两种描述。在多线程环境下,通过不断建立线程的方式的确会发生OutOfMemoryError异常,但是无法确定是由于栈空间申请内存不足产生的,因为操作系统分配给每个进程的内存资源是有限的,例如32位的windows位2GB。虚拟机可以通过参数控制JAVA堆和方法区两个区域的最大值,其余的一些内存耗费忽略不计的情况下,剩余的内存空间就由虚拟机栈和本地方法栈瓜分,当线程分配到的栈容量越大(即这里栈能分配到很多空间,但是依然发生内存溢出),所能构造的线程数量就会减少。这时可以通过减少最大堆容量和减少栈容量来换取获得更多线程。
栈容量大小通过-Xss控制。
本地方法栈:
Native Method Stack,与虚拟机栈发挥的作用是相似的。区别不过是虚拟机栈位执行JAVA方法(字节码)服务,而本地方法栈则为虚拟机使用的Native方法服务。该区域也会抛出OutOfMemoryError、StackOverflowError异常。
JAVA堆:
对于大多数应用,JAVA堆是JVM所管理的内存中最大的一块,JAVA堆是被所有线程共享的一块内存区域,该区域的唯一目的就是存放对象实例,几乎所有的对象实例在这里分配内存。该区域也是垃圾收集器(GC)的主要区域,从内存回收角度看,由于现在垃圾收集器都采用分代算法,所以JAVA堆可以细分为:新生代和老年代。再细致一点的有Eden空间、From Survivor 空间、To Survior空间等。无论哪个区域都是存放的对象实例、细分只是为了更好更快地进行内存回收。JAVA堆可以处于物理上不连续的内存空间中,只要逻辑上是连续即可,当堆无法扩展时,将会抛出OutOfMemoryError异常。
堆的最小值通过-Xms控制、最大值通过-Xmx控制。
对于堆的OutOfMemoryError异常,一般是通过内存映像分析工具(如Eclipse Memory Analyzer)堆Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,即要先分清楚是内存泄漏Memory Leak还是内存溢出Memory Overflow,若是内存泄漏则通过工具查看泄漏对象到GC Root的引用链(下面会介绍到垃圾收集),于是就能找到是通过什么路径与GC Roots相关联导致无法进行垃圾回收。若是内存溢出,则检查虚拟机参数-Xmx和-Xms和机器物理内存。看是否能调大或者检查是否存在生命周期过长的对象。
方法区:
即人们所说的永久代(实际并不等价),该区域也是线程共享的内存区域。它用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虚拟机规范将方法区描述为堆的一个逻辑部分。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。
运行时常量池是方法区的一部分。Class文件有一个常量池用来存放编译器生成的各种字面量和符号引用,这部分内容在类加载后存放到方法区的运行时常量池中。
运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,间接限制常量池容量。
String.intern()是一个Native方法,作用为 如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象,若是首次出现则将此String对象添加到常量池中并返回此String的引用。
在JDK1.7中,对于String类的intern()方法,将不再复制对象实例,而是在常量池中记录首次出现的实例的引用。即JDK1.7的HotSpot虚拟机中,已经把原本放在永久代的字符串常量池移出。
直接内存:
直接内存并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现。
为了提高IO速度,在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
大小默认为堆最大值-Xmx,也可手动通过-XX:MaxDirectMemorySize设置。
垃圾收集GC
JAVA内存回收技术
即使有了垃圾回收机制,程序员还是需要排查各种内存溢出和内存泄漏问题,当垃圾收集成为系统达到高并发的瓶颈时,我们就更要对这项技术进行监控和调节。
判断一个对象的是否存活用可达性分析算法:
即通过一系列GC Roots对象作为根节点,从这些节点向下搜索,搜索走过的路径为引用链,当一个对象到GC Roots没有任何引用链时,则证明对象不可用,需要进行回收。
可作为GC Roots的对象:1)虚拟机栈中栈帧的本地变量表引用的对象
2)方法区中类静态属性的引用对象
3)方法区中常量的引用对象
4)本地方法栈中JNI(即一般的Native方法)引用的对象JDK1.2后对引用概念进行扩充,将引用划分为强引用、软引用、弱引用、虚引用
强引用:指程序代码中普遍存在,类似“Object obj = new Object ()”这类引用,只要强引用存在,垃圾收集器永远不会回收被引用的对象。
软引用:指有用但不是必须的对象,在系统将要发生内存溢出异常前,将会把这类引用对象列入回收范围进行第二次回收,若果第二次回收后还是内存溢出,则会抛出异常。
弱引用:也指非必须的对象,但强度比软引用要更弱一些。被弱引用关联的对象只能生存到下一次垃圾回收之前,即垃圾收集器工作时,无论内存是否足够,都会回收弱引用关联对象。
虚引用:也称为幽灵引用,最弱的引用关系,无法通过虚引用获取对象实例,虚引用也不影响对象生存与否,唯一目的就是能在这个对象呗垃圾收集器回收时收到一个系统通知。
当对象经过可达性分析发现是不可达对象时,并不是立即回收,还要有一个宣告死亡的过程,即判断是否有必要执行finalize()方法,若对象没有覆盖该方法或虚拟机已经调用过该方法则不执行。若判断为要执行则将对象放置入一个队列中,并由虚拟机建立一个线程去执行,但并不保证该方法的正常执行,也就是该方法可能执行缓慢或死循环,这时GC将会对队列中的对象进行二次标记,若此时对象已经与引用链上的对象建立关联,则可以被移出队列死里逃生,若没有则进行垃圾回收。即一个对象呗执行finelize()后对象不一定死亡。
方法区的垃圾回收
在JDK1.7中,HotSpot虚拟机中的永久代,即PermGen space(两者并不等价,前者为JVM的一种规范,后者为JVM规范的具体实现)。永久代垃圾回收主要为两部分:废弃常量和无用的类,且效率低下,判断废弃常量即为判断常量池中常量是否存在引用,但判断无用的类则必须同时满足3个条件:
1)该类所有实例被回收,堆中不存在任何该类的实例
2)加载该类的ClassLoader已经被回收
3)该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类的方法
由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。但是在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。下面我们就来看看 Metaspace 与 PermGen space 的区别。
Metaspace(元空间)
其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。 JDK 1.8中 PermSize 和 MaxPermGen 已经无效,JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,并且 JDK 1.8 中已经不存在永久代的结论,而类的元数据与类的加载器被一同放入Metasapce中。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
为什么JDK1.8要把永久代转为元空间呢
原因:
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。
- Metaspace不再与“老年代”绑定,由元数据虚拟机单独管理,分配本地内存;这样有几个好处:
- 在full gc时,元空间的数据不会被扫描到;
- CMS中特定于Permgen的复杂代码可以移除;
- Metaspace可以动态增长,Permgen(永久代)在运行时不可变;
- 在元空间中,类和其元数据的生命周期和其对应的类加载器是相同的;每个类加载器一块虚拟内存,内部再分成不同的小块;
- 元空间虚拟机管理内存的数据结构是链表,分配方式是分组分配,目前的缺点是有碎片;
- 内存分布对比
堆的分代以及垃圾回收
现在的垃圾回收器都采用分代收集算法,堆分代的唯一理由就是优化GC性能,如果没有分代,那么所有的对象都在一块,GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。堆可以细分为年轻代与年老代,并且年轻代还分为了三部分:1个Eden区和2个Survivor区(分别叫From和To),默认比例为8:1:1。一般情况下,新创建的对象都会被分配到Eden区(大对象直接分配到年老代),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每经过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中(一般为>15)。
在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都对整个半区进行内存回收,分配时不用考虑碎片的情况。
在GC开始的时候,对象存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。Eden区和Survivor区并不是按照1:1的比例来划分内存空间的,当Survivor空间不够用时,需要依赖老年代的空间来进行分配。
关于一些内存分配:
默认情况新对象优先分配到Eden空间,需要大量连续存储空间的JAVA对象直接分配到老年代
收集算法是垃圾收集的方法论对于垃圾收集器的具体实现,还有不同的垃圾收集器,如G1收集器等等等等。