1)JVM架构图
- 类装载器 ClassLoader
- 负责加载class文件,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构。
- ClassLoader只负责class文件的加载,至于它是否可以运行,由执行引擎Execution Engine决定。
- 双亲委派机制:当一个类收到加载请求时,会先把请求委派给父类去完成,因此所有的类加载请求都是按照如下顺序加载,因此保证了使用不同的类加载器最终能得到的都是同样一个Object对象,保证了安全性(鲁棒性)。
- Bootstrap Class Loader>>Extension Class Loadder>>AppClassLoader>>User Defined Class Loader(用户自定义类)。
- 程序计数器 Program Counter Register
- 每个线程都有一个程序计数器Program Counter Register,是线程私有的。
- 如同一个指针,指向方法区的方法字节码(即用来存储指向下一条指令的地址),由执行引擎Execution Engine读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
- 如果执行的是一个Native方法,则计数器为空。
- 用以完成分支、循环、跳转、异常处理、线程回复等基础功能。不会发生内存溢出OutOfMemory错误。
- 本地方法栈 Native Method Stack
- 登记以native修饰的方法,在执行引擎Execution Engine执行时加载本地方法库。
- 方法区 Method Area
- 供各线程共享的运行时内存区域,存储了每一个类的结构信息(如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容)。
- 方法区是规范,在不同的虚拟机里实现有所不同。最典型的代表就是永久代Permanent Space(jdk1.7之前)和元空间Metaspace(jdk1.7之前)。
- Java栈 Java Stack
- 主管Java程序的运行,8种基本类型的变量+对象的引用变量+实例方法都是在栈内存中分配。
- 对于栈来说,不存在垃圾回收问题,生命周期跟随线程,线程结束时栈内存也就释放,是线程私有的。
- 堆 Heap
-
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。
-
类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分:
-
Young Generation Space 新生区(新生代) Young/New
-
Tenuer Generation Space 养老区(老年代) Old/Tenure
-
方法区有个别名叫Non-Heap(非堆),很多开发者习惯将方法区称之为“永久代”,但严格上来说两者并不同,永久代是方法区的一个实现。
- Permanent Space 永久区(永久代) Perm(jdk1.7之前)
- Metaspace 元空间(jdk1.7之前)
-
- 执行引擎 Execution Engine
- 负责解释命令,提交操作系统执行。
2)垃圾回收 GC
2.1 GC堆
-
堆内存逻辑上分为三部分:新生区+养老区+永久代(jdk1.7之前)/元空间(jdk1.8之后)
-
永久代/元空间
- 是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据(及运行环境必须的类信息),不会被垃圾回收器回收掉,关闭JVM才会释放此区域所占用的内存。
- 堆内存物理上分为两部分:新生代+老年代
2.2 GC原理
-
新生代(Eden Space)满了,开启
GC=YGC=轻GC 幸存者进入s0满了,则GC后进入s1区 s1区满了,则GC后进入s0区 (即s0和s1区循环交换) 每次GC计数加一,默认计数到15后,进入养老区(Tenure Generation Space)
-
养老区满了,开启
Full GC=FGC=重GC 多次FGC后若发现养老区空间清理效果不明显 则报OOM(OutOfMermoy)。
2.3 GC算法
- 引用计数法(现以淘汰)
- 每次对象引用时,计数加一,但计数为0时清楚。
- 缺点:每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗,较难处理循环引用。
- 复制算法(Copying)
- 将内存分为两块,每次只用其中一块,当这一块用完,就将存活的对象复制到另外一块,不会产生内存碎片。
- 从根集合(GC Root)开始,通过Tracing从From中找到存活对象,拷贝到To中,交替进行。
- 缺点:损耗空间,浪费了一半的内存,且当对象的存活率很高时,复制花费时间过高。
- 标记清除(Mark-Sweep)
- 算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象。
- 缺点:两次扫描(标记、清除),耗时严重,会产生内存碎片。
- 标记压缩(Mark-Compact)
- 算法分成标记和压缩两个阶段,先标记出存活对象,再一起往一端移动,然后清除边界以外的内存,不会产生内存碎片
- 缺点:需要移动对象的成本,耗时长。
- 标记-清除-压缩(Mark-Sweep-Compact)
- 标记清除和标记压缩的结合,多次标记清楚后压缩,去除内存碎片。
没有最好的垃圾回收算法,只有最合适的,因此一般使用分代收集算法。
-
内存效率:复制算法>标记清除>标记整理(简单的对比时间复杂度)
-
内存整齐度:复制算法=标记整理>标记清楚
-
内存利用率:标记整理=标记清楚>复制算法
-
年轻代
- 区域相对老年代较小,对象存活率低。
- 复制算法效率最高,内存率利用不高的问题,通过两个幸存区Survivor的设计得到缓解。
-
老年代
- 区域较大,对象存活率高。
- 一般由标记清除或者是标记-清除-整理算法混合实现。
3)JVM参数详解
3.1 原理图
- JDK1.7
- JDK1.8
- 在Jave8中,永久代被元空间所取代,元空间的本质和永久代类似。
- 永久代使用的是JVM的堆内存,元空间使用的是本机物理内存。
- 因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
3.2 参数调优
- 标配参数
- -verison 输出版本信息
- -help 输出帮助信息
- showversion 输出版本信息和帮助信息
- -verison 输出版本信息
- X参数(了解即可)
- Xint 解释执行
- Xcomp 第一次使用就编译成本地代码
- Xmixed 混合模式(默认)
- Xint 解释执行
- XX参数
- Boolean类型 (+表示开启 -表示关闭)
- KV设值类型 (K=V)
jps -l查看正在运行的进程的编号
jinfo -flags [进程编号] 查看配置参数
jinfo -flag [XX参数] [进程编号] 查看对应的[xx参数]信息
java -XX:+PrintFlagsInitial 查看全局默认配置信息
java -XX:+PrintFlagsFinal 查看全局正在使用的最终配置信息
=表示默认没改动 :=表示JVM或者人为修改过的
3.2 常用参数解析
- -Xms
- 初始化内存大小,默认为物理内存的1/64
- 等价于 -XX:InitialHeapSize
- -Xmx
- 最大分配内存,默认为物理内存的1/4
- 等价于 -XX:MaxHeapSize
- -Xss
- 设置单个线程栈的大小,默认为512k~1024k(通过jinfo -flag 查看值为0,表示默认值 )
- 等价于 -XX:ThreadStackSize
- -Xmn
- 设置新生代大小
- -XX:MetaspaceSize
- 设置元空间大小,默认21M左右
- -XX:+PrintGCDetails
- 输出GC详情,收集日志信息
- -XX:SurvivorRatio
- 设置新生代幸存区比例,默认Eden:s0:s1=8:1:1
- -XX:SurvivorRatio=4,及Eden:s0:s1=4:1:1
- -XX:NewRatio
- 设置新生代和老年代的比例,默认Young:Old=1:2
- -XX:NewRatio=4,及Young:Old=1:4
- -XX:MaxTenuringThreshold
- 设置垃圾回收最大年龄,默认为15(允许设置年龄为0-15)
3.3 参数配置
根据实际开发需要,设置对应参数。