堆
JVM启动时就创建了堆,其大小也就确定了(堆得大小在启动前可调节),是JVM管理最大的一块内存区。一个JVM实例只存在一个堆内存。 堆可以在物理上不连续的内存空间,但是逻辑上要连续。
所有的对象实例和数组都应当在运行时分配在堆上。
堆是gc垃圾回收的重点区域,在方法结束后,堆中的对象不会马上被移除,只有当垃圾收集是才会被移除。
内存细分
现代垃圾收集器大部分都是基于分代收集理论设计,堆空间细分为:
- jdk7及之前:新生区(Young)+养老区(Old)+永久区(Perm)
- jdk8及之后:新生区+养老区+元空间(Meta Space)
设置堆空间大小
“-Xms”:用来设置堆空间初始内存大小(年轻代+老年代),单位默认字节,k,m
“-Xmx”:用来设置堆最大内存空间大小(年轻代+老年代),
“默认堆空间大小”:初始为物理电脑内存的 1/64,最大为物理电脑内存的 1/4
查看设置参数: -XX:+PrintGCDetai在这里插入图片描述 或用javaVisualVM查看
//获取堆内存初始值
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//获取堆内存最大值
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms :"+initialMemory);
System.out.println("-Xmx :"+maxMemory);
System.out.println("系统内存大小:"+initialMemory * 64.0 / 1024 +"G");
System.out.println("系统内存大小:"+maxMemory * 4.0 / 1024 +"G");
OOM :
OutOfMemoryError:堆溢出
年轻代与老年代
配置新生代与老年代的比例
默认(可修改):-XX:NewRatio=2,新生代占1,老年代占2,新生代占整个堆 1/3
-XX:-SurvivorRatio=8 设置新生代中Eden与Survivor的比例为8:1:1(默认也是8:1:1,但是由于自适应内存分配策略的影响不会达到,所以要加前面的参数解决)
几乎所有的java对象都是在 Eden 区被new出来的。
对象分配概述
1、new的对象先放进Eden区。
2、当Eden(伊甸园)区填满时,JVM垃圾回收器才会对Eden进行垃圾回收(Minor GC == Y GC)
,将不再被其他对象所引用的对象进行销毁(图中为红色),又会有新创建的对象进来。
3、然后将Eden中的剩余对象移动至幸存者区,“age” 加 1。
4、幸存者区中分 from 和 to它们是随机变化的,to 代表空,from中含有对象。这里就是经过垃圾回收Eden剩余的对象移动至 s1区(to区,空),s0区幸存的对象也移动至 s1,age增长。
5、经过15次垃圾回收,也就是age为15的对象将会晋升到老年区。(默认是15次,这个可通过 -XX:MaxTenuringThreshould=16更改)
总结:
- 针对幸存者s0,s1区:复制之后有交换,谁空谁就是 to
- 关于垃圾回收:频繁在新生区收集,很少在养老区收集,机会不在永久区/元空间收集
minor gc 新生代
magor gc 老年代
Full gc 收集整个java堆和方法区的垃圾回收,是开发或调优中尽量要避免的,这样暂时时间会短一些。
分代的目的就是优化 gc性能。
内存分配策略
- 优先分配到Eden
- 大对象直接分配到老年代 (Eden无法容纳)
- 长期存活对象分配到老年区
- 动态对象年龄判断·
- 空间分配担保 :极端例子经过minor gc后对象全部存活,survivor区无法存储这么多对象,只能存放在老年区(由老年区进行空间分配担保)。
为对象分配内存:TLAB
为什么有TLAB?
- 堆区是线程共享区域(一个进程的多个线程共享堆数据)。
- 由于对象的实例是在堆中创建,当多个线程并发创建会导致堆区中划分内存空间线程不安全。
- 为避免多个线程操作同一地址,需要加锁等机制,进而影响分配速度。
什么是TLAB?
从内存模型的角度,对Eden区域继续进行划分
,**JVM为每个线程分配了一个私有缓存区域**
,它包含在Eden空间内。
多个线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能提高内存分配的吞吐量,称之为快速分配策略
。
TLAB空间的内存非常小,仅占整个Eden空间1%
,虽然不是所有对象实例都能够在TLAB中成功分配,但是JVM首选内存分配仍是TLAB
。
“-XX:UseTLAB” 开启TLAB空间(默认开启)。
分配过程:
堆空间常用参数
- -XX:+PrintFlagsInitial:查看所有参数的默认初始值
- -XX:+PrintFlagsFinal:查看所有参数的最终值
- -Xms:初始堆空间内存值(默认物理内存 1/64)
- -Xmx:最大堆空间内存值(默认物理内存 1/4)
- -Xmn:设置新生代的大小(初始值即最大值)
- -XX:NewRatio:配置新生代与老年代在堆结构占比
- -XX:SurvivorRatio:设置新生代中Eden与s0,s1的比例
- -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
- -XX:+PrintGCDetails:输出详细的gc处理日志
- 打印gc简要信息:-XX:+PrintGC 、-verbose:gc
- -XX:HandlePromotionFailure:是否设置空间分配担保
空间分配担保:
在jdk之前:在发生minor GC之前,虚拟机会检查老年代的最大可用的连续空间是否大于新生代所有对象的总空间
。
- 如果大于,则此次minor GC安全
- 如果小于,则虚拟机会查看 -XX:handlePromotionFailure设置值是否允许担保失败
- 如果-XX:handlePromotionFailure=true,那么会继续
检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
。- 如果大于,则会进行minor GC,但有一定风险
- 如果小于,则会改为 Full GC
- 如果 -XX:handlePromotionFailure=false,则会改为Full GC
- 如果-XX:handlePromotionFailure=true,那么会继续
在jdk7即以后:只要**老年代的连续空间大于新生代对象的总大小**
或者**历次晋升的平均大小就会进行minor GC**
,否者就会进行Full GC
逃逸分析
堆是分配对象存储唯一选择吗?
不是,经过逃逸分析后发现,一个对象没有逃逸出方法,那么就有可能被优化成栈上分配。
,这样就无须在堆上分配,也不用垃圾回收。
随着逃逸分析技术
逐渐成熟,栈上分配、标量替换优化技术
使得对象分配到堆变得不那么绝对。
逃逸分析就是确定对象的作用域: 主要看new的实体
- 当一个对象在方法中定义后,对象只在方法内部使用,则认为没有发生逃逸。可分配到栈
public void method(){
Student student = new Student();
student.say();
//......
}
- 当一个对象在方法中定义后,它被外部方法所引用,则认为发生了逃逸。
public StringBuffer method(String s1,String s2){
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(s1);
stringBuffer.append(s2);
return stringBuffer;
}
总结:开发中能使用局部变量的,就不要使用方法外定义。
代码优化
一、栈上分配。逃逸分析开启且未发生逃逸
常见逃逸场景:成员变量赋值、方法返回值、实例引用传递
二、同步省略(琐消除)。同步代码块使用的锁对象只能被一个线程访问即可消除琐。如下面每个线程new出来的锁对象地址都不同所以可以消除。
public void methodA(){
Object o = new Object();
synchronized (o) {
System.out.println(o);
}
}
public void methodB() {
Object o = new Object();
System.out.println(o);
}
三、分离对象或标量替换
标量
是指一个无法再分解成更小的数据的数据,例如java的基本数据类型。
相反的还可以分解的数据叫做聚合量
,java中的对象就是聚合量,它还可以分解成其它集合量和标量。
在 JIT 阶段,经过逃逸分析,发现一个对象不会被外界访问的话,那么经过 JIT 优化,就会把这个对象拆解成若干个其它成员变量来代替。称之为标量替换。