一个进程对应一个jvm实例,一个jvm实例只有一个运行时数据区,有多个线程共享同一个堆,每个线程有私有的程序计数器,本地方法栈,虚拟机栈
一个jvm实例只存在一个堆内存,堆也是java内存管理的核心区域。
java堆区在jvm启动时就被创建,其空间大小也就确定了,是jvm管理的最大一块内存空间
堆内存大小是可以调节的
《java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为是连续的
所有的线程共享java堆,在这里还可以划分线程私有的缓冲区(thread local allocation buffer, tlab)
一个进程一个堆验证
package com.cxf.heap; /** * -Xms=10m * -Xmx=10m */ public class HeapDemo { public static void main(String[] args) { System.out.println("start"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end"); } }
package com.cxf.heap; /** * -Xms=20m * -Xmx=20m */ public class HeapDemo1 { public static void main(String[] args) { System.out.println("start"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end"); } }
如图所示区域加起来是等同于我们我们设置的堆大小
《java虚拟机规范》中对java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。
“几乎”所有的对象实例都会在这里分配内存—从实际使用角度来看
数组和对象永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
在方法结束后,堆中的对象不会马上移除,仅仅在垃圾收集的时候才会被移除
堆是gc执行垃圾回收的重要区域
字节码指令中每一次的new都会创建对象实例,并在堆中开辟空间
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
java堆区用于存储Java对象实例,那么堆的大小在jvm启动时就已经设定好了,大家可以通过选项“-Xms”和“-Xmx”来进行设置
-Xms用于表示堆的起始内存,等价于-XX:InitalHeapSize
-Xmx用于表示堆的最大内存,等价于-XX:MaxHeapSize
一旦堆区中的内存大小超过-Xmx所指定的内存就会抛出OutofMemoryerror异常
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
默认情况下:初始内存大小:物理电脑内存/64
最大内存大小:物理电脑内存大小/4
OOM的举例说明
示例代码及参数
package com.cxf.heap; import java.util.ArrayList; import java.util.Random; /** * -Xms600m -Xmx600m */ public class HeapSpaceOomTest { public static void main(String[] args) { ArrayList<pictures> pictures = new ArrayList<>(); // long l = Runtime.getRuntime().maxMemory(); // System.out.println(l/1024/1024+"M"); while (true){ try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } pictures.add(new pictures(new Random().nextInt(1024*1024))); } } } class pictures{ private byte[] piexs; public pictures(int length){ this.piexs=new byte[length]; } }
存储在jvm中的java对象可以被划分为两类
一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
另外一类的对象的生命周期却非常长,在某些极端的情况下甚至能和jvm的生命周期保持一致
java堆区进一步细分的话,可以划分为年轻代(youngGen)和老年代(oldGen)
其中年轻代又可以划分为eden空间、survivor0空间和survivor1空间(有时也叫做from区、to区)
下边这参数开发中一般不会调
配置新生代与老年代在堆结构的占比
默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
package com.cxf.heap; /** * -Xms600m -Xmx600m */ public class test9 { public static void main(String[] args) { System.out.println("打酱油"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
内存比例我们可以通过jvisualvm、jps jstat -gc pid、jps jinfo -flag newratio pid 来查看新生区和老年区的内存比例
在hotspot中,eden空间和另外survivor空间缺省所占的比例是8:1:1
当然开发人员可以通过选项-XX:SurvivorRation 调整这个空间比例。比如-XX:SurvivorRatio=8
几乎所有的java对象都是在eden区被new出来的
绝大部分的java对象的销毁都在新生代进行了
ibm公司的专门研究标明,新生代中80%的对象都是朝生夕死的
可以使用选项“-Xmn”设置新生区最大内存大小
这个大小一般使用默认值就好
默认是有一个自适应的机制
所以上边打酱油的那个程序你在查看堆内新生区的内存分配的时候斌并不是按我们上述的8:1:1而是6:1:1
可以使用-XX:-UseAdaptiveSizePolicy 来关闭自适应的机制
发现没有用 哈哈哈哈嗝
还是得用
-XX:SurvivorRatio=8 手动设置比例
在运行时就是默认的8:1:1了
为新对象分配内存是一件非常严谨和复杂的任务,jvm的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法和垃圾回收算法密切相关,所以还需要考虑gc执行完内存回收后是否会在内存空间中产生内存碎片
1.new的对象先放到伊甸园区,此区有大小限制
2.当伊甸园区的空间填满时,程序又需要创建对象,jvm的垃圾回收器将对伊甸园区进行垃圾回收,minor gc,将伊甸园区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到伊甸园区
3.然后将伊甸园区中的剩余对象移动到幸存者0区
4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区
5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区
6.啥时候可以去养老去呢?可以设置次数,默认是15次
7.在养老区,相对悠闲。当养老区内存不足时,再次触发GC:major GC,进行养老区的内存清理
8.若养老区执行了major Gc之后发现依旧无法执行对象的保存,就会产生oom
每个对象分配一个年龄计数器,从伊甸园区到幸存者0区,年龄计数器会变为1
此时伊甸园区没有被gc的会被放在幸存者1区
幸存者0区的对象也会被判断是否被回收
如果没有被回收的话,则也会移动到幸存者1区,同时年龄计数器++
幸存者0区,和幸存者1区同时也被称为from、to,但这个from、to是不确定的
比如经过上图的gc过程后,幸存者0区就是to区,而幸存者1区就是from区
即每次执行完gc之后,哪个幸存者区是空的,哪个就是to区
to区即伊甸园区执行完gc之后,剩余对象往哪放
从幸存者区晋升到老年区才会用到年龄计数器
1.当新对象进行创建的时候,先会判断伊甸园区是否放得下,如果放的下的话就直接进行内存分配,如果放不下就会进行YGC
2.在判断伊甸园区是否放得下,如果伊甸园区放的下就直接分配对象内存,如果还是放不下,就判断老年区是否放的下,如果老年区放得下就直接再老年区分配对象内存,如果老年区也放不下先对老年区进行fgc类似于major gc ,之后再判断老年区是否放的下,放的下的话直接分配对象内存,放不下的话直接抛出oom
package com.cxf.heap; import java.util.ArrayList; import java.util.Random; /** * -Xms600m -Xmx600m */ public class HeapInstanceTest { byte[] buffer=new byte[new Random().nextInt(1024*1024)]; public static void main(String[] args) { ArrayList<HeapInstanceTest> buffers = new ArrayList(); while(true){ buffers.add(new HeapInstanceTest()); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
常用的调优工具
minor gc=ygc
针对老年代的gc即major gc
jvm在进行gc时,并非每次都对上边三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代
针对hotspot vm的实现,它里边的gc按照回收区域又分为两大种类型:一种是部分收集,一种是整堆收集
1.新生代收集(minor gc/young gc):只是新生代(eden\s0,s1)的垃圾收集
2.老年代收集(major gc/old gc):只是老年代的垃圾收集
目前只有,cms gc会有单独收集老年代的行为
注意,很多时候major gc会和full gc混淆使用,需要具体分辨是老年代回收还是整堆回收
3.混合收集mixed gc:收集整个新生代以及部分老年代的垃圾收集
目前只有g1 gc会有这种行为
整堆收集(FULL gc):收集整个java堆和方法区的垃圾收集
年轻代gc(minor gc)触发机制
老年代GC(major gc/full gc)触发机制
full gc触发机制
触发full gc执行的情况有如下五种:
说明:full gc是开发或调优中尽量要避免的,这样暂时时间会短一些
package com.cxf.GC; import java.util.ArrayList; /* *体会minor gc、major gc、full gc */ public class GcTest { public static void main(String[] args) { int i=0; try { ArrayList<String> list = new ArrayList<>(); String a="cxfszz"; while(true){ list.add(a); a=a+a; i++; } } catch (Exception e) { e.printStackTrace(); System.out.println("遍历次数为"+i); } } }
为什么需要把java堆分代?不分代就不能正常工作了嘛?
如果对象再eden出生并经过第一次minorgc 后任然存活,并且能被survivor容纳的话,将被移动到survivor空间中,并将对象年龄设为1.对象在survivor区每熬过一次minorgc,年龄就增加1岁,当他的年龄增加到一定程度,(默认为15岁,其实每个jvm、每个gc都有所不同)时,就会被晋升到老年代中。
对象晋升到老年代的年龄阈值,可以通过选项-XX:MaxTenuringThreshold来设置年龄的阈值
内存分配策略
优先分配对象到eden
大对象直接分配到老年代
尽量避免程序中出现过多的大对象
就如上边对象分配的特殊原则,会进行ygc在判断eden是否放得下,放不下再去判断老年代是否放得下,然后再在老年代中为对象分配内存,倘若老年代放不下的话,还要进行major gc
,进行gc的时候会触发stw,倘若放在老年代的这个对象朝生夕死的话就会很浪费
长期存活的对象分配到老年代
动态年龄判断
空间分配担保
-XX:HandlePromotionFailure
在eden进行完minor gc之后如果剩余的对象过多,无法全部放入survivor区剩余的会直接放入老年代
为什么有TLAB(thread local allocation buffer)?
什么是tlab?
tlab的再说明:
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但Jvm确实是将Tlab作为内存分配的首选
在程序中,开发人员通过选项“-XX:UseTLAB”设置是否需要开启Tlab空间,默认情况下该参数是开启状态
package com.cxf.GC; /** * 默认情况下-XX:UseTLAB 是开启的状态 */ public class TlabArgsTest { public static void main(String[] args) { System.out.println("打酱油"); try { Thread.sleep(10000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
package com.cxf.GC; /** * 此时堆空间常用的jvm参数: * -XX:+PrintFlagsInitial:查看所有的参数的默认初始值 * -XX:+PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值) * -Xms:初始堆空间内存 默认为物理内存的1/64 * -Xmx:最大堆空间内存(默认为物理内存的1/4) * -Xmn:设置新生代的大小(初始值及最大值) * -XX:NewRatio:配置新生代与老年代在堆结构中的占比 * -XX:SurvivorRatio:设置新生代中eden和s0/s1的空间占比 * -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄 * -XX:+PrintGCDetails:输出详细的GC处理日志 * 打印gc简要信息 * 1.-XX:+PrintGC 2.-verbose:gc * -XX:HandlePromotionFailure:是否设置空间担保 * */ public class HeapArgsTest { }
-XX:HandlePromotionFailure:是否设置空间担保
jdk7及以后该jvm参数有所变化及当老年代的最大可用的连续空间大于新生代的所有对象的总和或者大于历次晋升老年代的平均大小就进行minor gc否则则进行 full gc
在发生minor gc之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总和
根据前边的学习,大多数情况下新创建的数组和对象都会在堆中创建
堆是分配对象存储的唯一选择吗?
结论:开发中能使用局部变量的,就不要使用在方法外定义
使用逃逸分析,编译器可以对代码做如下优化:
一:栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
package com.cxf.GC; /** * -Xms1G -Xmx1G -XX:-DoEscapeAnalysis -XX:PrintGCDetails */ public class EscapeAnalysisTest { public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i <10000000 ; i++) { getUser(); } long end = System.currentTimeMillis(); System.out.println(end-start+"ms"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } private static void getUser(){ user user = new user(); } } class user{ }
package com.cxf.GC; /** * -Xms1G -Xmx1G -XX:+DoEscapeAnalysis -XX:PrintGCDetails */ public class EscapeAnalysisTest { public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i <10000000 ; i++) { getUser(); } long end = System.currentTimeMillis(); System.out.println(end-start+"ms"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } private static void getUser(){ user user = new user(); } } class user{ }
同样的代码当我们把堆内存的设置减少到256m时
不开启逃逸分析的运行结果如下,可以看到发生了gc
而开启了逃逸分析的相同代码,则没有发生gc
二:同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以考虑不同步
同样的同步省略并不是在编译器进行的,而是在运行 的时候进行的
通过观察反编译过后的字节码文件我们就能看出
三:分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分或者全部可以不存储在内存,而是存储在cpu寄存器中
标量(scalar)是指一个无法在分解成更小的数据的数据。java中的原始数据类型就是标量(比如基本数据类型),相对的那些还可以分解的数据叫做聚合量(Aggregate),java中的对象就是聚合量,因为他可以分解为其他聚合量和标量
在jit阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过jit优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换
代码示例
package com.cxf.GC; /** * -Xms60m -Xmx60m -XX:-EliminateAllocations -XX:+DoEscapeAnalysis -XX:+PrintGCDetails */ public class ScalarReplaceTest { public static class user{ public int id; public String name; } public static void allow(){ user user = new user();//未发生逃逸 user.id=18; user.name="cxf"; } public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i <1000000 ; i++) { allow(); } long end = System.currentTimeMillis(); System.out.println(end-start+"ms"); } }
package com.cxf.GC; /** * -Xms60m -Xmx60m -XX:+EliminateAllocations -XX:+DoEscapeAnalysis -XX:+PrintGCDetails */ public class ScalarReplaceTest { public static class user{ public int id; public String name; } public static void allow(){ user user = new user();//未发生逃逸 user.id=18; user.name="cxf"; } public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i <1000000 ; i++) { allow(); } long end = System.currentTimeMillis(); System.out.println(end-start+"ms"); } }