堆针对一个 JVM 进程来说是唯一的,也就是一个进程只有一个 JVM,但是进程包含多个线程,他们是共享同一堆空间的。
数组和对象可能永远不会存储在栈
上,因为栈帧中保存引用
,这个引用指向对象或者数组在堆
中的位置。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集
的时候才会被移除。
堆,是 GC
(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
Java 8 及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
设置堆内存大小与 OOM:
Java 堆区用于存储== Java 对象实例==,堆的大小在 JVM 启动时就已经设定好了,可以通过选项"-Xmx"和"-Xms"来进行设置。
-XX:InitialHeapSize
-XX:MaxHeapSize
通常会将-Xms 和-Xmx 两个参数配置相同的值,其目的是为了能够在垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况下
年轻代与老年代:
其中年轻代又可以划分为 Eden 空间、Survivor0 空间和 Survivor1 空间(有时也叫做 from 区、to 区)
下面这参数开发中一般不会调:
配置新生代与老年代在堆结构的占比。
-XX:NewRatio=2
,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3-xx:SurvivorRatio
”调整这个空间比例。比如-xx:SurvivorRatio=8
为新对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配
、在哪里分配
等问题,并且由于内存分配算法
与内存回收算法
密切相关,所以还需要考虑 GC
执行完内存回收后是否会在内存空间中产生内存碎片
。
-Xx:MaxTenuringThreshold= N
进行设置总结
VM 在进行 GC 时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
针对 Hotspot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)
新生代
的垃圾收集很多时候 Major GC 会和 Full GC 混淆使用
,需要具体分辨是老年代回收还是整堆回收。整个 java 堆和方法区
的垃圾收集。年轻代 GC(Minor GC)触发机制:
频繁
,一般回收速度也比较快。这一定义既清晰又易于理解。STW
,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行老年代 GC(Major GC / Full GC)触发机制:
Full GC 触发机制(后面细讲)::
说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些
为什么要把 Java 堆分代?不分代就不能正常工作了吗?
其实不分代完全可以,分代的唯一理由就是优化 GC 性能。如果没有分代,那所有的对象都在一块。GC 的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当 GC 的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 survivor 空间中,并将对象年龄设为 1。对象在 survivor 区中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁,其实每个 JVM、每个 GC 都有所不同)时,就会被晋升到老年代
对象晋升老年代的年龄阀值,可以通过选项-XX:MaxTenuringThreshold
来设置
针对不同年龄段的对象分配原则如下所示:
MaxTenuringThreshold
中要求的年龄。-XX:HandlePromotionFailure
什么是 TLAB?
说明:
在发生 Minor GC 之前,进行判断:只要老年代的连续空间大于新生代Eden区对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则将进行 FullGC。
如何将堆上的对象分配到栈,需要使用逃逸分析手段。
逃逸分析的基本行为就是分析对象动态作用域
:
看逃逸小技巧:1. 看方法返回值类型。2. 看是否有参数传递 3. 看方法中对对象进行了修改
举例:
没有发生逃逸,没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除,每个栈里面包含了很多栈帧
发生逃逸,返回的是sb对象,为stringbuffer类型,可变类型,将sb暴露给外界了
没有发生逃逸,返回的是sb的string对象,为string类型,不可变类型
结论:开发中能使用局部变量的,就不要使用在方法外定义。
为什么:口述:尽量声明为局部变量有两个方面吧,第一个是线程安全的角度,比如说一个类中我们不声明为局部变量而是声明为类的成员属性时,那如果很多方法访问到它时,多线程环境下同时对它读写,所以可能会引发线程安全问题,那如果声明为局部变量的话,可以将它的作用域范围限制在 一个方法内,可以在一定程度来去避免这种线程安全问题,当然也不一定能解决,还得看方法参数传递呀等等具体情况的话不一样,但是声明这个局部变量的话也其实是线程安全的一种解决思路。第二个方法是GC方面,局部变量比如说我们new对象的话,那它如果在局部变量中声明并且经过逃逸分析后不会逃逸,那么其实对它的内存分配就不是在堆中了,而是在栈上分配,局部变量随着栈消亡而回收,不同像堆那样进过GC,进一步提高了性能。
使用逃逸分析,编译器可以对代码做如下优化:
一、栈上分配:将堆分配转化为栈分配。如果一个对象在子程序中被分配,并且不会发生逃逸,那对象就栈上分配的,而不是堆上分配
二、同步省略:如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
三、分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存
,而是存储在 CPU 寄存器中
。
栈上分配:==JIT ==编译器在编译期间
根据逃逸分析
的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配
。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了
。
同步省略:线程同步的代价是相当高的,同步的后果是降低并发性和性能。
在动态编译同步块的时候,JIT 编译器可以借助逃逸分析
来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么 JIT 编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
标量替换:在 JIT 阶段,如果经过逃逸分析
,发现一个对象不会被外界访问的话,那么经过 JIT 优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
point 这个聚合量经过逃逸分析
后,发现他并没有逃逸,就被替换成两个标量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用
。因为一旦不需要创建对象了,那么就不再需要分配堆内存
了。 标量替换为栈上分配
提供了很好的基础。
年轻代是对象的诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。
老年代放置长生命周期的对象,通常都是从 survivor 区域筛选拷贝过来的 Java 对象。当然,也有特殊情况,我们知道普通的对象会被分配在 TLAB 上;如果对象较大,JVM 会试图直接分配在 Eden 其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM 就会直接分配到老年代。当 GC 只发生在年轻代中,回收年轻代对象的行为被称为 MinorGc。
当 GC 发生在老年代时则被称为 MajorGc 或者 FullGC。一般的,MinorGc 的发生频率要比 MajorGC 高很多,即老年代中垃圾回收发生的频率将大大低于年轻代。