为什么要JVM调优
- 代码是运行在jvm中,而部署环境多样,每种环境采用默认配置可能运行的效率较差
- 高并发的情况下,默认配置不足以支撑
JVM调优的目标
JVM调优目标:使用较小的内存占用来获得较高的吞吐量或者较低的延迟。
- 延迟:由于垃圾收集而引起的程序停顿时间
- 吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值
JVM的组成
运行时数据区、类加载子系统、本地方法库、执行引擎
类加载子系统
类加载机制:当需要使用某个类时,虚拟机将会加载它的”.class”文件,并创建对应的class对象,将class文件加载到虚拟机的内存
类的加载过程
类加载到虚拟机过程:加载 验证 准备 解析 初始化 ----------------加载后:使用 卸载
- 加载:将类的class文件读入内存中,并为之创建一个Class对象【Class类型】
任何类被使用时,都会在堆内存中建立一个该类型的Class对象
类的加载方式分为隐式加载和显示加载两种
隐式加载:使用new关键词创建对象
显示加载:直接调用Class.forName()方法
-
验证:验证类是否符合java的语言规范,与其他类的兼容性(如final修饰的类不能被继承)
-
准备:为类的静态成员变量分配内存空间,并设置默认初始值为0或null(如定义static int i=123,此时只会被赋值为0,123在接下来的初始阶段才会被赋值,但如果static final int i=123,则准备阶段i会被赋值为123)
-
解析:把类中的符号引用变为直接引用。
符号引用:方法名,对象名
直接引用:内存地址
-
初始化:静态代码块的执行、静态成员变量的初始化、普通成员变量的初始化
当初始化一个类的时候发现其父类还没有初始化,需要先对父类进行加载
类加载器
-
类加载器有四种:前三种必须有
①启动类加载器(BootStrap ClassLoader):加载jdk安装目录下的核心类
②扩展类加载器(Extension ClassLoader):加载jdk安装目录下的扩展类
③应用程序类加载器(Application ClassLoader):加载classpath下的类,如第三方jar包的类、自定义的类
④自定义加载器(User ClassLoader)
-
不同类加载器加载同一个字节码文件,等到的类不同
-
双亲委派机制:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载
-
双亲委派机制的优势:
- 避免重复加载造成的混乱,同名的类父类加载成功,子类就不需要加载了
- 安全,防止java的核心内库被修改。比如在calsspath下自定义一个java.lang.Object类,通过双亲委派机制最终会委托启动类加载器加载,而启动类加载器会在自己的加载路径下发现这个同名的类,就不会去加载classpath下的object类,直接返回自己加载路径下的object类
运行时数据区
运行时数据区组成:方法区、堆、虚拟机栈、本地方法栈、程序计数器
线程共享的数据区:方法区、堆
线程隔离的数据区:虚拟机栈、本地方法栈、程序计数器
程序计数器
- 程序计数器:用于确定指令的执行顺序
- 每个线程都有自己独立的程序计数器,程序计数器占用内存空间很小,但为了保证每个线程执行的顺序,JVM中唯一不会发生内存溢出的区域
OOM:out of memory 内存溢出
虚拟机栈
虚拟机栈中放栈帧
虚拟机会为每一个线程创建一个虚拟机栈,每个栈又存放了若干栈帧,每个方法执行都会创建一个栈帧,每个栈帧中存储了局部变量、操作数栈、方法返回地址、动态链接等。
虚拟机栈常见的两个错误:
- StackOverFlowError:
- OOM:OutofMemoryError
栈溢出 stack overflow
方法的运行是在栈中进行的,每调用一次方法就会从栈顶往栈底压一层栈帧,调用结束就从栈顶弹出,当方法递归层次过深时,栈的空间就无法再存放栈帧,导致溢出
本地方法栈
-
本地方法栈和虚拟机栈的区别:
本地方法栈执行的是本地方法(native修饰的方法,非java语言实现),虚拟机栈执行的是java方法
方法区
- JDK1.7以前,方法区主要用于存储虚拟机加载的类信息、静态变量、常量以及编译器编译后的代码。
- JDK1.7及其以后,静态变量、字符串常量池在存放在堆内存,类信息、除字符串常量池以外的常量池、编译器编译后的代码还在方法区中
- JDK1.7及其以前,方法区是堆内存的一部分,是一片连续的存储空间,当时为了和堆进行区分,方法区也叫:非堆、永久代
- JDK1.8开始,类信息、静态变量、编译后的代码等存放到元空间,元空间直接占用的本地内存(不是java虚拟机占用的内存),方法区已经不存在
- 去永久代的原因:
- 字符串常量放在永久代中,多了以后容易影响性能和内存溢出
- 永久代中GC回收效率低,容易造成堆内存泄漏
常量池
常量池分为静态常量池和运行时常量池。
- 静态常量池:*.class文件中的常量池,主要包括字面量常量和符号引用量。
- 字面量:字符串常量、final修饰的常量
- 符号引用量:类和接口的完全限定名、字段名称和描述符、方法名和描述符
- 运行时常量池:类加载完成后,将class文件中的常量池载入到内存中,并保存在方法区中,此外运行期间也可以放入新的常量
堆内存
- 堆主要存放对象、数组,jDK1.7开始存放字符串常量和静态变量,是垃圾回收器的主要区域。
- 堆内存和方法区被所有线程共享
- 堆内存在虚拟机启动的时候创建
- 堆内存占用JVM内存最多
JDK1.8以前,堆被分为:新生代、老年带、永久代(方法区),JDK1.8开始永久代(方法区)不存在了,被元空间替代,但元空间存放在本地内存,不占用虚拟机所占用的内存
-
新生代又分为Eden区和两个Survivor区
Eden主要存放new或instance方法创建出来的 的对象。
当经历一次GC后,对象就会被存入其中一个survivor区。
当再默认经历15次GC后,对象会被放入另外一个survivor区。
堆内存溢出OOM
新创建的对象最初存放在新生代,新生代满了后进行一次GC,如果Minor GC后仍然空间不足,就会把该对象和新生代中满足条件的对象就会被放入老年代,老年代空间不足时就会进行Full GC,如果之后的空间仍然不足以存放新对象,就会抛出OutofMemoryError错误。
常见原因:
- 内存中加载数据过多,如一次性从数据库获取大量数据
- 集合对对象引用过多,且使用完后没有清空集合
- 循环过程中产生过多对象
- 堆内存分配过小
堆内存泄漏
申请堆内存使用完后,无法释放该空间,导致无法再次使用该空间。一次内存泄漏可以忽略,多次就会出问题
C/C++容易出现,java有自动回收机制
元空间
- 元空间占用的是本地内存,不占用虚拟机所占用的内存
- 元空间存储:类信息、编译器编译后的代码、除字符串常量池以外的常量池
JDK1.8和JDK1.7最大区别
元空间取代了永久代(方法区),元空间位于本地内存,永久代位于虚拟机的堆内存
元空间存储
执行引擎
执行引擎包含即时编译器(JIT)和垃圾回收器(GC)
即时编译器JIT
执行特别频繁的代码,会被即时编译器翻译为与本地平台相关的机器码
垃圾回收器GC
Xms:JVM启动时的初始堆内存
Xmx:JVM运行时的最大堆内存
Xss:栈的大小