jvm内存模型概述1.Jvm简介1.JVM架构2.JVM运行时数据区3.JVM内存模型JVM运行时内存=共享内存区+线程内存区3.1、共享内存区sharing内存区=持久区(方法区+其他)+堆(OldSpace+YoungSpace(den+S0+S1))持久代:JVM使用持久区(PermanentSpace)实现方法区,主要存放所有加载的类信息,方法信息,常量池等。持久化区域的初始化值和最大值可以通过-XX:PermSize和-XX:MaxPermSize指定。永久空间与方法区不同。只不过HotspotJVM使用了PermanentSpace来实现方法区。有些虚拟机没有PermanentSpace,使用其他机制来实现方法区。堆(heap):主要用来存放类的对象实例信息(包括new操作实例化的对象和定义的数组)。堆分为OldSpace(又名,TenuredGeneration)和YoungSpace。OldSpace主要存放应用程序中长生命周期的存活对象;伊甸园(GardenofEden)主要存放新生的物品;S0和S1是两个大小相同的内存区域,主要存放Eden每次垃圾回收后存活的对象,作为对象从Eden到OldSpace(S指英文单词SurvivorSpace)的缓冲区。堆之所以需要分段,是为了方便对象的创建和垃圾回收。3.2、线程内存区线程内存区(JVM栈):线程内存区=单线程内存+单线程内存+……单线程内存=PCRegster+JVM栈+本地方法栈JVM栈=栈帧+栈帧+.....栈帧=局部变量区+操作数区+帧数据区在Java中,一个线程对应一个JVM栈(JVMStack),JVM栈记录了线程的运行状态。JVM栈是由栈帧组成的,一个栈帧代表一次方法调用。栈帧由三部分组成:局部变量区、操作数栈、帧数据区。线程在栈区,不能共享数据。它们只能通过复制共享区域中的数据来用作缓存。所有的多线程写法都会有bug。Voliate使得获取的数据不被缓存和实时更新。关键字volatile是一种轻量级的同步机制。Volatile变量对所有线程的可见性意味着当一个线程修改这个变量的值时,新值是可见的并且立即被其他线程知道。volatile变量在多线程下不一定是安全的,因为它只有可见性和顺序性,没有原子性。2、JVM内存空间管理JVM将内存分为以下几个区域:共享内存区=持久区(方法区+其他)+堆(OldSpace+YoungSpace(den+S0+S1));Java内存模型和线程:每个线程都有工作内存,线程只能修改自己工作内存中的数据,然后同步回主内存,由多个内存共享。2.1方法区(共享内存区的持久区)方法区(又称持久代):要加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final的常量,类中的字段信息,类中的方法信息。方法区也是全局共享的。开发者调用类对象中的getName、isInterface等方法获取信息时,数据来自于方法区。在一定条件下也会被GC。当方法区要使用的内存超过允许的大小时,会抛出OutOfMemory:PermGenSpace异常。错误信息。在SunJDK中,该区域对应PermanetGeneration。默认最小值为16MB,最大值为64MB。可以通过-XX:PermSize和-XX:MaxPermSize指定最小值和最大值。在Hotspot虚拟机中,这个区域对应的是永久代(PermanentGeneration)。一般很少对方法区进行垃圾回收,所以方法区也被称为PermanentGeneration的原因之一,但这并不意味着方法区完全没有垃圾回收。其上的垃圾回收主要是常量池的内存回收和加载类的卸载。方法区的垃圾回收是苛刻和困难的,我们稍后会介绍。运行时常量池(RuntimeConstantPool)是方法区的一部分,用于存放字面量常量、符号引用以及编译时产生的翻译后的直接引用(符号引用是用字符串表示某些变量和接口的编码)的位置,直接引用是根据符号引用翻译的地址,翻译会在类链接阶段完成);运行时常量池不仅可以存放编译时常量,还可以存放运行时产生的常量,比如String类的intern()方法,该方法的作用就是String维护了一个常量池。如果被调用的字符“abc”已经在常量池中,则返回该字符串在常量池中的地址;否则,创建一个新常量并将其添加到池中,并返回地址。JVM方法区相关参数,最小值:--XX:PermSize;最大值--XX:MaxPermSize。2.2堆区(堆区是所有线程共享的)堆用来存放对象实例和数组值。可以认为Java中new创建的所有对象的内存都分配在这里,堆区是所有线程共享的。Heap中对象占用的内存被GC回收。在32位操作系统上,最大为2GB,在64位操作系统上,没有限制。它的大小可以通过-Xms和-Xmx来控制,-Xms是JVM启动时申请的最小堆内存,默认为物理内存的1/64但小于1GB;-Xmx是JVM可以申请的最大堆内存,默认是物理内存的1/4但小于1GB,默认是当空闲堆内存小于40%时,JVM会增加Heap到-Xmx指定的大小,可以通过-XX:MinHeapFreeRatio=指定;当空闲堆内存大于70%时,JVM会将Heap的大小减小到-Xms指定的大小,可以通过-XX:MaxHeapFreeRatio=来指定这个比例,对于正在运行的系统,为了为避免在运行时频繁调整Heap的大小,通常将-Xms和-Xmx的值设置为相同。堆区是理解JavaGC机制最重要的区域。在JVM管理的内存中,堆区是最大的一块。堆区也是JavaGC机制管理的主内存区。堆区由所有线程共享,在虚拟机启动时创建。堆区用于存储对象实例和数组值。可以认为java中new创建的所有对象都在这里分配。2.3本地方法栈(NativeMethodStack)本地方法栈用于支持本地方法的执行,存储每一次本地方法调用的状态。本地方法栈和虚拟机方法栈的运行机制是一样的。它们之间唯一的区别是虚拟机栈是用来执行Java方法的,而本地方法栈是用来执行native方法的。在很多虚拟机中(比如Sun的JDK默认的HotSpot虚拟机),本地方法栈和虚拟机栈会一起使用。2.4虚拟机栈(JVMStack)(线程私有)JVM方法栈:线程私有,在内存分配上非常高效。当方法运行结束时,其对应的栈帧占用的内存会自动释放。当JVM方法栈空间不足时,会抛出StackOverflowError错误,其大小可由SunJDK中的-Xss指定。虚拟机栈占用操作系统的内存,每个线程对应一个虚拟机栈,线程私有,分配效率很高。一个线程的每个方法在执行的同时都会创建一个栈帧(StatckFrame)。栈帧存储局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中被压入栈中,当方法执行完成时,栈帧从堆栈中弹出。方法的相关局部变量存储在局部变量表中,包括各种基本数据类型、对象引用、返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32位),其他都是1个Slot。需要注意的是,局部变量表是在编译时确定的,分配给方法运行的空间完全在栈帧中确定,在方法的生命周期内不会改变。虚拟机堆栈中定义了两个异常。如果线程调用的堆栈深度大于虚拟机允许的最大深度,将抛出StatckOverFlowError(堆栈溢出);但是,大多数Java虚拟机都允许动态扩展虚拟机栈大小(有些Part是定长的),所以线程可以一直申请栈,直到内存不足,这时候就会抛出OutOfMemoryError(内存溢出)被抛出。2.5程序计数器(ProgramCounterRegister)(线程私有)程序计数器是一块比较小的内存区域,可能是CPU寄存器,也可能是操作系统内存。主要用来表示当前线程执行的字节码。可以理解为当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。每个程序计数器只用来记录一个线程的行号,所以是线程私有的(一个线程有一个程序计数器)。如果程序正在执行Java方法,则计数器记录正在执行的虚拟机的字节码指令地址;如果是执行本地(native,用C语言写的)方法,计数器的值为Undefined,由于程序计数器只记录当前指令地址,不存在内存溢出。因此,程序计数器也是所有JVM内存区中唯一没有定义OutOfMemoryError的区域。三、内存溢出和内存泄漏内存泄漏:分配的内存无法回收内存溢出:系统内存不够1.堆溢出分为:内存泄漏和内存溢出,两者都会抛出OutOfMemoryError:javaheapSpace例外:内存泄漏:内存泄漏是指对象实例在创建和使用后仍然被引用,无法通过垃圾回收释放,一直累积到没有剩余可用内存为止。如果存在内存泄漏,我们需要找出泄漏的对象是如何被GCROOT引用的,然后通过引用链来分析泄漏的原因。分析内存泄漏的工具有:Jprofiler、visualvm等。while(true){list.add(UUID.randomUUID());}}}看一下控制台的输出,因为我这边JVM设置的参数内存足够大,所以需要一定的时间才能看到效果:b.内存溢出内存溢出是指当我们创建一个新的强度对象时,实例对象所需的内存空间大于堆的可用空间。如果出现内存溢出问题,往往是程序本身需要的内存大于我们为虚拟机配置的内存。这种情况下,我们可以增加-Xmx来解决这个问题。公共类OOMTest_1{publicstaticvoidmain(Stringargs[]){ListbyteList=newArrayList();byteList.add(新字节[1000*1024*1024]);}}2.栈溢出栈(JVMStack)主要存放栈帧(局部变量表、操作数栈、动态链接、方法退出信息)。注意栈和栈帧的区别:栈包含栈帧。与线程栈相关的内存异常有两种:a:StackOverflowError(方法调用层次太深,内存不够创建新的栈帧)b:OutOfMemoryError(线程太多,内存不够创建)anewthread)a、java.lang.StackOverflowError堆栈溢出抛出java.lang.StackOverflowError错误。出现这种情况是因为方法运行时,请求新的栈帧时,栈的剩余空间小于栈帧需要的空间。例如,通过递归调用该方法,不断产生栈帧,直到抛出异常,栈空间被填满:}/***通过递归调用该方法,不断生成栈帧并填满栈空间,直到抛出异常:*@paramargs*/publicstaticvoidmain(String[]args){SOFTestsof=newSOFTest();sof.stackOverFlowMethod();}}b。OutOfMemoryError(暂不介绍)4.JVM内存分配Java对象占用的内存主要是在堆上实现的,因为堆是线程共享的,所以在堆上分配内存时需要加锁,从而导致对象创建价格昂贵。当堆上空间不足时,就会触发GC。如果GC后空间仍然不足,则会抛出OutOfMemory异常。为了提高内存分配效率,新生代Eden区的HotSpot虚拟机采用了两种技术来加速内存分配,即bump-the-pointer和TLAB(Thread-LocalAllocationBuffers)。由于Eden区是连续的,bump-the-pointer技术的核心是跟踪最后创建的对象。创建对象时,只需要检查最后一个对象后面是否有足够的内存,从而大大加快了内存分配。;而TLAB技术是针对多线程的,它会在新一代的EdenSpace上为每一个新创建的线程分配一个独立的空间。这个空间叫做TLAB(ThreadLocalAllocationBuffer),它的大小是由JVM根据运行情况计算出来的。您可以使用-XX:TLABWasteTargetPercent来设置它可以占用的EdenSpace的百分比,默认是1%。在TLAB上分配内存不需要锁定。一般JVM会先在TLAB上分配内存。如果对象太大或者TLAB空间用完了,还是会分配在堆上。因此,在编写程序时,分配多个小对象比大对象更有效率。可以在启动参数中加入-XX:+PrintTLAB,查看TLAB空间的使用情况。如果对象在新生代存活了足够长的时间而没有被清理(也就是经历了几次MinorGC存活),就会被复制到老年代。老年代的空间一般比年轻代大。存储的对象更多,老年代GC次数比年轻代少。当老年代内存不足时,会执行MajorGC,也叫FullGC。您可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略。如果是动态控制的话,可以动态调整Java堆中各个区域的大小和进入老年代的时间。如果对象比较大(比如长字符串或者大数组),新生代空间不足,会直接将大对象分配给老年代(大对象可能会触发earlyGC,应该使用应避免使用寿命短的大型对象)。使用-XX:PretenureSizeThreshold来控制直接提升到老年代的对象的大小。大于这个值的对象会直接分配到老年代。五、内存回收方式1、收集器:引用计数收集器、跟踪收集器1、引用计数收集器:上图中,ObjectA释放对ObjectB的引用后,ObjectB的引用计数器变为0,此时ObjectB占用的内存可以恢复。引用计数器需要在每次分配对象时增加或减少引用计数器,它有一定的消耗。另外,对于循环引用的场景,引用计数器也没有办法实现回收。比如上面的例子,如果ObjectB和ObjectC相互引用,即使ObjectA释放了对ObjectB和ObjectC的引用,ObjectB和ObjectC也无法恢复。非常不合适,SunJDK在实现GC的时候并没有使用这种方式。2.Trace收集器实现算法:Trace收集器采用集中管理的方式,会全局记录数据引用的状态。基于某些条件(如时机、空间不足)触发,在执行过程中需要从根集合中扫描对象的引用关系,这可能会导致应用程序暂停。主要有三种实现算法:Copying:年轻代的Eden区,Mark-Sweep和Mark-Compact。1、复制:特点:当需要回收的空间中存活对象很少时,复制算法的效率会更高,带来的代价是增加一块空内存空间和移动对象。Stop-copy算法:将可用内存按容量大小分成两块,每次只使用其中一块。当这块内存用完后,将存活的对象复制到另一块中,然后一次性清理掉已使用的内存空间。商用虚拟机:将内存分成一个较大的eden空间和两个较小的survivor空间。默认比例为8:1:1,即每次新生代可用内存空间为整个新生代容量的90%。每次使用伊甸园和一名幸存者。回收时,一次性将eden和survivor中的存活对象复制到另一块survivor中,最后清理掉刚刚使用过的eden和survivor空间。如果另一块幸存者空间没有足够的内存空间来存放上次新生代收集的对象,当有存活对象时,这些对象会通过分配保证机制直接进入老年代。复制的方法是从根集合中扫描出存活对象,将找到的存活对象复制到一个新的完全未使用的空间中,如图:复制收集器方法只需要从根集合中扫描出所有存活对象即可collection对于存活对象,当需要回收的空间中存活对象较少时,复制算法的效率会更高(这种算法用在年轻代的Eden区),带来的代价是增加清空内存空间并移动对象。2.mark-clear:特点:当空间中存活对象较多时效率更高,但由于mark-clear直接使用非存活对象占用的内存,会造成内存碎片。标记清除法是从根集合开始扫描,对存活的对象进行标记,标记后在整个空间内扫描未标记的对象,并进行清理。标记和清除的过程如下图所示:蓝色部分是被引用的存活对象,棕色部分是未被引用的可回收对象。在标记阶段,将扫描所有对象以标记对象,扫描过程是耗时的。未被引用的对象在清理阶段被回收,存活的对象被保留。内存分配器会保存一个空闲空间的引用列表,当有分配请求时,它会查询空闲空间引用列表进行分配。标记扫除动作不需要对象移动,并且仅适用于非生命对象。当空间中存活对象较多时效率更高,但由于mark-sweep直接回收非存活对象占用的内存,会造成内存碎片。3.Mark-compression特性:在mark-clear的基础上,需要移动对象,成本相对较高。优点是不会产生内存碎片。mark-compression和mark-clear一样,都是标记活对象,只是clear之后的处理不同。mark-compression清除对象占用的内存后,会将所有存活的对象移动到左端的空闲空间,然后更新引用其对象的指针,如下图所示:很明显,mark-compression在mark-clear的基础上移动并正则化存活对象,解决内存碎片问题,获得更连续的内存空间。提高分配效率,但是因为需要移动对象,所以成本比较高。总结:JVM通过GC回收堆区和方法区的内存,这个过程是自动进行的。谈到Java的GC机制,主要完成三件事:判断哪些内存需要回收;判断何时需要执行GC;如何执行GC。JVM主要使用收集器来实现GC。主要收集器包括引用计数收集器和跟踪收集器。垃圾回收算法:1.引用计数算法2.跟踪回收算法3.压缩回收算法4.复制回收算法5.分代回收算法。为什么要按代回收。Java对象的生命周期一般都不长。6.虚拟机中的GC过程6.1为什么要分代回收?一开始JVM的GC是以mark-clear-compression的方式进行的,效率不是很高,因为当分配的对象越来越多的时候,对象列表越来越大,扫描和移动也越来越复杂.越费时间,导致内存回收越慢。但是基于java应用的分析,发现大部分对象的生存时间都很短,只有少部分数据的生存期比较长。分代收集:新生代stop-copy算法oldgenerationmark-cleanormark-6.2虚拟机中清除GC的过程经过上面的介绍,我们已经知道为什么JVM需要分代回收了。让我们详细了解一下整个回收过程。1初始阶段,新创建的对象被分配到Eden区,survivor的两个空间都是空的。当Eden区满时,触发minorgarbage(MinorGC年轻代垃圾回收机制)2扫描标记后,将存活的对象复制到S0,回收未存活的对象3在下一次MinorGC中,Edenzone的情况同上,未被引用的对象被回收,存活的对象复制到survivorzone。但是在survivor区,S0的所有数据都被复制到S1。需要注意的是,上次MinorGC过程中移到S0的两个对象在复制到S1后年龄会加1。此时Eden区中的S0区被清空,将所有存活的数据复制到S1区,S1区存在不同年龄的对象。流程如下图所示:4.下一次MinorGC会重复这个流程。这次survivor两个区域交换了,将存活对象复制到S0,存活对象的年龄加1,清空Eden区域和另一个survivor区域。5让我们演示一下升级过程。经过几次MinorGC,当存活对象的年龄达到阈值(可通过参数配置,默认为8),就会从年轻代提升到老年代。6随着MinorGC一次又一次的进行,新的对象会不断的被提升到老年代。7以上基本涵盖了整个年轻一代的所有回收过程。最终老年代会发生MajorGC,老年代的空间会被清理和压缩。总结:从上面的流程可以看出,Eden区域是一个连续的空间,其中一个Survivor永远是空的。一次GC复制后,一个Survivor保存当前存活的对象,Eden区和另一个Survivor区的内容不再需要,直接清空即可。在下一次GC时,两个幸存者的角色将再次互动。改变。因此,这种方法在分配内存和清理内存方面效率极高。这种垃圾回收方式就是著名的“Stop-and-copy”清理方式(将Eden区和一个Survivor中的一个survivor对象复制到另一个Survivor中),这并不是说stop-and-copy清理方式效率很高,在事实上,它只有在这种情况下才有效率(基于大多数对象的生命周期都很短),如果停止在老年代复制是非常不合适的。老年代存放的对象比新生代多很多,大对象也很多。在清理老年代的内存时,如果使用stop-copy算法,效率是相当低的。一般老年代使用的算法是标记-压缩算法,即:标记出存活的对象(带引用),将所有存活的对象移动到一端,保证内存的连续性。当MinorGC发生时,虚拟机检查每次提升到老年代的大小是否大于老年代的剩余空间。如果比较大,会直接触发FullGC。否则检查是否设置了-XX:+HandlePromotionFailure(允许保证失败),如果允许则只进行MinorGC,此时可以容忍内存分配失败;如果不允许,仍然会执行FullGC(这意味着如果设置了-XX:+HandlePromotionFailure,MinorGC会同时触发FullGC,即使老年代还有很多内存,所以最好不要这样做)。关于方法区(共享内存区中的永久代)即永久代的回收,永久代回收有两种:常量池中的常量和无用的类信息。常量的回收很简单,不用引用就可以回收。对于无用类的回收,必须保证3点:类的所有实例都已经回收了没有必要,可以通过参数设置是否回收类。