那为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
1、从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据,分工明确,处理逻辑更为清晰体现了“分而治之”以及“隔离”的思想。
2、堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这样共享的方式有很多收益:提供了一种有效的数据交互方式(如:共享内存);堆中的共享常量和缓存可以被所有栈访问,节省了空间。
3、栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
4、堆和栈的结合完美体现了面向对象的设计。当我们将对象拆开,你会发现,对象的属性即是数据,存放在堆中;而对象的行为(方法)即是运行逻辑,放在栈中。因此编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。
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后新的存活周期的阈值 |
每次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对象 |
步骤 | 解析 |
---|---|
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)如果内存是规整的,那么虚拟机将采用的是指针碰撞法(Bump The Pointer)来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。
如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。
一般使用带有compact(整理)过程的收集器时,使用指针碰撞。
(2)如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为“空闲列表(Free List)”。
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
情况 | 解析 |
---|---|
1.对象优先在Eden分配 | 大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC;虚拟机提供了-XX:PrintGCDetails参数,发生垃圾回收时打印内存回收日志,并且在进程退出时输出当前内存各区域的分配情况。 |
2.大对象直接进入老年代 | 所谓的大对象就是指,需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串及数组。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值得对象直接在老年代中分配(这样做的目的是避免在Eden区及两个Survivor之间发生大量的内存拷贝) |
3.长期存活的对象将直接进入老年代 | 对象年龄计数器。-XX:MaxTenuringThreshold |
4、动态对象年龄判定 | 虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等MaxTenuringThreshold中要求的年龄。 |
5、空间分配担保 | 在发生Minor GC时(前),虚拟机会检测之前每次晋升到老年代的平均大小(因为当次会有多少对象会存活是无法确定的,所以取之前的平均值/经验值)是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。取平均值进行比较其实仍然是一种动态概率手段,也就是说如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure),这样会触发Full GC。 |
标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。建立在存活对象少,垃圾对象多的前提下。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去后还能进行相应的内存整理,不会出现碎片问题。但缺点也是很明显,就是需要两倍内存空间。
它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。
此算法是结合了“标记-清除”和“复制算法”两个算法的优点。避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。
基于这样的事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
新生代由于其对象存活时间短,且需要经常gc,因此采用效率较高的复制算法,其将内存区分为一个eden区和两个suvivor区,默认eden区和survivor区的比例是8:1,分配内存时先分配eden区,当eden区满时,使用复制算法进行gc,将存活对象复制到一个survivor区,当一个survivor区满时,将其存活对象复制到另一个区中,当对象存活时间大于某一阈值时,将其放入老年代。老年代和永久代因为其存活对象时间长,因此使用标记清除或标记整理算法
总结:
新生代:复制算法(新生代回收的频率很高,每次回收的耗时很短,为了支持高频率的新生代回收,虚拟机可能使用一种叫做卡表(Card Table)的数据结构,卡表为一个比特位集合,每个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对
垃圾回收器的任务是识别和回收垃圾对象进行内存清理,不同代可使用不同的收集器:
总结:
1、Serial old和新生代的所有回收器都能搭配;也可以作为CMS回收器的备用回收器;
2、CMS只能和新生代的Serial和ParNew搭配,而且ParNew是CMS默认的新生代回收器;
3、并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
4、并发(Concurrent):指用户线程和垃圾收集线程同时执行(但不一定是并行的,可能是交替执行),用户程序继续运行,而垃圾收集程序运行在另外的CPU上。
Java 中的堆(deap) 也是 GC 收集垃圾的主要区域。
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC(Minor GC)和Full GC(Major GC)。
基于大多数新生对象都会在GC中被收回的假设。新生代的GC 使用复制算法,(将年轻代分为3部分,主要是为了生命周期短的对象尽量留在年轻代。老年代主要存放生命周期比较长的对象,比如缓存)。可能经历过程:
GC日志相关参数:
-XX:+PrintGC:输出GC日志
-XX:+PrintGCDetails:输出GC的详细日志
-XX:+PrintGCTimeStamps:输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间
-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间
-XX:+PrintHeapAtGC:在进行GC的前后打印出堆的信息
-XX:+PrintTLAB:查看TLAB空间的使用情况
-XX:PrintTenuingDistribution:查看每次minor GC后新的存活周期的阈值
-XX:PrintReferenceFC:用来跟踪系统内的(softReference)软引用,(weadReference)弱引用,(phantomReference)虚引用,显示引用过程
案例分析:-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的调优也不例外,jvm调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量。
[文章转载:https://juejin.cn/post/6844903892774289421]