堆
8.1概述
JVM启动时,堆创建初始和最大大小也就固定,JVM规范规定,堆在物理上可以是不连续的,但是在逻辑上是连续的。JVM最大的一块内存空间,
设置:-Xms 初始大小, -Xmx 最大大小 ,-XX:+PrintGCDetails 打印GC
所有线程共享堆,但是可以划分线程私有的缓冲区。
java虚拟机规范:所有对象实例以及数据都应该分配到堆上(The heap is the run-time data area from which memory for all class instances and arrays is allocated.)
https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-2.html#jvms-2.5.3
堆越大可以允许更少的GC
堆越小则依赖频繁GC,如果GC频率低则容易溢出。
堆的内存细分
java7之前:新生代+老年代+永久代
java8之前:新生代+老年代+元空间
8.2 设置堆空间大小和OOM
-Xms = -XX:InitialHeapSIze 默认物理机器最大内存1/64
-Xmx = -XX:maxHeapSize 默认物理机器最大内存的1/4
jstat -gc 进程Id 打印堆信息
当堆的大小超过-Xmx则会OutOfMemoryError
-X表示JVM运行时参数 ms 表示memory start ,mx 表示memory max, 默认单位字节
服务器开发中将二者设置为一样的。避免扩容和减小。
8.3 年轻代和老年代(老年代有可能内存是不连续的,存在碎片)
配置占比:
默认: -XX:NewRatio=2 ,表示 新生代占1,老年代占2。
-Xmn 设置新代大小
可以修改比如: -XX:NewRatio=4 ,表示 新生代占1,老年代占4。
可以用 jinfo -flag NewRatio 进程Id 来查看JVM进程的 newRatio参数值。
默认 -XX:SurvivorRatio 表示Eden区与其他两个SurVivor的比值为 8:1:1,但是实验效果时6:1:1
-XX:+UseAdaptiveSizePolicy 使用自适应的内存分配策略
-XX:-UseAdaptiveSizePolicy 不使用自适应的内存分配策略
IBM研究表明新生代80%的对象都是朝生夕死
8.4 对象分配过程
为新对象内存分配是非常严谨的,内存分配算法和垃圾回收算法是密切相关的。还需要考虑内存碎片。
一般过程:
1、一开始都放在eden区,当eden区满的时候,触发GC(YoungGC),根据可达性算法进行垃圾回收。
2、在一次回收之后,没有消亡的便放置到survivor的to区,接着to和from交换身份。
3、eden区再次满了之后,再次触发YoungGC,这时会联通from一起回收
4、将eden和from区的幸存者放置到survivor的to区,接着to和from交换身份。(to一定是空的)
5、重复1-4,当survivor区的对象达到一定年龄(默认15可通过-XX:MaxTenuringThreshold = <N> 进行设置)或者survivor的to区满了(多余年长者晋身),晋身到老年代。
6、如果老年代放不下,则进行FGC,再尝试放置。
7、如果FGC之后还是放不下,并且堆不可扩容了,便OOM
原则:GC频繁发视在新生代,较少发生在老年代,几乎不在永久代或元空间。
特殊:如果对象是超大对象,触发YGC后,eden肯定还是放不下的,则直接放置到old space。
面试中问GC算法的时候,先回答分配,再回答YGC,再回答FGC(mager GC/ full GC)。
晋身老年代的几种可能:
1、survivor区对象达到一定年龄
2、to区满了
3、大对象进来触发YGC后还是放不下的情况多余的年长者对象直接到old。
4、其他特殊情况,定制设计情况,对象进来的时候就知道这个对象肯定不简单就放到老年代或者永久代。
5-Minor GC、Major GC、 FullGC
Partial GC部分GC
新生代收集(Minor GC/Young GC)
老年代收集(Major GC/Old GC)
目前只有CMS GC 才会单独收集老年代
很多时候MajorGC 会和 Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
Mixed GC 收集整个新生代和部分老年代的垃圾收集 目前只有G1 GC会有这种行为。
整堆回收(Full GC)
收集整个堆和方法区的垃圾收集
年轻代GC minor GC
当年轻代空间不足,就会触发Minor GC
Minor GC 会引发STW(stop the world),暂停其他用户线程。垃圾回收结束,用户线程恢复。
老年代GC (Major GC)
出现Major GC通常会伴随一次Minor GC(但非绝对,Parallel Scavenge收集器直接进行MajorGC)
Major GC速度一般会比Minor GC慢10倍以上。(因为老年代内存不连续,有碎片)
Major GC之后内存不足就会OOM
Full GC (包括方法区的元数据)
Full GC执行原因
调用 System.gc。建议系统执行Full GC,但不必然执行
老年代不足
方法区不足
通过Minor GC后进入老年代导致空间不足
当放入to区的对象太大,只能转入老年代,且老年代可以用内存不足,触发Full GC
堆空间分代思想:
为什么分代,70%到90%的对象都是临时的。分代是为了提升VM效率。
内存分配策略
优先分配Eden
大对象直接old
长期存活的对象
动态对象年龄判断
如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需MaxTenuringThreshold中要求的年龄,为了减少to和from的复制。
survivor区放不下就放入了old
空间分配担保:
-XX:HandlePromotionFailure
TLAB(thread local allocation Buffer)
堆区是线程共享区域
由于对象创建都是再JVM非常频繁,因此在并发环境下,为了避免多个线程操作同一个地址,需要加锁,影响分配速度。
从内存分配角度对eden区域继续划分,JVM为每个线程在eden区分配了一个私有的缓存区域
当eden可以避免一系列,非线程安全问题,同时提升内存分配的吞吐量,将这种分配方式称为“快速分配策略”
所有从OpenJdk衍生出来的JVM都提供了TLAB
尽管不是所有对象实例都能在TLAB中创建成功,但是JVM将TLAB视为首选。
用-XX:+useTLAB 设置开启,-XX:-useTLAB设置关闭,默认开启。
TLAB很小只有eden的1%,可以通过-XX:TLABWasteTargetPercen 设置TLAB占eden空间的百分比大小
如果TLAB内存分配失败,JVM就会尝试通过加锁的机制确保数据原子性。从而在TLAB之外分配内存。
8.5 小结堆空间的参数设置:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:PrintFlagsInitail:查看所有参数的默认初始值
-XX:PrintFlagsFinal :查看所有参数的最终值 (":= " 代表我们修改了)
(命令方式:jps查看java进程,jinfo -flag survivorRatio 进程id)
-Xms: 设置堆空间初始内存大小(1/64)
-Xmx:设置推空间最大内存大小 (1/4)
-Xmn:设置新生代大小(初始和最大值)
-XX:NewRatio 设置老年代和新生代的占比
-XX:SurvivorRatio 设置新生代中eden:s0:s1
-XX:MaxTenuringThreshold :survivor中进入老年代的阈值
-XX:+PrintGCDetails 输出详细GC的处理日志
打印gc简要信息:-XX:PrintGC , -verbose:gc
-XX:HandlePromotionFailure 设置空间分配担保
在发生Minor GC之前,虚拟机会检查 老年代最大可用的连续空间是否大于新生代所有对象总空间
如果大于,则此次Minor GC是安全的
如果小于,则虚拟机会查看-XX:HandlePromotionFailure 设置值是否允许担保失败
如果true 那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平局大小。(如果以前每次进入老年代一个批次都是15M,这次居然有20M,老年代慌了,就干脆FullGC吧)
如果大于,则进行一次Minor GC,但是这个MinorGC依然是由有风险的。
如果小于,则改为进行一次Full GC
如果false,则改为一次FullGC
JDK update24之后,HandlePromotionFailure参数失效了,其实就是永远== true了。
问题1、如果suvivor区过小,会有什么问题?
minor GC失去意义,因为对象很容易进入老年代
问题2、如果survivor区过大,会有什么问题?
很容易就触发YGC
8.6 堆是分配对象存储的唯一选择吗?
随着JIT编译器的发展与“逃逸分析技术”逐渐成熟,栈上分配,标量替换,将导致一些微妙变化。
如果经过逃逸分析(escape analysis)后发现,一个对象并没有逃逸出去,就会可能被优化为栈上分配。这样无需再堆中分配,无需GC。
此外 TAOBAOVM 使用GCIH 将生命周期很长的放到heap之外。GC不会考虑GCIH的对象。
8.6.1 逃逸分析
通过逃逸分析,hotspot编译能够分析出一个新的对象的引用使用范围,只在该方法内部创建并只在本方法使用,就会在栈上分配对象。
public String test() {
StringBuilder sb = new StringBuilder();
sb.append("aaa");
sb.append("aaa");
return sb.toString();
}
栈上分配的对象,在出栈后,该对象就直接被C语言显示delete了。
在JDK 6u23之后,hotspot就默认开启逃逸分析。
-XX:+DoEscapeAnalysis 显示开启。
-XX:+PrintEscapeAnalysis 查看逃逸分析筛选结果。
这样的话,如果我们在一个for 循环中不停new 对象,但是该对象引用没有传递出去,那么就不会因此而OOM
8.6.2 代码优化
栈上分配
同步省略:
JIT编译器借助逃逸分析判断同步代码块使用的对象只能够被一个线程访问。而没有发布到其他线程,就会锁消除。
public void test() {
Object o = new Object();
synchronized(o) {
System.out.println(o);
}
}
这段代码就会被优化
public void test() {
Object o = new Object();
System.out.println(o);
}
其实编译器就可以直接转化了。
分离对象或标量替换:-XX:+EliminateAllocations ,默认开启
有的对象可能不需要作为连续的内存结构存在也可以被访问到,那么对象的部分或全部可以不存在内存,而是在CPU的寄存器中。
标量:无法再分解成更小的数据的数据,比如java的8种基本数据类型。
反之称为 "聚合量" 如对象。
如果JIT编译器发现该对象使用范围只在本方法。就会将这个对象肢解成标量(将其成员变量拿出直接使用)。
如果开启逃逸分析,但是没有开启标量替换,该对象还是会创建在堆中,并且参与GC,所以啊,JVM规范中(The heap is the run-time data area from which memory for all class instances and arrays is allocated.)这句话是绝对的。
用我代码体会一下:
/**
* -Xms600m -Xmx600m -XX:SurvivorRatio=8 -XX:NewRatio=2 -XX:+PrintGCDetails -XX:+DoEscapeAnalysis -XX:+EliminateAllocations
* -Xms600m -Xmx600m -XX:SurvivorRatio=8 -XX:NewRatio=2 -XX:+PrintGCDetails -XX:+DoEscapeAnalysis -XX:-EliminateAllocations
* @author : 江鹏亮
* @date : 2020-06-26 22:28
**/
public class EscapeAnalysisTest {
public static void main(String[] args) {
EscapeAnalysisTest escapeAnalysisTest = new EscapeAnalysisTest();
for (int i = 0; i < 1000000000; i++) {
escapeAnalysisTest.alocate();
//escapeAnalysisTest.buildString();
}
}
public String buildString() {
StringBuilder sb = new StringBuilder();
sb.append("aaaa");
sb.append("bbbbb");
return sb.toString();
}
public void alocate() {
InnerClass innerClass = new InnerClass();
innerClass.id = 1;
innerClass.name = 2;
}
class InnerClass {
private int name;
private int id;
}
}
The heap is the run-time data area from which memory for all class instances and arrays is allocated 这句话:
1、栈上分配其实并没有应用起来,案例中成功其实是依赖于标量替换。
2、1.8之后, intern字符串的缓存和静态变量和静态变量都被分配到永久代,而永久代被元空间取代。但是其真正的对象并不在元空间,而是在堆。其实我们在静态变量里面写的static String a = "aaa"一开始也是在eden中(亲测)