1.1 java虚拟机运行时数据区
java虚拟机运行时数据区分布图:
其中,堆(Heap)和JVM栈是程序运行的关键,因为:
那为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
1.2 堆(Heap)和JVM栈:
1.2.1 堆(Heap)
Java堆是java虚拟机所管理内存中最大的一块内存空间,处于物理上不连续的内存空间,只要逻辑连续即可,主要用于存放各种类的实例对象。该区域被所有线程共享,在虚拟机启动时创建,用来存放对象的实例,几乎所有的对象以及数组都在这里分配内存(栈上分配、标量替换优化技术的例外)。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor(S0)、To Survivor(S1)。如图所示:
堆的内存布局:
这样划分的目的是为了使jvm能够更好的管理内存中的对象,包括内存的分配以及回收。而新生代按eden和两个survivor的分法,是为了
1.2.2 堆栈相关的参数
参数 | 描述 |
---|---|
-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,而且两者相等。
1.3 jvm对象
1.3.1 创建对象的方式
各个方式的实质操作如下:
方式 | 实质 |
---|---|
使用new关键 | 调用无参或有参构造器函数创建 |
使用Class的newInstance方法 | 调用无参或有参构造器函数创建,且需要是publi的构造函数 |
使用Constructor类的newInstance方法 | 调用有参和私有private构造器函数创建,实用性更广 |
使用Clone方法 | 不调用任何参构造器函数,且对象需要实现Cloneable接口并实现其定义的clone方法,且默认为浅复制 |
第三方库Objenesis | 利用了asm字节码技术,动态生成Constructor对象 |
在虚拟机层面上创建对象的步骤:
步骤 | 解析 |
---|---|
1、判断对象对应的类是否加载、链接、初始化 | 虚拟机遇到一条new指令,首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有,那么必须先执行类的加载、解释、初始化(类的clinit方法)。 |
2、为对象分配内存 | 类加载检查通过后,虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后便可以完全确定,为对象分配空间无非就是从Java堆中划分出一块确定大小的内存而已。 |
3、处理并发安全问题 | 另外一个问题及时保证new对象时候的线程安全性:创建对象是非常频繁的操作,虚拟机需要解决并发问题。 虚拟机采用了两种方式解决并发问题:(1)CAS配上失败重试的方式保证指针更新操作的原子性;(2)TLAB 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区,(TLAB ,Thread Local Allocation Buffer)虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。 |
4、初始化分配到的空间 | 内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值 |
5、设置对象的对象头 | 将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC分代年龄等数据存储在对象的对象头中 |
6、执行init方法进行初始化 | 在Java程序的视角看来,初始化才正式开始,开始调用方法完成初始赋值和构造函数,所有的字段都为零值。因此一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之后会接着就是执 行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。 |
1.3.3 对象分配内存方式
分配对象内存,有两种分配方式,指针碰撞和空闲列表:
(1)如果内存是规整的,那么虚拟机将采用的是指针碰撞法(Bump The Pointer)来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带有compact(整理)过程的收集器时,使用指针碰撞。
(2)如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为“空闲列表(Free List)”。
Note: 选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
1.3.4 那什么样的对象能够进入老年代(Old)
那什么样的对象能够进入老年代(Old)?
被誉为现代垃圾回收算法的思想基础。
标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
2.4 复制算法(Copying)
该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。建立在存活对象少,垃圾对象多的前提下。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去后还能进行相应的内存整理,不会出现碎片问题。但缺点也是很明显,就是需要两倍内存空间。
它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。
2.5 标记-整理(或标记-压缩算法,Mark-Compact,又或者叫标记清除压缩MarkSweepCompact)
此算法是结合了“标记-清除”和“复制算法”两个算法的优点。避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。
2.6 分代回收策略(Generational Collecting)
基于这样的事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
新生代由于其对象存活时间短,且需要经常gc,因此采用效率较高的复制算法,其将内存区分为一个eden区和两个suvivor区,默认eden区和survivor区的比例是8:1,分配内存时先分配eden区,当eden区满时,使用复制算法进行gc,将存活对象复制到一个survivor区,当一个survivor区满时,将其存活对象复制到另一个区中,当对象存活时间大于某一阈值时,将其放入老年代。老年代和永久代因为其存活对象时间长,因此使用标记清除或标记整理算法
总结:
2.7 垃圾回收器
总结:
Java 中的堆(deap) 也是 GC 收集垃圾的主要区域。由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC(Minor GC)和Full GC(Major GC)。
3.1 触发Full GC执行的场景
3.3 新生对象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]
3.5 减少GC开销的措施
从代码上:
从JVM参数上调优上:
3.6 内存溢出分类
4.1 调优目的
4.2 JVM性能调优所处的层次
4.3 JVM调优流程
4.4 性能监控工具
调优的最终目的都是为了令应用程序使用最小的硬件消耗来承载更大的吞吐。jvm的调优也不例外,jvm调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量。
作者:Ccww 来源:https://juejin.im/post/5d200b54f265da1bac40384a