对于Java程序员来讲
JVM是内功,也是考核之
在这里,我们从以下几个方面去了解
JMM 内存结构
运行时数据区
垃圾回收
类加载机制
JVM优化
/ 概述 /
JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。
/ JVM内存结构 /
JVM组成
可以看出JVM分两个子系统和两个组件
子系统:类加载器 + 执行引擎
组件:运行时数据区 + 本地库接口
内存结构&内存模型&对象模型
这三个完全是三个不同的概念
JVM内存结构
---->和Java虚拟机的运行时区域有关。
由《Java虚拟机规范(Java SE 8)》中描述的所知
JVM内存结构由Java虚拟机规范定义,
其描述的是Java程序执行过程中, 由JVM管理的不同的数据区域。
Java内存模型
---->和Java的并发编程有关。
Java内存模型是根据英文Java Memory Model(JMM)翻译过来的。
其实JMM并不像JVM内存结构一样是真实存在的,只是一个抽象的概念。
Java对象模型
---->和Java对象在虚拟机中的表现形式有关。
Java是一种面向对象的语言,Java对象在JVM中的存储也是有一定的结构的。
而这个关于Java对象自身的存储模型称之为Java对象模型。
/ 运行时数据区 /
方法区(Method Area)
存储类的信息[创建的时间/元数据的信息]、常量池、静态变量、即时编译器编译之后的代码
线程共享,但数据非安全
如果方法区域中的内存无法满足分配请求,Java虚拟机将抛出OutOfMemoryError。
堆(Heap)
用来存放对象/数组
线程共享,会被多个线程共享,数据非安全
如果计算需要的堆超过了自动存储管理系统所能提供的堆,Java虚拟机将抛出OutOfMemoryError.
本地方法栈(Native Method Stack)
线程私有
可用于实现Java虚拟机指令集的解释器。是我们的代码运行空间。我们编写的每一个方法都会放到 栈 里面运行。
如果线程中的计算需要比允许的更大的本地方法堆栈,则Java虚拟机将抛出StackOverflowError。
如果可以动态扩展本机方法堆栈并尝试扩展本机方法堆栈,但可用内存不足,或者如果可用内存不足,无法为新线程创建初始本机方法堆栈,则Java虚拟机将抛出OutOfMemoryError。
Java虚拟机栈(JVM Stack)
线程私有
每个方法执行的同时会创建一个栈帧,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息
如果线程中的计算比Java虚拟机堆栈允许的更大,会抛出StackOverflowError。
如果可以尝试动态扩展Java虚拟机堆栈,但无法提供足够的内存来实现扩展,无法为新线程创建初始Java虚拟机堆栈,会抛出OutOfMemoryError。
程序计数器(The PC Register)
Java虚拟机可以同时支持多个执行线程。
每个Java虚拟机线程都有自己的pc(程序计数器)寄存器。
在任何时候,每个Java虚拟机线程都在执行单个方法的代码,即该线程的当前方法。
如果该方法不是本机方法,则pc寄存器包含当前正在执行的Java虚拟机指令的地址。
如果线程当前执行的方法是本机的,则Java虚拟机的pc寄存器的值是未定义的。
方法区&永久区&元空间
1、方法区&永久区&元空间的由来
方法区(Method) 是 JVM 的规范,所有虚拟机必须遵守的。
永久区(PermGen space)是HotSpot虚拟机基于JVM规范对方法区的一个落地实现, 并且只有 HotSpot 才有 PermGen space。
而如 JRockit(Oracle)、J9(IBM) 虚拟机有方法区 ,但是没有 PermGen space。
PermGen space 是 JDK7及之前,HotSpot虚拟机对 方法区的一个落地实现,在JDK8被移除。
Metaspace(元空间)是JDK8及之后,HotSpot 虚拟机对方法区的新的实现。
Jdk1.7 前 方法区就是永久代 运行时常量池在 永久代
Jdk1.7 时 方法区就是永久代 运行时常量池在 堆
Jdk1.8 时 方法区就是元空间 运行时常量池在 堆
2、元空间参数使用
永久代使用的是jvm内存
元空间并不在虚拟机中,而是使用本地内存。
因此,默认情况下,元空间的大小仅受本地内存限制,
但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
3、元空间的特点
每个加载器有专门的存储空间。
不会单独回收某个类。
元空间里的对象的位置是固定的。
如果发现某个加载器不再存货了,会把相关的空间整个回收。
/ 垃圾回收 /
年轻代 VS 老年代
JVM将对象生命长短进行分代:年轻代、老年代、永久代。
JVM将Java堆内存划分为了两个区域,年轻代,老年代。
年轻代,创建和使用完之后立马就要回收的对象存放的区域
老年代,创建之后需要一直长期存在的对象存放的区域。
分代原因
跟垃圾回收有关
对于年轻代里的对象,它们的特点是创建之后很快会被回收,所以需要用一种垃圾回收算法
对于老年代里的对象,他它们的特点是需要长期存在,所以需要另外一种垃圾回收算法。
所以需要分成两个区域来放不同的对象。
四种引用
强引用:强引用(StronglyReference)是最传统的"引用",指在程序代码之中普遍存在的引用赋值,即Object obj = new Object()。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被强引用的对象。因此,强引用是造成Java内存泄漏的主要原因之一。
软引用:软引用需要用SoftReference类来实现,它是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。软引用通常用在对内存敏感的程序中。
弱引用:弱引用需要用WeakReference类来实现,它是用来描述那些非必须的对象,但强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集为止。当垃圾收集器开始工作时,无论当前内存是否足够,都会回收掉被只有被弱引用的对象。
虚引用:虚引用需要用PhantomReference类来实现,它也被称为“幽灵引用”或“幻影引用”,它是最弱的引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例。为一个对象设置虚引用的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
判断对象是否存活
引用计数器:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
可达性分析:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
垃圾回收算法
标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
标记-复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
如何选择垃圾回收算法
答:不同代用不同的垃圾回收算法
Young区:复制算法-->每次垃圾回收,存活的对象都比较少
Old区:清除算法/整理算法-->一般存活时间比较长,意味着很难回收
绝大多数的对象都被回收-->一般对象是对朝生夕死的
垃圾回收器的分类
1.评判一个垃圾回收器的好坏的两个标准:
吞吐量+停顿时间
2.从Young Old 分
3.从串行、并行、并发分
串行收集器
Serial、Serial Old
适用于内存较小的嵌入式设备,只能有一个垃圾回收线程执行,用户线程暂停
并行收集器(吞吐量优先)
ParNew、Parallel Scanvenge、Parallel Old
适用于科学计算、后台处理等弱交互场
并发收集器(停顿时间优先)
CMS、G1
适用于对时间有要求的场景,如web,用户线程和垃圾收集线程同时执行
4.并行 & 并发
并行(Concurrent):Two queues one coffee machine.
并发(Parallel): Two queues two coffee machines.
————Jeo Armstrong
对比CMS和G1
CMS(ConcurrentMark-Sweep)
CMS 是以牺牲吞吐量为代价来获得最短停顿时间的垃圾回收器,非常适用 B/S 系统。
在启动JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用CMS 垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候会产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现Concurrent Mode Failure,临时 CMS 会采用Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
G1(Garbage First)
G1是一种兼顾吞吐量和停顿时间的GC 实现,是 JDK 9 以后的默认 GC 选项。
Garbage first 垃圾收集器是目前垃圾收集器理论发展的前沿成果,相比与CMS 收集器,G1 收集器两个突出的改进是:
1. 基于标记-整理算法,不产生内存碎片。
2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域 的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾 多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得高的垃圾收集效率。
/ 类加载机制 /
类加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们的顺序如下图所示:
双亲委派原则
一个类加载器,自己不会先加载,而是先在找自己的父类加载
如果父类加载器还存在父类加载器,则进一步是向上委托
如果父类加载器可以完成类加载任务,就成功返回,
如果父类加载器无法完成任务,子类加载器才会尝试自己去加载
这就是双亲委派原则,这就保证了只加载一次
/ JVM优化 /
JVM调优工具
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,
其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
jconsole:用于对 JVM 中的内存、线程和类等进行监控;
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、gc变化等。
常用的JVM调优参数
-Xms2g:初始化堆大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
-XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。