堆
知识点
- 多线程共享一个堆内存,且是最大的内存空间
- Java内存管理的核心区域
- 在JVM启动的时候创建、大小确定(但是可以调节)
- 《Java虚拟机规范》规定:堆可以处于物理上不连续的内存空间中,当在逻辑上它应该被视为连续的
- 其实在堆中,存在线程私有的缓冲区------》Thread Local Allocation Buffer, TLAB.
- “几乎”所有的对象实例都在这里分配内存。
- 栈帧中只会保留引用,这个引用指向对象或者数组的堆中的位置
- 方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
- 堆,是GC执行垃圾回收的重点区域
步骤:
- .class文件通过类加载到方法区中
- 栈根据方法区的类,实例化对象,对象的信息存储在堆当中
堆内存----细分
JDK7
新生区:Edn区、Survivor1、Survivor2
养老区:
永久区:--------在方法区当中
JDK8
新生区:Edn区、Survivor1、Survivor2
养老区:
元空间:--------在方法区当中
设置堆空间大小与OOM
Java堆区用于存储Java对象实例,在JVM启动时就已经设定好了
“-Xms”:用户表示堆区的起始内存,等价于 -XX:initialHeapSize
“-Xmx”:则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
一旦堆区中的内存大小超过-Xmx所指定的最大内存,就会抛出OOM异常
通常会将 -Xms 和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况:初始内存大小:物理电脑内存大小 / 64
最大内存大小:物理内存大小 / 4
假如没有设置初始化堆大小,但是设置了最大堆大小,堆的大小会自动调整
public class HeapSpaceInitial { public static void main(String[] args) { //返回Java虚拟机中的堆内存总量,(单位字节) long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; //返回Java虚拟机试图使用的最大堆内存量 long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; System.out.println(initialMemory); M System.out.println(maxMemory); M System.out.println(initialMemory * 64 / 1024); //15G System.out.println(maxMemory * 4 / 1024); //14G } }
手动设置初始化堆大小与最大堆内存
皮一下吧
假如设置的初始化堆大小 < 设置的最大堆内存
起飞,果然不行!!!
查看堆内存大小参数
方式1:jps / jstat -gc 进程id
方式2:-XX:+PrintGCDetails
(25600 + 4096 * 2 + 68608) / 1024 = 100M
年轻代与老年代
存储在JVM中的Java对象可以划分为两类:
活得短的与活的久的
分为:年轻代 + 老年代
年轻代结构
老年代结构
调整 《新生代和老年代》 在堆结构的占比
默认堆分为3份,新生代占1份,老年代占2份
通过上述命令:
新生代占1份,老年代占4份
调整《年轻代,Eden空间和另外两个Survivor空间比例》
默认年轻代配比:Eden占8份,Survivor0占1份,Survivor1占1份
通过上述指令:
Eden占10份,Survivor0占1份,Survivor1占1份
设置《年轻代大小》
百分之80的对象都在Eden区死掉了
其它指令:
-XX:-UseAdaptiveSizePolicy
对象分配过程
- new的对象先放伊甸园区,此区有大小限制
- 当伊甸园的空间填满时,程序有需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将不再被引用的对象进行销毁,再加载新的对象放在伊甸园区
- 然后对伊甸园区中的剩余对象移动到幸存者0区
- 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区
- 啥时候去养老区?可以设置次数,默认15次
- -XX:MaxTenuringThreshold = <N>进行设置
第一次清理:
第二次清理:也就是将S0的所有转移到S1,并将Eden内存活下来的类转移到S1,当中,此时S0为空了
第三次清理:假如S1满了,那么就将部分转移到永久区 + S0区,此时S1区就空了
总结:
针对幸存者S0,S1,复制之后有交换,谁空谁是to
关于垃圾回收:频繁在新生去收集,很少在养老区收集,几乎不再永久区/元空间收集
常用调优工具
Minor GC、Major GC、Full GC
JVM在进行GC时,并非每次都对上面三个内存(新生代、老年代、方法区)区域一起回收的,大部分时候回收的都是新生代
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
Partial GC(部分收集)
不是完整收集整个Java堆的垃圾收集。其中又分为:
新生代收集(Minor GC/Young GC):只是新生代(Eden、S0、S1)的垃圾收集
老年代收集(Major GC/old GC):只是老年代的垃圾收集
目前,只有CMS GC会有单独收集老年代的行为。
注意:很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
目前,只有G1 GC会有这种行为
整堆收集(Full GC):
收集整个Java堆和方法区的垃圾收集
年轻代GC(Minor GC)触发机制:
当年轻代空间不足时,就会触发Minor GC,这里的年轻代指的是Eden满,Survivor满是不会引发GC;每次Minor GC会清理年轻代内存
Minor GC非常频繁,一般回收速度比较快
Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
老年代GC(MajorGC/Full GC)触发机制:
指发生在老年代的GC,对象从老年代消失时,我们说Major GC或者Full GC发生了
出现Major GC,经常会伴随至少一次的Minor GC,但是不是绝对的-----一般的,老年代空间不足时,会先尝试Minor GC,如果之后空间还不足,则触发major GC
Major GC的速度一般会比Minor GC慢10倍,STW的时间更长
Major GC后,内存还不足,就报OOM
Full GC触发机制
调用System.gc()时,系统建议执行Full GC,但是不必然执行
老年代空间不足
方法区空间不足
通过Minor GC后进入老年代的平均大小 > 老年代的可用内存
由Eden区、survivor space0(from space)区向survivor space1(To space)区复制时,对象大小大于To space可用内存,则把该对象转存到老年代,且老年代的可用内存 < 该对象的大小
full GC是开发或调优中尽量避免的,这样暂时时间会短一些
堆空间分代思想
为什么需要Java堆分代?不分代就不能正常工作吗?
答:百分之80的对象都会死的很快,优化GC,免得每次上来就是整体扫描
内存分配策略(或者对象Promotion规则)
对象出生在Eden中-----经过一次Minor GC后存活-----》被Survivor容纳,该对象年龄设置为1-------》往后该对象在Survivor区中每活过一次Minor GC,年龄+1--------》当年龄 》 15,即经过了15次Minor GC,那么就晋升老年代
这个15阈值,可以通过-XX:MaxTenuringThreshold设置
一些原则
针对不同年龄段的对象分配原则如下:
优先分配到Eden
大对象直接分配到老年代
尽量避免大对象
长期存活的对象分配到老年代
动态对象年龄判断
如果Survivor区中的相同年龄对象的大小总和大于Survicor空间的一半,年龄大于或等于该年龄的对象直接进入老年代,无须等到maxTenuringThreshold
空间分配担保
-XX:HandlePromotionFailure