现代垃圾收集器大部分都基于分代收集理论设计,堆空间分为:
设置堆空间大小的参数
OutOfMemory举例
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}
class Picture{
private byte[] pixels;
public Picture(int length) {
this.pixels = new byte[length];
}
}
//Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
存储在JVM中的java对象可以被划分为两类:
java堆区进一步细分的话,可以分为年轻代和老年代
其中年轻代又可以划分为Eden空间,Survivor0和Survivor1空间(也叫from,to区)
配置新生代与老年代在堆结构的占比
在HotSpot中,Eden空间和另外两个Survivor空间大小所占比例为8:1:1
可以通过-XX:SurvivorRatio调整空间比例
几乎所有的Java对象都是在Eden区被new出来的
绝大部分的Java对象的销毁都在新生代进行了
可以通过-Xmn设置新生代的最大内存大小
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片
注意事项
survivor区满了不会进行垃圾回收,而是在伊甸园区满了之后垃圾回收算法对伊甸园区进行回收的同时,survivor区会被动的进行垃圾回收
总结
JVM在进行GC时,并非每次都对上面三个内存(新生代,老年代,方法区)区域一起回收的,大部分的时候回收都是指新生代
针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FULL GC)
部分收集:不是完整收集java堆的垃圾收集。其中又分为:
整堆收集(FULL GC):收集整个java堆和方法区的垃圾收集
年轻代GC(Minor GC)触发机制:
老年代GC (Major GC/Full GC)触发机制:
Fu11 GC触发机制
触发Fu1l GC执行的情况有如下五种:
说明: full gc是开发或调优中尽量要避免的。这样暂时时间会短一些。
为什么要把java堆分代?
不分代能正常工作吗?
其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
如果对象在Eden出生并经过第一次MinorGC 后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次MinorGC ,年龄就增加1 岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过选项-XX : MaxTenuringThreshold来设置
针对不同年龄段的对象分配原则如下所示:
什么是TLAB
分配过程
测试堆空间常用的jvm参数:
具体查看某个参数的指令: jps:查看当前运行中的进程
jinfo -flag SurvivorRatio 进程id
在发生MinorGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
在JDK6 Update24之 后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察OpenJDK中的源码变化,虽然源码中还定义了
HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么绝对了
在JVM中,对象是在java堆中分配内存的,这是一个普遍的常识,但是,有一种特殊的情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象没有逃逸出方法的话,那么久可能被优化成栈上分配。这样就无需在堆上分配内存,也无需进行;垃圾回收了。这也是最常见的堆外存储技术。
此外,在基于OpenJdk深度指定的TaoBaoVm,其中创新的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的java对象,以此达到降低GC的回收频率和提升GC的回收率的目的
参数设置:
在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析。
如果使用的是较早的版本,开发人员则可以通过:
选项“-XX: fDoEscapeAnalysis"显式开启逃逸分析
通过选项“-XX: +PrintEscapeAnalysis" 查看逃逸分析的筛选结果。
代码优化
栈上分配
JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成之后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需进行垃圾回收了。
常见的栈上分配场景:给成员变量赋值,方法返回值,实例引用传递
同步省略
public void f(){
Object hollis = new Object();
synchronized(hollis){
System.out.print(hollis)
}
}
代码中hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程所访问,所以在JIT编译阶段就会被优化掉。优化成:
public void f(){
Object hollis = new Object();
System.out.print(hollis)
}
标量替换
标量(Scalar)是指一个无法在分解成更小的数据的数据。Java中的原始数据类型就是标重.
相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
public static void main(String[] args) {
alloc();
}
private static void alloc(){
Point point = new Point(1,2);
}
class Point{
private int x;
private int y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
}
以上代码经过标量替换后就会变成
private static void alloc(){
int x = 1;
int y = 2;
}
可以看到,Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。
标量替换的好处:可以大大减少堆内存的占用,因为一旦不需要创建了,那么就不需要分配堆内存了
标量替换参数
-XX:+EliminateAllocations:开启了标量替换(默认打开),允许对象打散分配在栈上
本文地址:https://blog.csdn.net/qq_43455790/article/details/107059845
如对本文有疑问, 点击进行留言回复!!
现在微服务这么火,你还不了解吗?阿里P8推荐的微服务学习指南
论文笔记:SlowFast Networks for Video Recognition
网友评论