java虚拟机运行时数据区分布图:java
其中,堆(Heap)和JVM栈是程序运行的关键,由于:程序员
那为何要把堆和栈区分出来呢?栈中不是也能够存储数据吗?算法
Java堆是java虚拟机所管理内存中最大的一块内存空间,处于物理上不连续的内存空间,只要逻辑连续便可,主要用于存放各类类的实例对象。该区域被全部线程共享,在虚拟机启动时建立,用来存放对象的实例,几乎全部的对象以及数组都在这里分配内存(栈上分配、标量替换优化技术的例外)。
在 Java 中,堆被划分红两个不一样的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor(S0)、To Survivor(S1)。如图所示:数组
堆的内存布局: 缓存
这样划分的目的是为了使jvm可以更好的管理内存中的对象,包括内存的分配以及回收。 而新生代按eden和两个survivor的分法,是为了安全
参数 | 描述 |
---|---|
-Xms | 堆内存初始大小,单位m、g |
-Xmx | 堆内存最大容许大小,通常不要大于物理内存的80% |
-Xmn | 年轻代内存初始大小 |
-Xss | 每一个线程的堆栈大小,即JVM栈的大小 |
-XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值 |
-XX:NewSzie(-Xns) | 年轻代内存初始大小,能够缩写-Xns |
-XX:MaxNewSize(-Xmx) | 年轻代内存最大容许大小,能够缩写-Xmx |
-XX:SurvivorRatio | 年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1 |
-XX:MinHeapFreeRatio | GC后,若是发现空闲堆内存占到整个预估堆内存的40%,则放大堆内存的预估最大值,但不超过固定最大值。 |
-XX:MaxHeapFreeRatio | 预估堆内存是堆大小动态调控的重要选项之一。堆内存预估最大值必定小于或等于固定最大值(-Xmx指定的数值)。前者会根据使用状况动态调大或缩小,以提升GC回收的效率,默认70% |
-XX:MaxTenuringThreshold | 垃圾最大年龄,设置为0的话,则年轻代对象不通过Survivor区,直接进入年老代。对于年老代比较多的应用,能够提升效率.若是将此值设置为一个较大值,则年轻代对象会在Survivor区进行屡次复制,这样能够增长对象再年轻代的存活 时间,增长在年轻代即被回收的几率 |
-XX:InitialTenuringThreshold | 能够设定老年代阀值的初始值 |
-XX:+PrintTenuringDistribution | 查看每次minor GC后新的存活周期的阈值 |
Note: 每次GC 后会调整堆的大小,为了防止动态调整带来的性能损耗,通常设置-Xms、-Xmx 相等。
新生代的三个设置参数:-Xmn,-XX:NewSize,-XX:NewRatio的优先级:
(1).最高优先级: -XX:NewSize=1024m和-XX:MaxNewSize=1024m
(2).次高优先级: -Xmn1024m (默认等效效果是:-XX:NewSize==-XX:MaxNewSize==1024m)
(3).最低优先级:-XX:NewRatio=2
推荐使用的是-Xmn参数,缘由是这个参数很简洁,至关于一次性设定NewSize和MaxNewSIze,并且二者相等。性能优化
方式 | 实质 |
---|---|
使用new关键 | 调用无参或有参构造器函数建立 |
使用Class的newInstance方法 | 调用无参或有参构造器函数建立,且须要是publi的构造函数 |
使用Constructor类的newInstance方法 | 调用有参和私有private构造器函数建立,实用性更广 |
使用Clone方法 | 不调用任何参构造器函数,且对象须要实现Cloneable接口并实现其定义的clone方法,且默认为浅复制 |
第三方库Objenesis | 利用了asm字节码技术,动态生成Constructor对象 |
在虚拟机层面上建立对象的步骤:数据结构
步骤 | 解析 |
---|---|
一、判断对象对应的类是否加载、连接、初始化 | 虚拟机遇到一条new指令,首先去检查这个指令的参数可否在常量池中定位到一个类的符号引用,而且检查这个符号引用表明的类是否已经被加载、解析和初始化。若是没有,那么必须先执行类的加载、解释、初始化(类的clinit方法)。 |
二、为对象分配内存 | 类加载检查经过后,虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后即可以彻底肯定,为对象分配空间无非就是从Java堆中划分出一块肯定大小的内存而已。 |
三、处理并发安全问题 | 另一个问题及时保证new对象时候的线程安全性:建立对象是很是频繁的操做,虚拟机须要解决并发问题。 虚拟机采用了两种方式解决并发问题: (1)CAS配上失败重试的方式保证指针更新操做的原子性; (2)TLAB 把内存分配的动做按照线程划分在不一样的空间之中进行,即每一个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区,(TLAB ,Thread Local Allocation Buffer)虚拟机是否使用TLAB,能够经过-XX:+/-UseTLAB参数来设定。 |
四、初始化分配到的空间 | 内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中能够不用赋初始值就能够直接使用,程序能访问到这些字段的数据类型所对应的零值 |
五、设置对象的对象头 | 将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC分代年龄等数据存储在对象的对象头中 |
六、执行init方法进行初始化 | 在Java程序的视角看来,初始化才正式开始,开始调用方法完成初始赋值和构造函数,全部的字段都为零值。所以通常来讲(由字节码中是否跟随有invokespecial指令所决定),new指令以后会接着就是执 行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算彻底建立出来。 |
分配对象内存,有两种分配方式,指针碰撞和空闲列表:
(1)若是内存是规整的,那么虚拟机将采用的是指针碰撞法(Bump The Pointer)来为对象分配内存。意思是全部用过的内存在一边,空闲的内存在另一边,中间放着一个指针做为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。 若是垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。 通常使用带有compact(整理)过程的收集器时,使用指针碰撞。
(2)若是内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为“空闲列表(Free List)”。并发
Note: 选择哪一种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。app
那什么样的对象可以进入老年代(Old)?
状况 | 解析 |
---|---|
1.对象优先在Eden分配 | 大多数状况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC;虚拟机提供了-XX:PrintGCDetails参数,发生垃圾回收时打印内存回收日志,而且在进程退出时输出当前内存各区域的分配状况。 |
2.大对象直接进入老年代 | 所谓的大对象就是指,须要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串及数组。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值得对象直接在老年代中分配(这样作的目的是避免在Eden区及两个Survivor之间发生大量的内存拷贝) |
3.长期存活的对象将直接进入老年代 | 对象年龄计数器。-XX:MaxTenuringThreshold |
四、动态对象年龄断定 | 虚拟机并不老是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,若是在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。 |
五、空间分配担保 | 在发生Minor GC时(前),虚拟机会检测以前每次晋升到老年代的平均大小(由于当次会有多少对象会存活是没法肯定的,因此取以前的平均值/经验值)是否大于老年代的剩余空间大小,若是大于,则改成直接进行一次Full GC。若是小于,则查看HandlePromotionFailure设置是否容许担保失败;若是容许,那只会进行Minor GC;若是不容许,则也要改成进行一次Full GC。取平均值进行比较其实仍然是一种动态几率手段,也就是说若是某次Minor GC存活后的对象突增,远远高于平均值的话,依然会致使担保失败(Handle Promotion Failure),这样会触发Full GC。 |
被誉为现代垃圾回收算法的思想基础。
该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。创建在存活对象少,垃圾对象多的前提下。此算法每次只处理正在使用中的对象,所以复制成本比较小,同时复制过去后还能进行相应的内存整理,不会出现碎片问题。但缺点也是很明显,就是须要两倍内存空间。
它开始时把堆分红 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集中扫描活动对象,并将每一个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分红对象面和空闲区域面,在对象面与空闲区域面的切换过程当中,程序暂停执行。
此算法是结合了“标记-清除”和“复制算法”两个算法的优势。避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
基于这样的事实:不一样的对象的生命周期是不同的。所以,不一样生命周期的对象能够采起不一样的回收算法,以便提升回收效率。
新生代因为其对象存活时间短,且须要常常gc,所以采用效率较高的复制算法,其将内存区分为一个eden区和两个suvivor区,默认eden区和survivor区的比例是8:1,分配内存时先分配eden区,当eden区满时,使用复制算法进行gc,将存活对象复制到一个survivor区,当一个survivor区满时,将其存活对象复制到另外一个区中,当对象存活时间大于某一阈值时,将其放入老年代。老年代和永久代由于其存活对象时间长,所以使用标记清除或标记整理算法
总结:
总结:
Java 中的堆(deap) 也是 GC 收集垃圾的主要区域。 因为对象进行了分代处理,所以垃圾回收区域、时间也不同。GC有两种类型:Scavenge GC(Minor GC)和Full GC(Major GC)。
基于大多数新生对象都会在GC中被收回的假设。新生代的GC 使用复制算法,(将年轻代分为3部分,主要是为了生命周期短的对象尽可能留在年轻代。老年代主要存放生命周期比较长的对象,好比缓存)。可能经历过程:
GC日志相关参数:
案例分析:-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime一块儿使用
Application time: 0.3440086 seconds
Total time for which application threads were stopped: 0.0620105 seconds
Application time: 0.2100691 seconds
Total time for which application threads were stopped: 0.0890223 seconds
复制代码
得知应用程序在前344毫秒中是在处理实际工做的,而后将全部线程暂停了62毫秒,紧接着又工做了210ms,而后又暂停了89ms。
2796146K->2049K(1784832K)] 4171400K->2049K(3171840K), [Metaspace: 3134K->3134K(1056768K)], 0.0571841 secs] [Times: user=0.02 sys=0.04, real=0.06 secs]
Total time for which application threads were stopped: 0.0572646 seconds, Stopping threads took: 0.0000088 seconds
复制代码
应用线程被强制暂停了57ms来进行垃圾回收。其中又有8ms是用来等待全部的应用线程都到达安全点。
只要设置-XX:+PrintGCDetails 就会自动带上-verbose:gc和-XX:+PrintGC
33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
复制代码
从代码上:
从JVM参数上调优上:
最后可关注公众号,一块儿学习。加群,天天会分享干货,还有学习视频领取!