5. JVM
5.1 初识JVM
5.1.1 JDK、JRE和JVM
5.1.2 源码到类文件
javac Person.java--->Person.class
Person.java -> 词法分析器 -> tokens 流 -> 语法分析器 -> 语法树/ 抽象语法树 -> 语义分析器
-> 注解抽象语法树 -> 字节码生成器 -> Person.class 文件
Person.class是个16进制文件,可用sublime打开,其结构可从官方文档查看如下:
5.1.3 类加载机制
装载:
(1)通过一个类的全限定名获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在Java堆中生成一个代表这个类的java.lang.Class
对象,作为对方法区中。
链接:
(1)验证:保证被加载类的正确性文件格式验证/元数据验证/字节码验证/符号引用验证。
(2)准备:为类的静态变量分配内存,并将其初始化为默认值。 默认值不是类设定的默认值而是数值类型的默认值,例如:
static int a = 0
此时给a 分配空间,并初始化为0。 int类型的默认值均为0.
(3)解析:把类中的符号引用转换为直接引用这些数据的访问入口。 就是将class的二进制数据转化为地址。
初始化:
对类的静态变量,静态代码块执行初始化操作。 此时设置a=10;
5.1.4 类加载器
双亲委派模型:
定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
优势:Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java中的Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的Object类。
双亲委派模型有多种打破方式:
- 自己实现
ClassLoad
类,重写loadClass
方法
5.2 运行时数据区
5.2.1 官网解读
方法区和堆的生命周期与虚拟机绑定在一起,其他与线程有关。
5.2.2 方法区
方法区:
方法区是各个线程共享的内存区域,在虚拟机启动时创建。
用于存储已被虚拟机加载的类信息(类的描述信息、创建的时间,作者)、常量、静态变量、即时编译器编译后的代码等数据。
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。
当方法区无法满足内存分配需求时,将抛出
OutOfMemoryError
异常。方法区在JDK 8中就是
Metaspace
【元空间】,在JDK6或7中就是Perm Space
【永久代】。Run-Time Constant Pool(运行时常量池)在方法区分配。
方法区是非线程安全的。
5.2.3 堆
堆:
堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所
有线程共享,每个进程只有一个堆。Java对象实例以及数组都在堆上分配。
当堆无法满足内存分配需求时,将抛出
OutOfMemoryError
异常。
5.2.4 Java虚拟机栈
Java虚拟机栈:
虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
每一个被线程执行的方法,为该栈中的栈帧,即每个方法的执行对应一个栈帧。
调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
如果调用的方法过多,栈的深度不够就会抛出
StackOverflowError
异常。
栈帧:
每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。
每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、动态链接(运行时常量池的引用、把类中的符号引用转换为直接引用)、方法返回地址(Return Address)和附加信息。
理解栈帧:
对Class进行反编译,生成字节码指令
javap -c Person.class > Person.txt
5.2.5 程序计数器
程序计数器:
一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置。
- 程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)。
- 如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址。
- 如果正在执行的是Native方法,则这个计数器为空。
5.2.6 本地方法栈
本地方法栈:
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。
。
5.3 JVM内存模型
触发讲对象拷贝到old区是,对象过大,或者经过15次GC,仍然存活。
Young
区:
Young
区分为两大块,一个是Survivor
区(S0+S1),一块是Eden
区。Eden
:S0:S1=8:1:1。 划分s0 和s1是为了解决空间碎片问题。
Eden
区:
正常对象创建所在区域,大多数对象“朝生夕死”,避免频繁触发GC
Survivor
区:
- Survivor区分为两块S0和S1,也可以叫做From和To。
- 在同一个时间点上,S0和S1只能有一个区有数据,另外一个是空的。
- 当某个对象值太大,s区放不下时,会触发担保机制:从old区借用一些内存空间,来存放,知道他被回收或者超过15次被移动到old区。
Old
区:
一般Old
区都是年龄比较大的对象,或者相对超过了某个阈值的对象。
结合工具体验与验证:
插件下载链接
- jvisualvm 在bin目录下打开。
- 堆内存溢出:设置参数比如-Xmx20M -Xms20M
- 方法区内存溢出:-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M
- 虚拟机栈溢出:-Xss128k
5.4 垃圾回收
5.4.1 如何确定一个对象是垃圾
- 引用计数法:一旦相互持有引用,就导致对象永远没法被回收。
-
可达性分析:由
GC Root
出发,开始寻找,看看某个对象是否可达。
GC Root
:类加载器、Thread、本地变量表、static成员、常用引用、本地方法栈中的变量等
垃圾回收算法
-
标记清除
扫描整个内存空间,会造成大量空间碎片。
-
复制
解决空间碎片问题,造成空间浪费。
-
标记整理
young区用复制算法,old区用标记整理或者标记清楚
5.4.2 分代收集算法
Young
区:复制算法
Old
区:标记清除或标记整理
5.4.3垃圾收集器
-
Serial
Serial Old
-
ParNew
Parallel
相比ParNew,更加关注吞吐量Parallel Old
-
CMS
更加关注停顿时间
-
G1
更加关注停顿时间:用户可以设置一个预期的停顿时间
5.4.4 相关知识梳理
垃圾收集器分类
吞吐量和停顿时间
如何选择收集器
如何开启收集器
5.5 工欲善其事必先利其器
5.5.1 JVM参数
- 标准参数
- -X参数
- -XX参数
-XX:[+/-] -XX:+UseG1GC
-XX:<name>=<value> -XX:InitialHeapSize=100M
(4)其他参数
-Xms100M ===>-XX:InitialHeapSize=100M
-Xmx100M -Xss100
5.5.2 JVM命令
-
jps
: 当前的java进程 -
jinfo
: 查看某个java进程目前的参数设置的情况 -
jstat
: 查看java进程统计性能 -
jstack
:查看当前java进程的堆栈信息 -
jmap
:打印出堆转存储快照
jmap -heap PID
dump出堆内存相关信息:jmap -dump:format=b,file=heap.hprof PID
5.5.3 常用工具
1.jconsole
jvisualvm
arthas
mat/perfma
:内存相关的信息gceasy.io
/gcviewer
: 垃圾回收
5.6 性能优化
5.6.1 JVM全局理解
5.6.2 OOM
通过MAT分析dump文件,定位OOM。
5.6.3 GC优化
通过不断调整,观察GC日志的吞吐量和停顿时间,寻找最佳值
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+UseG1GC
-Xloggc:gc.log
gceasy.io
主要就是调整各种参数,垃圾收集器--->查看吞吐量和停顿时间的变量 高吞吐量,低停顿时间。