Java工程师知识树 / Java基础
概要
存在一个堆内存,堆也是 java 内存管理的核心区域。Java 堆区在 JVM 启动的时候即被创建,其空间大小也就确定了。是 JVM 管理的最大的一块内存空间。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)
《Java虚拟机规范》中对 Java 堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。
( The heap is the run-time data area from which memory for all class instances and arrays is allocated)
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象在堆中的位置。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
堆,是 GC (Garbage Collection,垃圾收集器) 执行垃圾回收的重点区域。
一句话描述:Java堆是JVM内存中最大的一块区域,JVM几乎所有的对象实例都在堆里分配内存并创建。堆内部区域的划分取决于JVM的垃圾回收策略,即GC策略。目前主流的GC策略大部分是基于分代收集算法的,如 parNew+CMS,或者G1等等。因此我们可以将Java堆再划分为
新生代
,老年代
。
存储数据
堆中存储:1.运行时类实例(即对象,包含全局变量);2.运行时数组;3.静态变量;4.运行时常量池。
不能在栈上存储数组和对象,因为栈帧被设计为创建以后无法调整大小。
栈帧只存储指向堆中对象或数组的引用。与局部变量数组(每个栈帧:调用方法创建栈帧)中的原始类型和引用类型不同,对象总是存储在堆上以便在方法结束时不会被移除。
对象和数组永远不会显式回收,而是由垃圾回收器自动回收。
通常,过程是这样的:
- 新的对象和数组被创建并放入新生代。
- Minor垃圾回收将发生在新生代。依旧存活的对象将从 eden 区移到 survivor 区。
- Major垃圾回收一般会导致应用进程暂停,它将在三个区内移动对象。仍然存活的对象将被从新生代移动到老年代。
- 每次进行老年代回收时也会进行永久代回收。它们之中任何一个变满时,都会进行回收。
堆的限制
OutOfMemoryError产生原因主要包含下面几种情况:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体;
- 使用的第三方软件中的BUG;
- 启动参数内存值设定的过小;
堆空间的大小设置:
-Xms 用于表示堆区的起始内存,等价于 -XX:InitialHeapSize
,eg:-Xms256M
-Xmx 用于表示堆区的最大内存,等价于-XX:MaxHeapSize
,eg:-Xmx256M
一旦堆区中的内存大小超过 “-Xmx
" 所指定的最大内存是,将会抛出 OutOfMemoryError
异常。
通常会将 -Xms
和 -Xmx
两个参数配置相同的值,其目的是为了能够在 java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况下,初始内存大小:物理电脑内存大小 / 64最大内存大小:物理电脑内存大小 / 4
堆内存区域
堆被分为了下面三个区域:
新生代:1.Eden区(伊甸区);2.Survivor区(幸存者区)0.1
老年代:养老区
永久代(jdk7以前)/元空间(jdk8以后):1.8开始持久代被废弃,使用元空间代替,元空间MetaSpace
并不是堆内存的一部分而是本地内存。
堆区与非堆区的区别
第一,Perm Gen(永久代)--1.8元空间
元空间是jdk8以后才加入的,用来替换原来的永久代。也就是说,原perm区(永久代)中的方法区,也在这里。从它原来的名字就可以看出来,永久代指的就是那些变动很少的数据,稳定为主。比如我们在jvm启动时,加载的那些class文件;以及在运行时,动态生成的代理类。
元空间的大小,默认是没有上限的。极端情况下,会一直挤占操作系统的剩余内存。
第二、CodeCache(代码缓存区)
CodeCahe存放的,就是即时编译器所生成的二进制代码。当然,JNI的代码也是放在这里的。
这个空间在不同的平台,大小都是不一样的,但一般够用了。但是把这个区域调的非常的小的情况下,JVM不会溢出,这个区域也不会溢出,但是会退化成解释型执行模式,速度和JIT不可同日而语,慢个数量级也是可能的。
第三、年轻代与老年代
存储在 JVM 中的 Java 对象可以被划分为两类:
-
- 一类是生命周期较短的瞬时对象,这类对象在创建和消亡都非常迅速
-
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与 JVM 的生命周期保持一致。
Java堆区进一步细分的话,可以划分为年轻代(YoungGen) 和老年代(OldGen)。年轻代又称为新生代/新生区。老年代又称为老年区/养老区。
其中年轻代有可以划分为 Eden 空间 、Survivor0 空间和 Survivor1 空间(有时也叫做 from区、 to 区)。
年轻代与老年代关系
大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。
需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
配置年轻代与老年代在对结构的占比:
年轻代与老年代的比例默认时 1:2;年轻代区的默认比例时 8:1:1
- 默认
-XX: NewRaio = 2
, 表示新生代占1, 老年代占2, 新生代占整个堆的 1/3 - 可以修改
-XX: NewRatio = 4
, 表示新生代占1, 老年代占4, 新生代占整个堆的1/5。
在 HotSpot 中,Eden 空间和另外两个 SurvIvor 空间缺省所占的比例是 8:1:1 开发人员可以通过选项 -XX:SurvivorRatio
调整整个空间比例。比如: -XX:SurvivorRatio = 8
几乎所有的 Java 对象都是在 Eden 区被new 出来的。绝大部份的 Java 对象的销毁都年轻代在进行了。可以使用选项 -Xmn
设置年轻代最大内存大小。
第四、对象分配与回收过程
- 针对幸存者s0, s1区的总结:复制之后有交换,谁空谁是to。
- 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。
对象的分配策略
(1)对象优先在Eden空间分配
当我们创建一个对象时,若没有特殊情况,这个对象会被分配在新生代的Eden
空间。此时,若Eden
空间不足,无法提供这个对象所需的空间时,将会触发垃圾回收机制,清理Eden
空间,为新对象腾出更多空间。在进行垃圾回收时,会将Eden
中仍然存活的对象放入一个空闲的Survivor
中,但是若Survivor
空间不足以存放这些存活的对象,则由于担保机制的存在,这些对象会被放入到老年代中。
(2)大对象直接在老年代分配
假设我们在代码中创建了一个很大的对象(比如数组或较长的字符串),而这个对象在很长一段时间都要使用,不会轻易被当作垃圾回收。这时候将面临一个问题:若将这个对象分配在新生代的Eden
中,每次进行垃圾回收时,都需要将这个大对象复制到Survivor
中保留,这个复制过程是一笔很大的开销,而且由于大对象占用了大量空间,垃圾回收将会频繁发生。所以,为了避免这种情况的发生,对于较大的对象,将会被直接分配到老年代中,原因是老年代发生垃圾回收的频率较低,而且不是使用复制算法进行垃圾回收。
那如何判断一个对象是否属于大对象呢?JVM
提供一个参数-XX:PretenureSizeThreshold
,通过这个参数来设定多大属于大对象。当需要为一个对象分配空间时,若此对象所需的空间大于这个参数的值,就会被判定为一个大对象,从而在老年代中为其分配空间。
(3)长期存活的对象将进入老年代
这个原则合情合理,老年代存在的首要目的,就是存放生命周期较长的对象,所以对于新生代中存活了很长时间的对象,就应该把他们移入老年代,而不是一直留在新生代中占用空间。每一个对象都有一个自己的年龄计数器,记录了自己的存活周期。对于新生代中的对象,初始时刻它的年龄计数器为0
,每经历一次垃圾回收后,年龄计数器+1
。当计数器的值到达设定好的阈值时(默认是15
),就证明这个对象是一个”老油条“,于是将它转入到老年代中。
对于生命周期长的对象,由于不会轻易死亡,所以每一次垃圾回收都会被它拖慢,而且垃圾回收对于短时间内不会死亡的对象也没有意义,所以不应该将它留在垃圾回收频繁的新生代占用空间,这就是需要将这种对象转让老年代的理由。JVM
中也提供了一个参数-XX:MaxTenuringThreshold
来设置对象进入老年代的阈值,当对象的年龄计数器超过这个值时将进入老年代。
(4)动态年龄判断
对于新生代中的对象,并不一定需要年龄计数器到达阈值才被放入老年代,有一种特殊情况会导致对象直接进入老年代。当新生代的Survivor
空间中,某一个年龄的对象相加,所占空间总和超过了Survivor
空间的一半,则大于或等于这个年龄的对象将直接进入老年代,而不需要到达阈值。比如说,在Survivor
空间中,年龄为5
的对象相加,所占空间超过了Survivor
总空间的一半,则所有年龄>=5
的对象,会被直接转入老年代。
第五、新生代与老年代使用的GC算法
对于新生代而言,这一块区域中的对象存活时间短,每一次垃圾回收都能回收大部分内存,所以适合使用复制算法进行垃圾回收,同时以老年代作为这个算法的担保空间;
对于老年代而言,每次垃圾回收只能释放小部分空间,若使用复制算法,每次将需要做大量复制,而且此时Survivor
需要较大的空间,所以不适合使用复制算法,因此在老年代中,一般使用标记—清除或者标记—整理算法进行垃圾回收。