什么是JVM
什么是JVM
JVM是Java虚拟机,是Java Virtual Machine的缩写。Java借助JVM实现了平台无关性,只需要在操作系统平台上部署JVM,Java编译生成的目标代码(字节码)就可以在Java虚拟机上运行,使得Java语言在不同平台上运行时,不需要重新编译,从而实现了一次编译多处运行。
JDK、JRE、JVM的关系
JDK是开发工具集,由开发环境和运行环境组成,处于操作系统层之上。JRE是JDK的组成部分,JRE是标准执行环境,包含JVM和Java核心类库。
JVM的体系结构
JVM是介于java程序和操作系统之间的一个区域,java文件编写完成后会被编译成class文件,然后经过类加载器进入运行时数据区。main方法在运行时会被压入栈底,每运行完一个方法会被弹出,垃圾回收不可能存在在栈里,而应该存在于堆中,由于方法区为特殊的堆,因此大部分性能调优都是对于堆的性能调优。
堆区(Heap Area):Java堆是被所有线程共享的一块内存区域,在虚拟机启动时被创建,是Java虚拟机所管理的内存中最大的一块区域。此内存区域的唯一用途是存放对象实例,几乎所有的对象实例都在这里分配内存。
栈区(Stacks Area):Java虚拟机栈是线程私有的,它的生命周期与线程相同。栈区描述的是Java方法执行的内存模型,每个方法被执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
方法区(Method Area):方法区与堆区一样,是各个线程共享的内存区域,它用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
程序计数器(Program Counter Register):程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
本地方法栈(Native Method Stacks):本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
堆内存中分为三个区域:新生区、老年区、永久区(元空间),JDK8以后永久区改名为元空间。
GC回收主要发生在新生区和老年区,当内存满值的时候,则会报错(OOM),即堆内存不够。
JVM调优的意义
程序在运行中有时会出现一些大大小小的JVM问题:
比如cpu load过高、请求延迟、tps降低等,甚至出现内存泄漏、内存溢出导致系统崩溃。
每次垃圾收集时间越来越长,收集频率越来越高,每次垃圾收集清理掉的垃圾数据越来越少。
因此需要对JVM进行调优使得程序在正常运行的前提下,获得更高的用户体验和运行效率。调优目标是用较小的内存占用,来获得较高的吞吐量,或者较低的延迟。
JVM调优经验总结
与CAP原则一样,同时满足内存占用小、延迟低、高吞吐量是不可能的,要求低延的可以多设置一点内存, 对延时要求不高的,可以少设置一点内存。
程序的目标不同,调优时所考虑的方向也不同,在调优之前,必须要结合实际场景,有明确的的优化目标,找到性能瓶颈,对瓶颈有针对性的优化,最后进行测试,通过各种监控工具确认调优后的结果是否符合目标。
年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年老代主要存放生命周期较长的对象,是经过几次的Young Gen的垃圾回收后仍然存在的对象,内存大小相对较大,垃圾回收相对没有那么频繁,可能几小时一次。
JVM配置一般可以先用默认配置,在测试中根据系统运行状况,结合gc日志、内存监控、使用的垃圾收集器等进行合理的调整,当老年代内存过小时可能引起频繁Full GC,当内存过大时Full GC时间会特别长。
那么JVM的配置比如新生代、老年代应该配置多大最合适呢?答案是不一定,调优就是找答案的过程。物理内存一定的情况下,新生代设置越大,老年代就越小,Full GC频率就越高,但Full GC时间越短;相反新生代设置越小,老年代就越大,Full GC频率就越低,但每次Full GC消耗的时间越大。建议如下:
-Xms和-Xmx的值设置成相等,堆大小默认为-Xms指定的大小,默认空闲堆内存小于40%时,JVM会扩大堆到-Xmx指定的大小;空闲堆内存大于70%时,JVM会减小堆到-Xms指定的大小。如果在Full GC后满足不了内存需求会动态调整,这个阶段比较耗费资源。
新生代尽量设置大一些,让对象在新生代多存活一段时间,每次Minor GC 都要尽可能多的收集垃圾对象,防止或延迟对象进入老年代的机会,以减少应用程序发生Full GC的频率。
老年代如果使用CMS收集器,新生代可以不用太大,因为CMS的并行收集速度也很快,收集过程比较耗时的并发标记和并发清除阶段都可以与用户线程并发执行。
方法区大小的设置,1.6之前的需要考虑系统运行时动态增加的常量、静态变量等,1.7后只要差不多能装下启动时和后期动态加载的类信息就行。
Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。永久代 PermSize和MaxPermSize设置为老年代存活对象的1.2-1.5倍。年轻代Xmn的设置为老年代存活对象的1-1.5倍。老年代的内存大小设置为老年代存活对象的2-3倍。
代码上的规范
代码实现方面,性能出现问题比如程序等待、内存泄漏除了JVM配置可能存在问题,代码实现上也有很大关系:
避免创建过大的对象及数组:过大的对象或数组在新生代没有足够空间容纳时会直接进入老年代,如果是短命的大对象,会提前出发Full GC。
避免同时加载大量数据,如一次从数据库中取出大量数据,或者一次从Excel中读取大量记录,可以分批读取,用完尽快清空引用。
当集合中有对象的引用,这些对象使用完之后要尽快把集合中的引用清空,这些无用对象尽快回收避免进入老年代。
可以在合适的场景(如实现缓存)采用软引用、弱引用,比如用软引用来为ObjectA分配实例:SoftReference objectA=new SoftReference(); 在发生内存溢出前,会将objectA列入回收范围进行二次回收,如果这次回收还没有足够内存,才会抛出内存溢出的异常。
避免产生死循环,产生死循环后,循环体内可能重复产生大量实例,导致内存空间被迅速占满。
尽量避免长时间等待外部资源(数据库、网络、设备资源等)的情况,缩小对象的生命周期,避免进入老年代,如果不能及时返回结果可以适当采用异步处理的方式等。
参考:JVM调优总结 - Andrew.Zhou - 博客园
————————————————
版权声明:本文为CSDN博主「扁豆的主人」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_40213018/article/details/116602377