zoukankan      html  css  js  c++  java
  • java内存机制

    Java内存主要有堆内存、栈内存和寄存器。栈是运行时的单位,堆是存储时的单位。

    堆内存主要是存储对象、类的静态方法、静态变量、成员变量。

    栈主要存储方法、局部变量、java基本类型、堆中对象实例的引用。

    一个对象的大小是不可估计的,但是在栈中,一个对象只对应了4kb的引用。

    基本类型:byte、short、int、long、char、float、double、Boolean

    引用类型:类、接口、数组

    Java中的参数传递时是传值还是传引用?

    程序允许是在栈中运行,所以在参数传递的时候,只传递基本类型和对象引用,不会直接传递对象本身。在运行栈中,基本类型和引用类型都是一样的,都是传值。所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到堆中的对象,这个时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是堆中的数据。所以这个修改是可以保持的了。

    对象,从某种意义上说,是由基本类型组成的。

    对象的大小?

    一个空的对象,在栈中存储基本的引用4kb,一个在堆中的空的object对象的大小是8kb,因此一个对象最少是12kb,但是空间占用一般是8的倍数,所以应该是16kb。

    引用类型?

    一般分为强引用,软引用,弱引用。

    强引用一般是我们经常用到的对象类型。就是我们一般声明对象是时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,则不会被垃圾回收

    软引用:软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。

    弱引用:弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。

    寄存器主要存储???

     这是速度最快的存储场所,因为寄存器位于处理器内部,这一点和其他的存储媒介都不一样。不过寄存器个数是有限的。在内存中的寄存器区域是由编译器根据需要来分配的。我们程序开发人员不能够通过代码来控制这个寄存器的分配。所以说,这第一个存储区域寄存器,我们只能够看看,而不能够对其产生任何的影响。,也没办法在程序里头感觉到寄存器的任何存在迹象。

    存储主要为运算中的数据,小的临时数据。如:for循环的 循环控制变量 放在寄存器

    垃圾回收算法种类:

    1.按基本的回收策略分

    引用计数:当对象有一个引用时,增加一个计数,删除一个引用时减少一个技术。垃圾回收器进行垃圾回收的时候,只回收计数为0的即可。此算法缺点是无法处理循环引用的情况。

    标记-清楚:此算法分2个阶段,第一个阶段是从根节点标记所用有引用的对象。第二阶段是把未标记的对象清楚。此算法确定是需要暂停整个应用,对产生内存碎片。

    复制:此算法是需要2个大小的内存区域,每次只使用其中的一个区域。垃圾回收的时候,把其中的一个区域中的对象复制到另外一个区域中,每次只复制正在使用中的对象,复制成本小。同时可以整理碎片。缺点是需要2个内存。

    标记-整理:此算法也是分2个阶段,首先从根节点开始标记所有正在使用的对象,第二阶段是清楚为标记的,并把标记的对象压缩到堆中的一块,并按顺序存放。此算法避免了“标记-清楚”中产生的碎片问题,同时有避免了“复制”算法中的空间问题。

    2.按分区对待

    分代收集:基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。

    为什么要分代?

    分代的垃圾回收策略,是基于这样的一个实时:不同的对象的生命周期是不一样的,因此不同的生命周期要采取不同的收集方式,以便提高回收效率。

    在java程序运行中,会产生大量的对象,其中对象和业务息息相关,对于有些对象,如http请求中的session对象、socket连接等,这些对象和业务联系比较紧密,因此这些对象存在的时间会比较的长;但是还有一些对象,存在的时间会比价短,如String对象。对于这些对象,垃圾回收器要及时的回收这些对象,避免浪费内存。而对于存在比价长的对象,每次的遍历都不能进行回收掉。所以对这两种对象要分不同的方式进行垃圾回收。因为每次不能回收掉的对象进行每次遍历的时候也是浪费内存的。所以采用分而治之的方式,把对象进行分代,不同代上采用不同的垃圾回收策略进行回收,提高效率。

    如何分代?

    对垃圾回收器的分代,一般分为三种,年轻的、年老代和持久代。其中持久代主要存储的是java类的类信息,与垃圾回收器收集的java对象关系不大,年轻代和年老代的划分是对垃圾回收影响比较大的。

    年轻代:java中所有新建立的对象首先都是放在年轻代中的。年轻代的目标就是尽可能的回收掉那些生命周期比较短的对象。年轻代划分为三个区;一个Eden区,两个survivor区。

    大部分的对象是存储在Eden区,当Eden区满的时候,还存活的对象会被复制到一个survivor区,当这个survivor区也满的时候,此时存活的对象会被复制到另外一个survivor区中,最终这个慢的时候,从第一个survivor区中复制过来的对象还存活时,会把这个对象复制到年老区中去。

    年老代:在年轻代中经历了多次垃圾回收后仍然存活的对象,会被放到年老代中。年老代中都是存在生命周期比较长的对象。

    什么时候垃圾回收?

    由于对象进行了分代,因此垃圾回收区域,时间也不一样,GC有两种类型,Scavenge GC和Full GC。

    Scavenge gc:一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

    Full GC: 对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

    · 年老代(Tenured)被写满

    · 持久代(Perm)被写满

    · System.gc()被显示调用

    ·上一次GC之后Heap的各域分配策略动态变化

    对象处于不可触及状态的时候,垃圾回收器会对其进行垃圾回收。不可触及状态也就是该对象没有被栈引用。当对象处于不可触及状态的时候,也不是立即进行垃圾回收,因为垃圾回收是不可管控的,什么时候垃圾回收是有jvm垃圾回收器决定的,但是可以提醒垃圾回收器,即调用System.exit()或CurrentThread.exit()方法。

    栈一般是不占用内存的,栈中的方法只有在调用的时候才占用内存。

    垃圾回收器算法选择:

    JVM给了三种选择:串行收集器、并行收集器、并发收集器

    (1)串行收集器

    适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

    (2)并行收集器——吞吐量优先

    如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。

    典型配置:

    java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20

    -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。

    -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

    java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC

    -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。

    java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100

    -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

    java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

    -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

    (3)并发收集器——响应时间优先

    如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。

    典型配置:

    java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

    -XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。

    -XX:+UseParNewGC: 设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

    java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

    -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。

    -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

    调优总结

    年轻代大小选择

    响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

    吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

    年老代大小选择

    响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

      1. 并发垃圾收集信息

      2. 持久代并发收集次数

      3. 传统GC信息

      4. 花在年轻代和年老代回收上的时间比例

    减少年轻代和年老代花费的时间,一般会提高应用的效率

    吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

    较小堆引起的碎片问题

    因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

    1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。

    2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

    垃圾回收的瓶颈

    传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限。但是他无法解决的一个问题,就是Full GC所带来的应用暂停。在一些对实时性要求很高的应用场景下,GC暂停所带来的请求堆积和请求失败是无法接受的。这类应用可能要求请求的返回时间在几百甚至几十毫秒以内,如果分代垃圾回收方式要达到这个指标,只能把最大堆的设置限制在一个相对较小范围内,但是这样有限制了应用本身的处理能力,同样也是不可接收的。

        分代垃圾回收方式确实也考虑了实时性要求而提供了并发回收器,支持最大暂停时间的设置,但是受限于分代垃圾回收的内存划分模型,其效果也不是很理想。

        为了达到实时性的要求(其实Java语言最初的设计也是在嵌入式系统上的),一种新垃圾回收方式呼之欲出,它既支持短的暂停时间,又支持大的内存空间分配。可以很好的解决传统分代方式带来的问题。

    增量收集的演进

        增量收集的方式在理论上可以解决传统分代方式带来的问题。增量收集把对堆空间划分成一系列内存块,使用时,先使用其中一部分(不会全部用完),垃圾收集时把之前用掉的部分中的存活对象再放到后面没有用的空间中,这样可以实现一直边使用边收集的效果,避免了传统分代方式整个使用完了再暂停的回收的情况。

        当然,传统分代收集方式也提供了并发收集,但是他有一个很致命的地方,就是把整个堆做为一个内存块,这样一方面会造成碎片(无法压缩),另一方面他的每次收集都是对整个堆的收集,无法进行选择,在暂停时间的控制上还是很弱。而增量方式,通过内存空间的分块,恰恰可以解决上面问题。

    Garbage Firest(G1)

    这部分的内容主要参考这里,这篇文章算是对G1算法论文的解读。我也没加什么东西了。

    目标:从设计目标看G1完全是为了大型应用而准备的。

    支持很大的堆

    高吞吐量

      --支持多CPU和垃圾回收线程

      --在主线程暂停的情况下,使用并行收集

      --在主线程运行的情况下,使用并发收集

    实时目标:可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收

    当然G1要达到实时性的要求,相对传统的分代回收算法,在性能上会有一些损失。

    G1 算法详解

     

    G1可谓博采众家之长,力求到达一种完美。他吸取了增量收集优点,把整个堆划分为一个一个等大小的区域(region)。内存的回收和划分都以region为单位;同时,他也吸取了CMS的特点,把这个垃圾回收过程分为几个阶段,分散一个垃圾回收过程;而且,G1也认同分代垃圾回收的思想,认为不同对象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。为了达到对回收时间的可预计性,G1在扫描了region以后,对其中的活跃对象的大小进行排序,首先会收集那些活跃对象小的region,以便快速回收空间(要复制的活跃对象少了),因为活跃对象小,里面可以认为多数都是垃圾,所以这种方式被称为Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。

    回收步骤:

    (1)初始标记(Initial Marking)

    G1对于每个region都保存了两个标识用的bitmap,一个为previous marking bitmap,一个为next marking bitmap,bitmap中包含了一个bit的地址信息来指向对象的起始点。开始Initial Marking之前,首先并发的清空next marking bitmap,然后停止所有应用线程,并扫描标识出每个region中root可直接访问到的对象,将region中top的值放入next top at mark start(TAMS)中,之后恢复所有应用线程。触发这个步骤执行的条件为:

        G1定义了一个JVM Heap大小的百分比的阀值,称为h,另外还有一个H,H的值为(1-h)*Heap Size,目前这个h的值是固定的,后续G1也许会将其改为动态的,根据jvm的运行情况来动态的调整,在分代方式下,G1还定义了一个u以及soft limit,soft limit的值为H-u*Heap Size,当Heap中使用的内存超过了soft limit值时,就会在一次clean up执行完毕后在应用允许的GC暂停时间范围内尽快的执行此步骤;

        在pure方式下,G1将marking与clean up组成一个环,以便clean up能充分的使用marking的信息,当clean up开始回收时,首先回收能够带来最多内存空间的regions,当经过多次的clean up,回收到没多少空间的regions时,G1重新初始化一个新的marking与clean up构成的环。

    (2)并发标记(Concurrent Marking)

    按照之前Initial Marking扫描到的对象进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期间应用线程并发修改的对象的以来关系则记录到remembered set logs中,新创建的对象则放入比top值更高的地址区间中,这些新创建的对象默认状态即为活跃的,同时修改top值。

    (3)最终标记暂停(Final Marking Pause)

    当应用线程的remembered set logs未满时,是不会放入filled RS buffers中的,在这样的情况下,这些remebered set logs中记录的card的修改就会被更新了,因此需要这一步,这一步要做的就是把应用线程中存在的remembered set logs的内容进行处理,并相应的修改remembered sets,这一步需要暂停应用,并行的运行。

    (4)存活对象计算及清除(Live Data Counting and Cleanup)

    值得注意的是,在G1中,并不是说Final Marking Pause执行完了,就肯定执行Cleanup这步的,由于这步需要暂停应用,G1为了能够达到准实时的要求,需要根据用户指定的最大的GC造成的暂停时间来合理的规划什么时候执行Cleanup,另外还有几种情况也是会触发这个步骤的执行的:

        G1采用的是复制方法来进行收集,必须保证每次的”to space”的空间都是够的,因此G1采取的策略是当已经使用的内存空间达到了H时,就执行Cleanup这个步骤;

        对于full-young和partially-young的分代模式的G1而言,则还有情况会触发Cleanup的执行,full-young模式下,G1根据应用可接受的暂停时间、回收young regions需要消耗的时间来估算出一个yound regions的数量值,当JVM中分配对象的young regions的数量达到此值时,Cleanup就会执行;partially-young模式下,则会尽量频繁的在应用可接受的暂停时间范围内执行Cleanup,并最大限度的去执行non-young regions的Cleanup。

    以后JVM的调优或许跟多需要针对G1算法进行调优了。

    jconsole – jconsole是基于Java Management Extensions (JMX)的实时图形化监测工具,这个工具利用了内建到JVM里面的JMX指令来提供实时的性能和资源的监控,包括了Java 程序的内存使用,Heap size, 线程的状态,类的分配状态和空间使用等等。

  • 相关阅读:
    LeftoverDataException,依赖包,apache license 2.0
    GPL,BSD,Apache,MIT开源许可协议
    一次重构经历
    转载:reactor模式学习
    版本控制学习
    系统开发,出错处理,日志
    最近学习linux命令的一个总结
    sudo,linux 新建账号,并开通ssh登录
    运行R 报错R cannot R_TempDir, 继而发现/dev/mapper/VG00-LV01 磁盘空间已满
    用InputStream读出来转换成String类型
  • 原文地址:https://www.cnblogs.com/yanghuiping/p/4001270.html
Copyright © 2011-2022 走看看