JVM运行数据区之堆空间
一,堆的核心概述
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域
- Java堆区在JVM启动的时候创建,其空间大小也就确定了,是JVM管理最大的一块内存空间,堆内存的大小是可以调节的
- 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
- 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread LocalAllocationBuffer,TLAB)
堆空间大小的设置:
可以通过选项:-X:是jvm的运行参数
-Xms10m 堆区起始内存
-Xmx10m 堆区的最大内存(年轻代+老年代)
一旦堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OutOfMemoryError异常
通常会将-Xmx和-Xms两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认:初始内存大小:物理电脑内存大小/64
最大内存大小:物理电脑内存大小/4
返回堆内存的代码:
long initialMemory=Runtime.getRuntime().totalMemory();//堆内存总量
long maxlMemory=Runtime.getRuntime().maxMemory();//最大内存
查看设置的参数:
一:jps (显示处=出进程号)
jstat -gc 进程id
二:加参数 -XX:+PrintGCDetails (+表示起作用)
二,堆空间的细分:
Java7及以前堆内存逻辑上分为三部分:新生区+养老区+永久区
Java8及以后,堆内存在逻辑上分为三部分:新生区+养老区+元空间
年轻代:Java对象的生命周期较短,这类对象的创建和消亡都非常迅速
还可以分为Eden空间,Survivor0空间和Survivor1空间
年老代:对象的声明周期非常长,在某些极端情况下可能与JVM的生命周期保持一致
在堆区中的占比:
默认:-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以通过-XX:NewRatio=4。表示新生代占1,老年代占4,新生代占整个堆的1/5
在HotSpot中,Eden空间和S0,S1所占的比例是8:1:1
--可以通过选项-XX:SurvivorRatio=8这个来调整(默认是6:2:2,调整后可以为8:1:1)
对象分配过程
解析:
-
new的对象先放在伊甸园区,此区有大小限制
-
当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园去进行垃圾回收(Minor GC),将伊甸园中不再被其他对象所引用的对象进行销毁,再加载新的对象放在伊甸园。
-
然后将伊甸园中剩余对象移动到幸存者0区
-
如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区
-
如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区
-
这样一直循环,每移动依次,标识年龄的字段就加一,当到达15时就会进入到养老区,这个参数可以设置 -XX:MaxTenuringThreshold=< N>进行设置
-
在养老区,不会轻易就被清除,当养老区内存不足时,再次触发GC(Major GC)进行养老区的内存清理
-
若养老区执行了Major GC之后依然无法进行对象的保存,就会产生OOM异常
java.lang.OutOfMemoryError:Java heap space
总结:
针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
关于垃圾回收:频繁在新生区收集,很少在养老区,几乎不再元空间
GC介绍
Minor GC:新生代的垃圾收集
Major GC:老年代的垃圾收集
Mixed GC:混合收集,收集整个新生代以及部分老年代的垃圾收集
Full GC:整堆收集,收集整个Java堆和方法区的收集
触发机制:
-
调用System.gc(),系统建议执行Full GC,但不必然执行
-
老年代空间不足
-
方法区空间不足
-
通过Minor GC进入老年代的平均大小大于老年代的可用内存
-
由Eden区,s0向s1复制时,对象大小大于To Space可用内存,则把改对象转到老年代,且老年代的可用内存小于该对象的大小
full gc是开发或调优中尽量要避免的
Major GC出现经常会伴随至少一次的Minor GC,即老年代空间不足时,会尝试先触发Minor GC,如果之后空间还不足,则触发Major GC
堆分代的思想
优化GC性能,减少检索垃圾的时间
内存分配策略:
- 优先分配到Eden
- 大对象直接分配到老年代
- 长期存活的对象分配到老年代
- 动态对像年龄判断:如果幸存者区中相同年龄的所有对象大小的总和大于幸存者区的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到设置的值
- 空间分配担保:-XX:HandlePromotionFailure
对象分配过程TLAB
一,TLAB出现的原因:
堆区是线程共享区域,所以在并发环境下划分内存空间是线程不安全的,为避免多个线程操作同一地址,需要加锁等机制,但会影响分配速度
二,TLAB介绍(快速分配策略):
从内存模型而不是垃圾收集角度,对Eden继续进行划分,JVM为每个线程分配了一个私有缓存区域,包含在Eden空间内
避免了非线程安全问题,提升内存分配的吞吐量
在程序中,可以通过选项-XX:UserTlA 设置是否开启TLAB空间
一旦对象在TLAB空间分配内存失败,JVM机会尝试通过使用加锁机制确保数据操作的原子性,直接在Eden空间中分配内存。
JVM设置参数汇总:
在发生Minor GC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
如果大于,则此次Mimor GC是安全的
如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败
如果HandlePromotionFailure=true,那么会继续老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
如果大于,则尝试一次Minor GC,但这次MinorGC是有风险的
如果小于,则改为进行一次Full GC
如果HandlePromotionFailure=false,则改为进行一次Full GC。
在jdk7之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,规则变为:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
堆是分配对象存储的唯一选择吗?
如果经过逃逸分析后发现,一个对象并没有逃逸出去的话,那么就可能被优化成栈上分配,这样就无需在堆上分配内存,也无需进行垃圾回收了,这就是常见的堆外存储技术,不过这个技术还不够成熟适用率不够广,但是TaobaoVM采用了这种技术
逃逸分析概述:、在jdk1.6后才有的,并不是十分成熟,Oracle Hotspot的对象都创建在堆上,没有采用逃逸分析,了解即可
逃逸分析的基本行为就是分析对象动态作用域:
(1)当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
(2)当一个对象在方法被定义后,他被外部方法所引用,则认为发生逃逸,例如作为调用参数传递到其他地方中
使用逃逸分析,编译器可以对代码做如下优化:
一,栈上分配:将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使用指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
--栈上分配,分配完后继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量也被回收,无须进行垃圾回收了。场景:给成员变量赋值,方法返回值,实例引用传递
二,同步省略(锁消除):如果一个对象被发现只能从一个线程被访问到,那么这个对象的操作可以不同考虑同步
三,分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在cpu寄存器中。大大减少堆内存的占用,因为一旦不需要创建对象了,那么就不再需要分配堆内存了。
--标量:一个无法再分解成更小的数据的数据,例,Java中的原始数据类型;聚合量:还可以分解的数据,Java中的对象。在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问到的话,那么经过JIT优化,就会把这个对象拆解成若干个其他包含的若干个成员变量来替代,这个过程就是标量替换
总结:开发中能使用局部变量的,就不要使用在方法外定义
使用:-server -Xmx100m -Xms100m -XX:DoEscapeAnlysis -XX:+PrintGC -XX:EliminateAllocations
参数-server:启动server模式下,才可以启用逃逸分析
参数 -XX:DoEscapeAnlysis:启用逃逸分析
参数 -XX:EliminateAllocations :开启了标量替换,允许将对象打散在栈上,比如:对象拥有id,name两个字段,那么这两个字段将会被视为两个独立的局部变量进行分配。
JVM之运行之数据区