堆的核心概述(重要)
1)堆是线程共享 进程唯一的
程序运行时 就可以创建一个JVM的实例 而在JVM实例中又会创建一个唯一的堆内存 因此在进程中堆也是唯一的
但是一个进程中可能包含着多个线程 堆内存是线程间共享的
2)一个JVM实例只存在一个堆内存 堆也是Java内存管理的核心区域
3)Java堆区在JVM启动的时候创建 其空间大小也就确定了 通常是JVM管理的最大一块内存空间
堆内存的大小是可以调节的(后期是可以通过参数来设置分配堆内存的大小)
4)Java虚拟机规范规定:堆可以处于物理上不连续的内存空间中 但是逻辑上它应该被视为连续的
堆内存是JVM中比较大的一块内存区域 在逻辑上连续的好处是 如果有比较大的对象 那我们直接分配一块比较大的内存地址就可以了
但是物理内存往往是不连续的 但是这也是可以实现的
因为即使分配的物理内存是不连续的 我们仍然可以使用地址映射(地址映射表)的方式组成一块较大的内存实现对于大的对象的内存分配
5)所有的线程共享Java堆 在这里还可以划分线程私有的缓存区(Threa Local Allpcation Buffer ,TLAB)
避免了堆中的数据都是线程共享的而导致线程同步的问题 而在TLAB空间内为每个线程都分配一些空间 是线程私有的
6)Java虚拟机规范中Java堆的描述:所有的对象实例都应该在堆上分配空间
实际上 是几乎所有的对象 还是有部分的对象并不是在堆上分配对象的
在JVM的更新迭代中 为了性能的提升 引入了逃逸分析
也就是说如果在栈方法中的对象是否发生了逃逸 如果没有发生逃逸的话 那么就可以在栈中分配对象
7)数组和对象可能永远不会存储在栈上 因为栈上保存引用 指向堆中对象或者数组的位置
8)在方法结束后 堆中的对象不会马上移除 仅仅在垃圾收集的时候才可能会被移除
a)也就是说 方法执行之后 可能堆中的对象和数组已经没有任何人引用它
但是它并不会立即被移除 而是等待垃圾回收线程进行回收
这里要和栈中的数据进行区分 栈不考虑垃圾回收 也正是因为出栈之后 就自动的回收
b)不会立即执行垃圾回收是为了性能考虑 不希望经常的进行垃圾回收
因为在垃圾回收时是可能发生 stop the world 那么原本的线程(用户线程)就停滞 等待垃圾回收机制进行完毕
这里也提到了GC和用户线程是否能并发执行的问题
9)堆是GC(Garbage Collection垃圾收集器)执行垃圾回收的终点
查看进程的内存大小
使用Java自带的监控工具jvisualVM可以监控堆内存的使用情况
堆的细分内存结构(重要)
现代垃圾收集器大部分都是基于分代收集算法设计的 堆空间细分:
Java7及之前堆内存逻辑上分为三部分:新生区+老年区+永久区
Java8及之后堆内存逻辑上分为三部分:新生代+老生代+元空间
约定:新生区= 新生代=年轻代
约定:养老区=老年区=老年代
约定:永久区=元空间
逻辑上包括三部分 但是事实上我们现在讲的堆的内存只包含新生代和老生代的内存
而元空间(永久代)的内存在方法区中 并不受到堆的管理
1 设置堆内存大小与OOM
Java堆区用于存储Java对象实例 那么堆的大小在JVM启动时就设定好了
如果设置的不合适 那么就容易造成OOM问题
可以通过”-Xmx”和 “-Xms”来设置
1)”-Xms”用于表示堆区的起始内存 等价于-XX:InitialHeapSize
2)”-Xmx”用于表示堆区的最大内存 等价于-XX:MaxHeapSize
一旦堆区的内存大小超过”-Xmx”所指定的最大内存时 就会抛出OutOfMemoryError异常
如何来记忆这些指令呢?
-X: 是jvm的运行参数
ms:memory start
比如-Xms256M, -Xms14420,-Xms2G都是可以的
这里所说的指令来设置对空间的大小 设置的其实就是新生代+老生代所占内存的大小
1)通常情况下将-Xms和-Xmx设置为相同的值
其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小 从而提高性能
频繁的扩容和释放堆空间会消耗一定的系统性能
2)默认情况下 初始化内存大小:物理电脑内存大小/64
默认情况下 最大内存大小:物理电脑内存大小/4
3)使用jps命令可以查看当前程序中运行的进程
4)jstat:来显示JVM在GC时候的统计信息和进程的使用情况
jstat -gc 9928
9928代表的是通过jps命令得到的运行进程的id
如果我们设置的堆的初始内存和最大内存都是600M的话 实际上我们的堆真正能使用的内存不能达到600M
原因:
堆中的内存内存又可以分为eden区+From区+To区 其中From区和To区中最多只有一个区域是有对象的
2 年轻代与老年代
1)存储在JVM中的java对象可以被划分为两类:
a)一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常的迅速
b)另一类对象的生命周期非常长 在某些极端的情况下还能够与JVM的生命周期保持一致
2)Java堆区进一步划分的话 可以划分为年轻代(YoungGen)和老年代(OldGen)
其中年轻代又可以划分为Eden空间、Survivor0空间和Suervivor1空间(有时也叫From区和To区)
3)默认情况下:新生代:老年代 = 1:2
也就是说 堆区的内存 老年代占2/3 新生代占1/3
这个参数在开发中一般是不会修改的 但是JVM提供了修改的命令
-XX:NewRatio 代表的是老生代和新生代的比例 默认值是2
可以修改 比如 -XX:NewRatio=1
4)设置新生代中Eden和Survivor区的比例
在JAVA的JVM文档中注明 默认的比例(Eden:Survivor0:Survivor1)为8:1:1
但是多次的通过命令行(jps jstat -gc 进程id)得到的比例还是6:1:1
后面查阅了相关资料 发现JVM是有自适应的堆内存分配可能导致的
然后配置了关闭自适应分配 发现问题还是没有解决 -XX:-UseAdaptiveSizePolicy
如果我们希望JVM的新生代的分区空间大小比例为8:1:1 需要显式的去指定
-XX:SurvivorRatio=8
5)针对不同年龄段的对象分配原则如下:
a)优先分配到Eden区
b)大对象可以直接分配到老年代 避免程序中出现大量的大对象
c)长期存活的对象分配到老年代
d)动态对象年龄判断
如果Survivor区中相同年龄的对象大小的总和大于Survivor空间的一半
那么可以直接将年龄大于或者等于该年龄的对象进入老年代 而不需要等待到最大年龄
注意:在开发中一定要避免创建大量的大对象 因为大对象可能伴随着频繁的GC
再者如果大对象是朝生夕死的 那就会更加频繁的GC!!
3 图解对象分配过程
对象分配的一般过程:
1)new的对象先放在伊甸园区 此区有大小限制
2)当伊甸园区的空间填满时 程序又需要创建对象
触发垃圾回收器(Minor GC)对伊甸园区进行垃圾回收 将伊甸园区中的不再被其他对象引用的对象进行销毁
步骤:
a)把 “存活对象” 复制到S0中
b)清空 Eden 区
c)将 S0 中的 “存活对象” 年龄(Age)设置为 1
4)当伊甸园区的空间再次填满时 再次触发垃圾回收(Minor GC)
GC步骤:
a)将 Eden 区和 S0 中的“存活对象” 复制 到S1中
b)清空 Eden 和 S0 区
c)然后将 Eden 中复制到 S1 中的对象年龄设置为 1 将 S0 中复制到 S1 中的对象年龄加 1
5)之后如果再次触发GC 基本就按上面的步骤进行
存货的对象在S0和S1之间来回移动
每移动一次 年龄加1
6)什么时候进入老年区
每经过一次垃圾回收的对象 若该对象没有被销毁 那么它的age值就会+1
默认达到15时(可以设置) 就会将其放到老年代
这个值可以通过 -XX:MaxTenuringThreshold= 进行设置
7)那么幸存者0区和幸存者1区如何进行垃圾回收呢?
幸存者0区和幸存者1区的对象是跟随这MinorGC一起进行垃圾回收的
并且当这两个区的对象空间满了的时候 也有机制让其提前转为老生代的对象
总结
1)针对幸存者S0、S1区的总结:复制之后有交换 谁空谁是To
也是说 我们的幸存者0区和幸存者1区这个内存空间还可以叫From区和To区
但是From区和To区所表示的位置是不固定的 会发生交换 也就是发生交换后永远会保证空的是To区
To区:表示下一次GC时要转入对象的区域
From区表示:下一次GC发生时的要转移对象的原空间
2)关于垃圾回收:频繁发生在新生区收集 很少在老年区收集 几乎不再永久区/元空间收集
当我们持续的创建对象并一直持有对象的引用(也就是不让垃圾收集回收它)这样一定是会出现OOM问题的
当Old区满时 虚拟机渴望进行Full GC来销毁Old区中无效的对象 但是发现我们的对象都持有引用 就发生了OOM
4 Minor GC Major GC Full GC
JVM在进行GC时 并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收的 大部分时候回收的都是指新生代
针对HotSpot VM的实现 它里面的GC按照回收区域又分为两大类型:一种是部分收集(Partial GC) 一种是整堆收集(Full GC)
部分收集
不是完成收集java堆的垃圾收集 其中又分为:
1)新生代收集(Minor GC/Young GC)
只是新生代(Eden/S0,S1)的垃圾收集
2)老年代收集(Major GC/Old GC)
只是老年代的收集
目前只有CMS GC会有单独收集老年代的行为
3)混和收集(Mixed GC)
收集整个新生代和部分老年代的垃圾收集
目前只有G1 GC会有这种收集方式
整堆收集(Full GC)
收集整个Java堆和方法区的垃圾收集
最简答的分代式GC策略的触发条件
1 年轻代GC(Minor GC)触发条件
1)当年轻代空间不足时就会触发Minor GC
这里的年轻代满的意思是:Eden区满 Survivor满是不会触发Minor GC的 而是直接将对象存入到老年代
2)因为新生代的对象虽然创建的很多 但是很多都是创建出来然后短时间就又销毁掉
因此Minor GC发生的很频繁 一般回收的速度也比较快 也不一定等Eden区满了才触发
3)Minor GC会引发STW(Stop the world)暂停其他线程 等待垃圾回收结束 用户线程才能恢复
为什么发生STW?
因为在GC线程运行的时候 是要判断哪些是垃圾 哪些对象需要被回收 并对对象进行位置的调节
这时候如果有用户线程在操作 那就可能造成并发的问题
2 老年代GC(Major GC)
1)指发生在老年代的GC 对象从老年代消失时 我们就说”Minor GC”发生了
2)出现了Major GC 经常会伴随至少一次的Minor GC(但并非绝对的 在Parallel Scaenge收集器的收集策略中有直接进行Major GC的策略选择过程)
也就是在老年代空间不足时 会先尝试触发Minor GC 如果之后空间还不足则触发Major GC
3)Major GC的速度一般会比Minor GC慢10倍以上 STW的时间更长
4)如果Major GC后 内存还不足 就报OOM
3 Full GC的触发机制
总的来说 Full GC触发的情况涵盖了整个堆空间 并且包括了方法区
Full GC是开发和调优中要尽量避免的 这样暂停的时间会短一些
触发Full GC执行的情况有如下5种:
1)调用System.gc()时 系统建议执行Full GC 但是不是一定执行
2)老年代空间不足时
3)方法区空间不足时
4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
5)由Eden区和From区向 To区复制时 对象大小大于To Space区的内存
则想要将该部分的对象转存到老年代 但是发现又大于老年代的可用内存
5 堆空间分代思想
堆空间为什么要采用分代收集的思想?可以不采用分代收集吗?
采用分代收集的思想是因为:经过大量的研究和实验 不同的对象的生命周期是不同的
70-99%都是临时对象 也就是说这些对象在创建之后使用一段时间后 就需要被销毁掉
因此采用分代收集的思想就会很高效率的回收掉需要被销毁的对象
总的来说 分代就是为了优化GC的性能 不采用分代收集也是可以的 只是GC的效率比较低
6 为对象分配内存:TLAB(Thread Local Allocation Buffer)
什么是TLAB?
1)从内存模型而不是垃圾回收角度考虑 对Eden区域继续进行划分 JVM为每个线程分配了一个线程私有缓冲区域 它包含在Eden区域内
2)多线程同时分配内存时 使用TLAB可以避免一系列的非线程安全的问题
同时还能够提升内存分配的吞吐量 因此将这种分配方式称为快速分配策略
3)已知的OpenJDK都是默认开启TLAB的
为什么是TLAB(Thread Local Allocation Buffer)?
1)堆区是线程共享区域 任何线程都可以访问到堆区中共享的数据(有利于进程之间的通信)
2)由于对象实例的创建都在JVM中非常频繁 那么在并发环境下从堆区操作对象可能是不安全的
3)为了避免多个线程操作同一地址的话 就考虑加锁的机制 但是加锁就会影响效率
![%ZLFP6ET8OQ8G]P@_8EN(I.jpg
在加入了TLAB之后对象地址的分配
会优先在Eden区的TLAB线程私有的区域分配对象的地址 如果满了才会去找Eden区的共享区域
7 总结堆空间的参数设置
1)-XX:+PrintFlagsInital
查看所有参数的默认初始值
2)-XX:+PrintFlagsFinal
查看所有参数的最终值(可能会存在修改 不再是初始值)
3)-Xms
初始化堆空间内存(默认是物理内存的1/64)
4)-Xmx
初始化堆空间的最大内存(默认是物理内存的1/4)
5)-Xmn
设置新生代的大小(初始值以及最大值)
6)-XX:NewRatio
配置新生代和老年代在堆结构的占比
7)-XX:SurvivorRatio
设置新生代中Eden区和s0/s1区的空间占比
8)-XX:MaxTenuringThreshold
设置新生代垃圾回收的最大年龄
9)-XX:+PrintGCDetails
输出详细的GC处理日志
10)-XX:+PrintGC 或者 -verbose:gc
打印gc简要信息
11)-XX:HandlePromotionFailure
是否设置空间分配担保**