后端JVM最优详解(内存模型堆GC直接内存性能调优)
JVM内存模型结构图jdk1。8结构图(极简)
jdk1。8结构图(简单)
JVM(Java虚拟机):是一个抽象的计算模型。如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域。目的是为构建在其上运行的应用程序提供一个运行环境,能够运行java字节码。JVM可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构。
jdk1。7结构图(详细)
JVM内存模型组成元素
Java内存模型主要包含线程私有的程序计数器、java虚拟机栈、本地方法栈和线程共享的堆空间、元数据区、直接内存。Java运行时数据区域Java虚拟机在执行过程中会将所管理的内存划分为不同的区域,有的随着线程产生和消失,有的随着Java进程产生和消失。根据JVM规范,JVM运行时区域大致分为程序计数器、虚拟机栈、本地方法栈、堆、方法区(jkd1。8废弃)五个部分。程序计数器(PC寄存器、计数器)程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过它主要实现跳转、循环、恢复线程等功能。在任何时刻,一个处理器内核只能运行一个线程,多线程是通过抢占CPU,分配时间完成的。这时就需要有个标记,来标明线程执行到哪里,程序计数器便拥有这样的功能,所以,每个线程都已自己的程序计数器。可以理解为一个指针,指向方法区中的方法字节码(用来存储指向下一个指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。倘若执行的是native方法,则程序计数器中为空Java虚拟机栈(JVMStacks)虚拟机栈也就是平常所称的栈内存,每个线程对应一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫栈帧的东西,每个方法在执行的同时都会创建一个栈帧,方法被执行时入栈,执行完后出栈。不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。每个栈帧主要包含的内容如下:局部变量表存储着java基本数据类型(bytebooleancharintlongdoublefloatshort)以及对象的引用注意:这里的基本数据类型指的是方法内的局部变量局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。操作数栈动态连接方法返回地址虚拟机栈可能会抛出两种异常:栈溢出(StackOverFlowError):若Java虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度时,抛出StackOverFlowError异常内存溢出(OutOfMemoryError):若虚拟机栈的容量允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出OOM异常本地方法栈(NativeMethodStacks)本地方法栈是为JVM运行Native方法准备的空间,由于很多Native方法都是用C语言实现的,所以它通常又叫C栈。本地方法栈与虚拟机栈的作用是相似的,都是线程私有的,只不过本地方法栈是描述本地方法运行过程的内存模型。本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出StackOverFlowError和OutOfMemoryError异常。虚拟机栈和本地方法栈的主要区别:虚拟机栈执行的是java方法本地方法栈执行的是native方法Java堆(JavaHeap)Java堆中是JVM管理的最大一块内存空间。主要存放对象实例。Java堆是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都存放在这里,是垃圾收集器管理的主要区域。Java堆的分区:在jdk1。8之前,分为新生代、老年代、永久代在jdk1。8及之后,只分为新生代、老年代永久代在jdk1。8已经被移除,被一个称为元数据区(元空间)的区域所取代Java堆内存大小:堆内存大小新生代老年代(新生代占堆空间的13、老年代占堆空间23)既可以是固定大小的,也可以是可扩展的(通过参数Xmx和Xms设定)如果堆无法扩展或者无法分配内存时报OOM主要存储的内容是:对象实例类初始化生成的对象基本数据类型的数组也是对象实例字符串常量池字符串常量池原本存放在方法区,jdk8开始放置于堆中字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张stringtable静态变量static修饰的静态变量,jdk8时从方法区迁移至堆中线程分配缓冲区(ThreadLocalAllocationBuffer)线程私有,但是不影响java堆的共性增加线程分配缓冲区是为了提升对象分配时的效率堆和栈的区别:管理方式,堆需要GC,栈自动释放大小不同,堆比栈大碎片相关:栈产生的碎片远小于堆,因为GC不是实时的分配方式:栈支持静态分配内存和动态分配,堆只支持动态分配效率:栈的效率比堆高方法区(逻辑上)方法区是JVM的一个规范,所有虚拟机必须要遵守的。常见的JVM虚拟机有Hotspot、JRockit(Oracle)、J9(IBM)方法区逻辑上属于堆的一部分,但是为了与堆区分,通常又叫非堆区各个线程共享,主要用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。关闭JVM就会释放这个区域的内存。Java8以前是放在JVM内存中的,由堆空间中的永久代实现,受JVM内存大小参数限制Java8移除了永久代和方法区,引入了元空间拓展:JDK版本方法区的实现运行时常量池所在的位置JDK6PermGenspace(永久代)PermGenspace(永久代)JDK7PermGenspace(永久代)Heap(堆)JDK8Metaspace(元空间)Heap(堆)元空间(元数据区、Metaspace)元空间是JDK1。8及之后,HotSpot虚拟机对方法区的新实现。元空间不在虚拟机中,而是直接用物理(本地)内存实现,不再受JVM内存大小参数限制,JVM不会再出现方法区的内存溢出问题,但如果物理内存被占满了,元空间也会报OOM元空间和方法区不同的地方在于编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:类元信息(Class)类元信息在类编译期间放入元空间,里面放置了类的基本信息:版本、字段、方法、接口以及常量池表常量池表:主要存放了类编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中运行时常量池(RuntimeConstantPool)运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些运行时常量池具备动态性,可以添加数据,比较多的使用就是String类的intern()方法直接内存(DirectMemory)直接内存不是虚拟机运行时数据区的一部分,而是在Java堆外,直接向系统申请的内存区域。常见于NIO操作时,用于数据缓冲区(比如ByteBuffer使用的就是直接内存)。分配、回收成本较高,但读写性能高。直接内存不受JVM内存回收管理(直接内存的分配和释放是Java会通过UnSafe对象来管理的),但是系统内存是有限的,物理内存不足时会报OOM。
Java程序内存JVM内存本地内存JVM内存(JVM虚拟机数据区)Java虚拟机在执行的时候会把管理的内存分配到不同的区域,这些区域称为虚拟机(JVM)内存。JVM内存受虚拟机内存大小的参数控制,当大小超过参数设置的大小时会报OOM本地内存(元空间直接内存)对于虚拟机没有直接管理的物理内存,也会有一定的利用,这些被利用但不在虚拟机内存的地方称为本地内存。本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制。虽然不受参数的限制,如果所占内存超过物理内存,仍然会报OOM
堆外内存直接内存直接内存不是虚拟机运行时数据区的一部分,而是在Java堆外,直接向系统申请的内存区域。可通过XX:MaxDirectMemorySize调整大小,默认和Java堆最大值一样内存不足时抛出OutOfMemoryError或者OutOfMemoryError:Directbuffermemory;线程堆栈可通过Xss调整大小内存不足时抛出StackOverflowError(如果线程请求的栈深度大于虚拟机所允许的深度)OutOfMemoryError(如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)Socket缓存区每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话这块内存占用也比较可观。如果无法分配,可能会抛出IOException:Toomanyopenfiles异常JNI代码如果代码中使用了JNI调用本地库,那本地库使用的内存也不在堆中,而是占用Java虚拟机的本地方法栈和本地内存虚拟机和垃圾收集器虚拟机、垃圾收集器的工作也是要消耗一定数量的内存JVM堆及各种GC详解
JVM中的堆,一般分为三大部分:新生代、老年代、永久代(Java8中已经被移除)
新生代、MinorGC(YoungGC)
新生代主要是用来存放新生的对象。一般占据堆的13空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代又分为Eden、S0、S1(SurvivorFrom、SurvivorTo)三个区:Eden区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。SurvivorFrom区:上一次GC的幸存者,作为这一次GC的被扫描者。SurvivorTo区:保留了一次MinorGC过程中的幸存者。Eden和S0,S1区的比例为8:1:1幸存者S0,S1区:复制之后发生交换,谁是空的,谁就是SurvivorTo区JVM每次只会使用eden和其中一块survivor来为对象服务,所以无论什么时候,都会有一块survivor是空的,因此新生代实际可用空间只有90当JVM无法为新建对象分配内存空间的时候(Eden满了),MinorGC被触发。因此新生代空间占用率越高,MinorGC越频繁。
MinorGCMinorGC的过程(采用复制算法):首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,一般是15,则赋值到老年代区)同时把这些对象的年龄1(如果ServicorTo不够位置了就放到老年区)然后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。MinorGC触发机制:当年轻代满(指的是Eden满,Survivor满不会引发GC)时就会触发MinorGC(通过复制算法回收垃圾)对象年龄(Age)计数器虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数XX:MaxTenuringThreshold(阈值)来设置。老年代、MajorGC(OldGC)
老年代老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。MajorGC采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象然后回收没有标记的对象。MajorGC的耗时比较长(速度一般会比MinorGC慢10倍以上,STW的时间更长),因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM(OutofMemory)异常。永久代、元数据区(元空间)、常量池
永久代(PermGen)是JDK7及之前,HotSpot虚拟机基于JVM规范对方法区的一个落地实现,其他虚拟机如JRockit(Oracle)、J9(IBM)有方法区,但是没有永久代。在JDK1。8已经被移除,取而代之的是元数据区(元空间)内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域。和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
元数据区(元空间、Metaspace)元空间的本质和永久代类似,都是对JVM规范中方法区的实现。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:XX:MetaspaceSize(初始空间大小):达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。XX:MaxMetaspaceSize(最大空间)默认是没有限制的。除了上面两个指定大小的选项以外,还有两个与GC相关的属性:XX:MinMetaspaceFreeRatio:在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集;XX:MaxMetaspaceFreeRatio:在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集;类的元数据放入本地内存中,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由虚拟机的MaxPermSize控制,而由系统的实际可用空间来控制。
元空间替换永久代的原因分析:字符串存在永久代中,容易出现性能问题和内存溢出。通常会使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。永久代会为GC带来不必要的复杂度,并且回收效率偏低。Oracle可能会将HotSpot与JRockit合二为一。
类常量池、运行时常量池、字符串常量池类常量池在类编译过程中,会把类元信息存放到元空间(方法区),类元信息其中一部分便是类常量池主要存放字面量(字面量一部分便是文本字符)和符号引用运行时常量池在类加载时,会将字面量和符号引用解析为直接引用存储在运行时常量池(文本字符会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池)在JDK6,运行时常量池存在于方法区在JDK7,运行时常量池存在于Java堆字符串常量池存储的是字符串对象的引用,而不是字符串本身字符串常量池在jdk7时就已经从方法区迁移到了java堆中(JDK8时,方法区就是元空间)
拓展字面量java代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示:inta1;这个1便是字面量Stringbiloveu;iloveu便是字面量符号引用由于在编译过程中并不知道每个类的地址,因为可能这个类还未加载,所以如果在一个类中引用了另一个类,被引用的类的全限定类名会作为符号引用,在类加载完后用这个符号引用去获取它的内存地址。比如:com。javabc。Solution类中引用了com。javabc。Quest,那么com。javabc。Quest作为符号引用就会存到类常量池,等类加载完后,就可以拿着这个引用去元空间找此类的内存地址FullGC、MajorGC(OldGC)
MinorGC、MajorGC、FullGC的区别新生代收集(MinorGCYoungGC):只是新生代的垃圾收集老年代收集(MajorGCOldGC):只是老年代的垃圾收集整堆收集(FullGC):收集整个java堆(younggenoldgen)和方法区的垃圾收集
FullGC触发机制:调用System。gc时,系统建议执行FullGC,但是不必然执行老年代空间不足方法区空间不足通过MinorGC后进入老年代的平均大小大于老年代的可用内存由Eden区、survivorspace1(FromSpace)区向survivorspace2(ToSpace)区复制时,对象大小大于ToSpace可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小当永久代满时也会引发FullGC,会导致Class、Method元信息的卸载堆空间分成不同区的原因堆空间分为新生代和老年代的原因根据对象存活的时间,有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。新生代分为了eden、Survivor区的原因为了更好的管理堆内存中的对象,方便GC算法(复制算法)来进行垃圾回收。如果没有Survivor区,那么Eden每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发FullGC,而FullGC是非常耗时的。将Eden区满了的对象,添加到Survivor区,等对象反复清理几遍之后都没清理掉,再放到老年区,这样老年区的压力就会小很多。即Survivor相当于一个筛子,筛掉生命周期短的,将生命周期长的放到老年代区,减少老年代被清理的次数。新生代的Survivor区又分为s0和s1区的原因:分两个区的好处就是解决内存碎片化。为什么一个Survivor区不行?假设现在只有一个survivor区,模拟一下流程:新建的对象在Eden中,一旦Eden满了,触发一次MinorGC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行MinorGC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。GC优化的本质,也是为什么分代的原因:减少GC次数和GC时间,避免全区扫描。堆不是对象存储的唯一选择(逃逸分析)
如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样无需在堆上分配内存。也无须进行垃圾回收了。
逃逸分析概述:一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,对象只在方法内部引用,则认为没有发生逃逸当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。GC(垃圾回收)System。gc()GC(GarbageCollection)垃圾回收。System。gc()是用Java,C和许多其他流行的高级编程语言提供的API。当它被调用时,它将尽最大努力从内存中清除垃圾(即未被引用的对象)。在默认情况下,通过System。gc()或者Runtime。getRuntime()。gc()的调用,会显式触发FullGC(完整的GC事件),对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。在GC完成之前,整个JVM将冻结(即正在运行的所有服务将被暂停),通常完整的GC需要很长时间才能完成。因此在不合适的时间运行GC,将导致不良的用户体验,甚至是崩溃。JVM具有复杂的算法,该算法始终在后台运行,进行所有计算以及有关何时触发GC的计算。当显式调用System。gc()调用时,所有这些计算都将被抛掉。system。gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(不能确保立即生效)System。gc()可以从应用程序堆栈的各个部分调用:开发的应用程序可以显式的调用System。gc()方法System。gc()也可以由第三方库,框架触发可以由外部工具(如VisualVM)通过使用JMX触发如果应用程序使用了RMI,RMI会定期调用System。gc()GC操作应该由JVM自行控制,在绝大部分的场景都不建议程序员手动写代码显式进行System。gc()操作。但是也不排除其中个别例外:在开发多个微服务时,每个服务都有多个备份节点。在非业务高峰时段,可以从微服务负载均衡的节点池中取出其中一个JVM实例。然后通过该JVM上的JMX显式触发System。gc()调用,一旦GC事件完成并且从内存中清除了垃圾,将该JVM放回到微服务负载均衡的节点池中。当然这个过程需要很好的微服务管理及服务发布机制配合,这样既能保证JVM垃圾内存的有效清理,又不影响业务的正常运行。
如何检测应用程序正在进行System。gc()?System。gc()可以从多个渠道进行的调用,而不仅仅是从应用程序源代码进行的调用。因此,搜索应用程序代码System。gc()字符串,不足以知道GC是否正在被调用。通过GC日志可以检测应用程序是否正在进行垃圾回收java8启动GC日志:XX:PrintGCDetailsXloggc:XX:PrintGCDetailsXloggc:opttmpmyappgc。logjava9启动GC日志:Xlog:gc:fileXlog:gc:fileopttmpmyappgc。logli建议始终在所有生产服务器中始终启用GC日志,因为它有助于排除故障并优化应用程序性能。启用GC日志只会增加微不足道的开销。还可以将GC日志上传到垃圾收集日志分析器工具,例如GCeasy,HPJMeter等。这些工具将生成丰富的垃圾收集分析报告。ul
如何禁止GC显式调用或调整调用GC的频率?
如果就是想避免程序员显式调用GC,避免不成熟的程序员在不合适时间调用GC,避免人为造成的GC崩溃,可以通过如下方法:搜索和替换在代码库中搜索System。gc()和Runtime。getRuntime()。gc()如果看到匹配项,则将其删除。但是这种方法无法避免第三方库、框架或通过外部源进行调用。通过JVM参数强制禁止通过传递JVM参数XX:DisableExplicitGC来强制禁止显式调用。这种方式强制、有效,应用程序内的任何GC显式代码调用System。gc()都将被禁止生效。JVM自身的GC策略不受此参数影响,只禁止人为的触发GC。RMI如果应用程序正在使用RMI,则可以控制GC调用的频率。启动应用程序时,可以使用以下JVM参数配置该频率:Dsun。rmi。dgc。server。gcIntervalnDsun。rmi。dgc。client。gcIntervaln这些属性的默认值在JDK1。4。2和5。0是60000毫秒(即60秒)JDK6和更高版本是3600000毫秒(即60分钟)如果应用主机内存资源非常富余,可以将这些属性设置为很高的值,以便可以将GC带来的对应用程序的影响最小化。这也是应用程序性能优化的一种方式之一。
STW(StopTheWorld)事件
stoptheworld,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
可达性分析算法中枚举根节点(GCRoots)会导致所有Java执行线程停顿。分析工作必须在一个能确保一致性的快照中进行一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以需要减少STW的发生。
STW事件和采用哪款GC无关,所有的GC都有这个事件。哪怕是G1也不能完全避免Stoptheworld情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
开发中除非特殊情况,不要用system。gc()进行手动GC,会导致stoptheworld的发生。
GC常用算法分代收集算法(现在的虚拟机垃圾收集大多采用这种方式)它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。新生代中,由于对象生存期短,每次回收都会有大量对象死去,所以使用的是复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以使用的是标记整理或者标记清除。标记清除算法每个对象都会存储一个标记位,记录对象的状态(活着或是死亡)。标记清除算法分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行GC操作。优点是可以避免内存碎片。标记压缩(标记整理)算法标记压缩法是标记清除法的一个改进版,和标记清除算法基本相同。不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩(整理),然后把剩下的所有对象全部清除,这样就可以解决内存碎片问题。复制算法复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。接下来GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。复制算法不会产生内存碎片。
直接内存(DirectMemory)详解文件的读写过程传统io方式Java本身不具备磁盘的读写能力,要想实现磁盘读写,必须调用操作系统提供的函数(即本地方法)。在这里CPU的状态改变从用户态(Java)切换到内核态(system)【调用系统提供的函数后】。内存这边也会有一些相关的操作,当切换到内核态以后,就可以由CPU的函数,去真正读取磁盘文件的内容,在内核状态时,读取内容后,会在操作系统内存中划出一块儿缓冲区,其称之为系统缓冲区,磁盘的内容先读入到系统缓冲区中(分次进行读取);系统的缓冲区是不能被Java代码直接操作的,所以Java会先在堆内存中分配一块儿Java的缓冲区,即代码中的newbyte〔大小〕,Java的代码要能访问到刚才读取的那个流中的数据,必须先从系统缓冲区的数据间接读入到Java缓冲区,然后CPU的状态又切换到用户态了,然后再去调用Java的那个输出流的写入操作,就这样反复进行读写读写,把整个文件复制到目标位置。可以发现,由于有两块儿内存,两块儿缓冲区,即系统内存和Java堆内存都有缓冲区,那读取的时候必然涉及到这数据存两份,第一次先读到系统缓冲区还不行,因为Java代码不能直接访问系统缓冲区,所以需要先把系统缓冲区数据读入到Java缓冲区中,这样就造成了一种不必要的数据的复制,效率因而不是很高。directBuffer(直接缓存区)方式当ByteBuffer调用allocateDirect方法后,操作系统这边划出一块缓冲区,即directmemory(直接内存),这段区域与之前不一样的地方在于这个操作系统划出来的内存可以被Java代码直接访问,即系统可以访问它,Java代码也可以访问它,它是java代码和系统共享的一段内存区域,这就是直接内存。磁盘文件读到直接内存后,Java代码直接访问直接内存,比传统io方式少了一次缓冲区里的复制操作,所以速度得到了成倍的提高。这也是直接内存带来的好处,适合做较大文件拷贝的这种io操作。
演示案例(运行并比较时间后可以发现,尤其是读写大文件时使用ByteBuffer的读写性能非常高):演示ByteBuffer作用publicclassDemo{staticfinalStringFORMD:asdasd。mp4;选比较大的文件,比如200多兆staticfinalStringTOD:asd。mp4;staticfinalint1Mb10241024;publicstaticvoidmain(String〔〕args){io用时:3187。41008(大概用了3秒),多跑几遍,多比较,跑一次不算。io();directBuffer用时:951。114625(不到1秒)derectBuffer();}privatestaticvoiddeirectBuffer(){longstartSystem。nanoTime();try(FileChannelfromnewFileInputStream(FROM)。getChannel();FileChanneltonewFileOutputStream(TO)。getChannel();){ByteBufferbbByteBuffer。allocateDirect(1Mb);读写的缓冲区(分配一块儿直接内存)while(true){intlenfrom。read(bb);if(len1){break;}bb。flip();to。write(bb);bb。clear();}}catch(IOExceptione){e。printStackTrace();}longendSystem。nanoTime();print(directBuffer用时:(endstart)1000000。0);}用传统的io方式做文件的读写privatestaticvoidio(){longstartSystem。nanoTime();try(网友1:写到try()括号里就不用手动close了FileInputStreamfromnewFileInputStream(FROM);FileOutPutStreamtonewFileOutputStream(TO);){byte〔〕bufnewbyte〔1Mb〕;byte数组缓冲区(与上面的读写缓冲区设置大小一致,比较时公平)while(true){intlenfrom。read(buf);用输入流读if(len1){break;}to。write(buf,0,len);用输出流写}}catch(IOExceptione){e。printStackTrace();}longendSystem。nanoTime();print(io用时:(endstart)1000000。0);}}直接内存的分配和回收
直接内存的分配和释放是Java通过UnSafe对象来管理的,并且回收需要主动调用freeMemory()方法,不直接受JVM内存回收管理。
ByteBuffer底层分配和释放直接内存的大概情况:ByteBuffer对象被创建时,调用Unsafe对象的allocateMemory(1Gb)方法分配直接内存,返回longbase,即内存地址ByteBuffer对象被销毁时,调用unsafe对象的freeMemory(base)方法释放直接内存。ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被(Java)垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory()方法来释放直接内存。
演示案例(演示直接内存溢出)运行后,输出36即循环36次(一次100兆,循环36次也算3个G多了)后,爆出直接内存溢出异常:Exceptioninthreadmainjava。lang。OutOfMemoryError:Directbuffermemory演示直接内存溢出publicclassDemo{staticint100Mb10241024100;publicstaticvoidmain(String〔〕args){ListByteBufferlistnewArrayList();inti0;try{while(true){ByteBufferbyteBufferByteBuffer。allocateDirect(100Mb);每次分配100兆内存list。add(byteBuffer);把这玩意放到List中,一直循环i;}}finally{print(i);}}}
使用System。gc()间接进行直接内存的回收可能存在的问题代码案例publicclassDemo{staticint1Gb102410241024;publicstaticvoidmain(String〔〕args)throwsIOException{ByteBufferbyteBufferByteBuffer。allocateDirect(1Gb);print(分配完毕);print(开始释放);byteBuffernull;System。gc();显式的垃圾回收}}System。gc()触发的是一次FullGC,是比较影响性能的垃圾回收,不光要回收新生代,还要回收老年代,所以它造成的程序暂停时间比较长。为了防止一些程序员不小心在代码里经常写System。gc()以触发显式的垃圾回收,做一些JVM调优时经常会加上JVM虚拟机参数XX:DisableExplicitGC,禁用这种显式的垃圾回收,也就是让System。gc()代码无效。但是加上这个虚拟机参数后,可能会间接影响到直接内存的回收机制。没加虚拟机参数的话,由于byteBuffer被null了,显式触发Java垃圾回收,byteBuffer的堆内存被回收时,会调用unsafe对象的freeMemory(base)方法释放直接内存,所以也导致了直接内存也被释放掉。加虚拟机参数之后,System。gc()代码失效,虽然byteBuffer被null了,但如果内存比较充足,那么它还会暂时存活着,其创建的直接内存(ByteBuffer。allocateDirect(1Gb))也会在byteBuffer的堆内存被JVM自动进行垃圾回收前一直存在着。所以禁用System。gc()之后,会发现别的代码不受太大影响,但直接内存会受到影响,因为不能用显式的方法回收掉Bytebuffer,所以ByteBuffer只能等到JVM自动进行垃圾回收时,才会被清理,从而它所对应的那块儿直接内存在此之前也会一直不会被释放掉,这就会造成直接内存可能占用较大,长时间得不到释放这样一个现象。所以使用直接内存的情况比较多,由程序员直接手动的管理直接内存时,推荐用Unsafe的相关方法,直接调用Unsafe对象的freeMemory()方法来释放直接内存。JVM的性能调优调优参数
配置方式java〔options〕MainClass〔arguments〕options:JVM启动参数。配置多个参数的时候,参数之间使用空格分隔。参数命名:常见为参数名参数赋值:常见为参数名参数值或参数名:参数值
内存参数:Xms(s为strating):初始堆大小,JVM启动的时候,给定堆空间大小。可以设置与Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。示例:Xms3550m:设置JVM初始内存为3550M。Xmx(x为max):最大堆大小,JVM运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。示例:Xmx3550m:设置JVM最大可用内存为3550M。Xmn(n为new):新生代大小整个堆大小新生代大小老年代大小持久代大小(jkd1。8废弃)持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的38示例:Xmn2g:设置年轻代大小为2G。Xss:设置每个线程的Java栈大小。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在30005000左右。JDK5。0以后每个线程Java栈大小为1M,以前每个线程堆栈大小为256K。示例:Xss128k:设置每个线程的堆栈大小为128k。XX:NewSizen:设置年轻代大小XX:NewRation:设置年轻代(包括Eden和两个Survivor区)与年老代的比值。示例:设置为4:年轻代与年老代所占比值为1:4,年轻代占整个堆栈的15XX:SurvivorRation:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。示例:设置为3:表示Eden:Survivor3:2,一个Survivor区占整个年轻代的15。XX:MaxPermSizen:设置永久代大小示例:XX:MaxPermSize16m:设置持久代大小为16m。XX:MaxTenuringThresholdn:设置垃圾最大年龄如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。
垃圾回收器参数
JVM给了三种选择:串行收集器、并行收集器、并发收集器。串行收集器只适用于小数据量的情况。XX:UseSerialGC:设置串行收集器。XX:UseParallelGC:设置并行收集器,表示年轻代使用并行收集器。XX:UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5。0以上,JVM会根据系统配置自行设置,所以无需再设置此值。XX:UseParallelOldGC:设置并行年老代收集器JDK6。0支持对年老代并行收集。XX:UseConcMarkSweepGC:设置年老代并发收集器CMS。XX:UseG1GC:设置G1收集器XX:ParallelGCThreadsn:设置并行收集器收集时最大线程数使用的CPU数。并行收集线程数。XX:MaxGCPauseMillisn:设置并行收集最大暂停时间,单位毫秒。可以减少STW时间。XX:GCTimeRation:设置垃圾回收时间占程序运行时间的百分比。公式为1(1n)并发收集器设置XX:CMSIncrementalMode:设置为增量模式。适用于单CPU情况。XX:UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等。此值建议使用并行收集器时,一直打开。XX:CMSFullGCsBeforeCompactionn:此值设置运行多少次GC以后对内存空间进行压缩、整理。因为并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生碎片,使得运行效率降低。XX:UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片。
元空间参数:XX:MetaspaceSize:初始化的Metaspace大小,该值越大触发MetaspaceGC的时机就越晚。随着GC的到来,虚拟机会根据实际情况调控Metaspace的大小,而上下浮动主要由XX:MaxMetaspaceFreeRatio和XX:MinMetaspaceFreeRatio两个参数控制。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用javaXX:PrintFlagsInitial命令查看本机的初始化参数。XX:MinMetaspaceFreeRatio:当进行过MetaspaceGC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增加MetaspaceSize的大小(为了避免过早引发一次垃圾回收)。默认值为40,也就是40。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。XX:MaxMetaspaceFreeRatio:当进行过MetaspaceGC之后,会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会减小MetaspaceSize的大小。默认值为70,也就是70。XX:MaxMetaspaceExpansion:Metaspace增长时的最大幅度。默认值大约为5MB。XX:MinMetaspaceExpansion:Metaspace增长时的最小幅度。默认值大约330KB。XX:MaxMetaspaceSize:最大空间。默认是没有限制的。指定该值可以防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。
辅助参数
JVM提供了大量命令行参数,打印信息,供调试使用。商业项目上线的时候,不允许使用。一定使用loggc。主要有以下一些:XX:PrintGC输出形式:〔GC118250K113543K(130112K),0。0094143secs〕〔FullGC121376K10414K(130112K),0。0650971secs〕XX:PrintGCDetails输出形式:〔GC〔DefNew:8614K781K(9088K),0。0123035secs〕118250K113543K(130112K),0。0124633secs〕〔GC〔DefNew:8614K8614K(9088K),0。0000665secs〕〔Tenured:112761K10414K(121024K),0。0433488secs〕121376K10414K(130112K),0。0436268secs〕XX:PrintGCTimeStampsXX:PrintGC:可与上面两个混合使用输出形式:11。851:〔GC98328K93620K(130112K),0。0082960secs〕XX:PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用输出形式:Applicationtime:0。5291524secondsXX:PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用输出形式:Totaltimeforwhichapplicationthreadswerestopped:0。0468229secondsXX:PrintHeapAtGC:打印GC前后的详细堆栈信息Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
调优建议年轻代大小选择响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达老年代的对象。吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。老年代大小选择响应时间优先的应用:老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可能会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:并发垃圾收集信息持久代并发收集次数传统GC信息花在年轻代和年老代回收上的时间比例减少年轻代和老年代花费的时间,一般会提高应用的效率吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的老年代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。较小堆引起的碎片问题因为老年代的并发收集器使用标记清除算法,所以不会对堆进行压缩。当收集器回收时,它会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现碎片,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记清除方式进行回收。如果出现碎片,可能需要进行如下配置:XX:UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。XX:CMSFullGCsBeforeCompaction0:上面配置开启的情况下,这里设置多少次FullGC后,对老年代进行压缩
文章来源:墨鸦https:developer。aliyun。comarticle1165868
动手高玩打造新玩具GB掌机强行连接单反镜头效果不错任天堂经典掌机GB曾经推出配件口袋相机,当时引发玩家更多趣味玩法,近日有高玩将高端单反镜头直接给连接上,来看看到底有什么效果。口袋相机是任天堂于1998年发售的GB周边,……
10位过气主持人现状摆摊卖猪肉县城混商演,各有各的心酸国内这么多优秀的主持人,你最喜欢的主持人是谁呢?或许有人会说何炅、撒贝宁、朱广权、汪涵。。。。。有名气有实力的主持人很多,大家喜欢的对象也五花八门。当然,这个……
故宫的日常夕阳下的故宫角楼,远处是林立的高楼。昔日的紫禁城,如今已成为北京这座繁华大都市最亮丽的景色之一。2019年春节,故宫张灯结彩,在宫门上悬挂门神画,欢迎全国游客来故宫里过大……
吃鸡蛋一定要吃蛋黄鸡蛋是很多人最爱的食材之一。不管是煎、蒸、煮做出来都营养美味。但是生活中依然存在很多疑惑,比如鸡蛋到底哪个部分最有营养?很多人认为鸡蛋的蛋白里含有丰富的蛋白质,事实并不是……
风雨路上,感恩相遇,永远珍惜人这一生,所有的遇见,都无预兆;所有的相逢,都是有缘。唯有缘分到了,才有两个人的意外遇见;一旦缘分过期,即便不舍也得说句再见。缘,……
李月明中国女排第一美女,身高1。88米,二婚嫁给美国丈夫1997年,曾经的中国女排第一美女李月明宣布退役,前往美国留学深造。直到退役李月明也没能实现自己心中的目标带领中国女排重回巅峰,不免让人唏嘘不已。另一方面,李月明因为出色……
魔兽世界社区委员会陷种族争论,Discord频道被封从创建到运行只有一个多月时间,为解决危机而创立的《魔兽世界》社区委员会最近爆出了丑闻,在他们单独设立的Discord(国外类似YY语音)频道,被发现有涉及种族主义的讨论,有截图……
中国式旅游,害惨了多少人什么是中国式旅游?随着我们生活水平的提高,我们不单单只是追求吃饱穿暖,更在不断地追求精神上的富足。有钱有闲的时候,就去旅旅游,散散心。可不知道什么时候开始,生……
转移目标?湖人步行者曝4换1地震交易,组新版四巨头冲击总冠军近日,根据ShamsCharania的报道,多位消息人士透露,独行侠和尼克斯有兴趣交易得到迈尔斯特纳。同时,湖人和黄蜂也对特纳产生了一定兴趣。据步行者随队记者ScottAgne……
从入门到精通,部落与弯刀新手玩家加精攻略《部落与弯刀》是一部带着奇幻色彩的沙河策略类游戏,相信不少玩家在刚接触这个游戏的时候,都处于一种好奇探索的心态,但一开始过多的职业选择会让很多新手玩家头疼。每个职业的背景介绍,……
苹果太狠了,MacBookPro2023存储性能腰斩,连51买过苹果产品的小伙伴可能都有印象,有时候自己追求够用就好,但苹果一定不会让你够用。比如此前就在手机的容量设置上做文章,再比如此前的MacBookPro低配版降性能之类。那现在的……
尼克斯有意得到萨迪克贝,但活塞并不愿交易走他SNY记者IanBegley报道,消息源透露,尼克斯成为对活塞前锋萨迪克贝有意的球队之一。IanBegley在稍后的采访中表示:总体来说,最近被谈论的名字就是活塞球员萨迪……
中国首家七星级酒店,斥巨资36亿坐落在三亚,网友太豪华了不知道大家平时住的酒店都是什么星级的呢?要知道,想要服务各方面都不错,肯定是星级越高越好了。接下来,就给大家介绍一下中国的首家七星酒店,看完之后你肯定也想要去住一住,因为真的太……
小米投资的自动驾驶公司,冲击科创板IPO,拟募资20亿2022年的自动驾驶市场有些冷,全球自动驾驶第一股图森未来今年市值已跌去九成,Mobileye的上市估值从500亿美元缩水至不足200亿美元,但这些依旧没有阻挡自动驾驶公司IP……
马斯克大裁员,同时取消居家办公,省时间却让人越来越颓废列位欢迎来到照理说事。此前的节目我们跟大家说过,美国现在很多高科技大公司遭遇到了危机。以推特为代表,包括谷歌、微软、facebook,甚至亚马逊现在纷纷提出了裁员计划。尤……
人生离不开美食(序)人不同冰箱,只要有电就能一直运转。但这并不意味着人完全不需要能量就可以一直维持日常运动,只是人体的能量是来源于食物。食物为什么可以提供给人体能量?通过日常生活中各类……
在绿城南宁的留恋栖息广西药用植物园,位于南宁市兴宁区长堽路189号,占地200多万平方米,是一座融游览、科研、教学和生产于一体的综合性园地,也是我国及东南亚地区规模最大、种植药用植物最多的专业药用……
今日全国尿素出厂报价2022年12月8日哈喽,大家好!这里是化肥价格行情!关注我每天看最新尿素、复合肥、磷铵、钾肥价格行情!今天(2022年12月8日)下面我们说说今日国内尿素价格行情!今日,国内尿素行情……
马未都被朋友带去土耳其浴场,声称再也不会去了,搓澡历经太难受有一次,马未都去土耳其出差,听说当地有一家洗澡堂400多年历史,屁颠屁颠就去了。可他刚刚坐下,就有些懊恼。一提到马未都,有点眼力劲的人就会认为他是娱乐圈的一员,事实……
潇湘麓山洞庭实验室揭牌欲打造国际先进实验室潇湘实验室揭牌。向一鹏摄中新网长沙12月5日电(向一鹏)12月5日,潇湘、麓山、洞庭实验室揭牌仪式在湖南大学举行。此次揭牌的三个实验室将分别围绕智能制造、工业设计和食品工……
孩子不会写作文,家长可以看看这篇文章有位家长分享了孩子写的作文,我们先一睹为快。这篇作文好在哪里呢?(一)主题明确,开篇点题。(二)动静结合,既写出了荷塘花开满塘的静态美,也写出了小鱼儿、小鸟儿……
贵阳花溪国家湿地公园十里河滩景区贵阳花溪国家城市湿地公园是贵州省首个国家城市湿地公园,平均海拔1140米,年平均气温14。9C,冬无严寒,夏无酷暑,气候温和湿润,空气清新宜人,具有生态大氧吧、天然大空调的美称……
如果孩子在学校遭遇霸凌,作为家长的你该怎么办?我是生活在东北农村的90后,我的学习生涯中从未见过霸陵这件事,小时候没有手机也没有很多玩具,孩子们最大的乐趣就是在一起玩游戏、疯跑、玩泥巴、弹溜溜、上树抓鸟、下河捞鱼,就算有些……
到底是什么?网传甘肃兰州发现不明发光体,气象局回应正在调查北京时间11月28号消息,对于近日网友表示意外拍摄到甘肃兰州上空有不明发光体的信息,兰州当地的气象局做出了最新回应,表示目前情况已收到,正在调查。相信大家应该还记得,在前……