认识
JDK > JRE > JVM
源码到类文件
javac ** .java —> **.class
** .java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器-> 注解抽象语法树 -> 字节码生成器 -> ** .class文件
类加载机制
转载 :
(1)通过一个类的全限定名获取定义此类的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
连接 :
(1)验证:保证被加载类的正确性
文件格式验证/元数据验证/字节码验证/符号引用验证
(2)准备
为类的静态变量分配内存,并将其初始化为默认值
(3)解析
把类中的符号引用转换为直接引用
初始化
对类的静态变量,静态代码块执行初始化操作
双亲委派模型
定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
优势:Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java中的Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的Object类。
运行时数据区
划分
跟虚拟机生命周期绑定在一起的
方法区
堆
跟线程的生命周期绑定在一起的
java虚拟机栈
程序计数器
本地方法栈
方法区
-
方法区是各个线程共享的内存区域,在虚拟机启动时创建
-
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
-
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来
-
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
-
方法区在JDK 8中就是Metaspace【元空间】,在JDK6或7中就是Perm Space【永久代】
-
Run-Time Constant Pool在方法区分配
堆
-
堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享
-
Java对象实例以及数组都在堆上分配
-
当堆无法满足内存分配需求时,将抛出OutOfMemoryError异常
Java虚拟机栈
(先进后出)
-
虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建
-
每一个被线程执行的方法,为该栈中的栈帧,即每个方法的执行对应一个栈帧
-
调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出
栈帧
每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。
每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、动态链接、方法返回地址(Return Address)和附加信息。
程序计数器
一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。
假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置。
-
程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)
-
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址
-
如果正在执行的是Native方法,则这个计数器为空。
本地方法栈
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。
JVM内存模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n8Ll7tpz-1614608651763)(img\内存模型.png)]
young区内的对象被gc15次会转到Old区 ,当young对象过大时放不下也会被分配到Old区 等等
在Young区中 Eden区有对象被gc回收 还有没有被回收的 放入s区,解决空间碎片问题,保证空间连续性
s区分为 s0 s1 两个来回往复的接收Eden没有被GC回收的数据保证一定会有一个空间为空
当存活对象很大 s0中放不下
会触发 担保机制 -> s区会去Old区借一些空间存放对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EvRxD89l-1614608651771)(img\内核模型流程图解.png)]
MinorGC和FullGC的区别
MinorGC:发生在新生代的垃圾回收,因为新生代的特点,MinorGC非常频繁,且回收速度比较快,每次回收的量也很大。
FullGC:发生在老年代的垃圾回收,也称MajorGC,速度比较慢,相对于MinorGc慢10倍左右。进行一次FullGC通常会伴有多次多次MinorGC。
Young区
Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区。 Eden:S0:S1=8:1:1
会有10%的空间浪费 用于解决空间碎片问题,保证空间连续性
Eden区
正常对象创建所在区域,大多数对象“朝生夕死”
Survivor区
Survivor区分为两块S0和S1,也可以叫做From和To。
在同一个时间点上,S0和S1只能有一个区有数据,另外一个是空的。
Old区
一般Old区都是年龄比较大的对象,或者相对超过了某个阈值的对象。
垃圾回收
如何判断一个对象是垃圾
-
引用计数法
一旦相互持有引用,就导致对象永远没法被回收
-
可达性分析
由GC Root出发,开始寻找,看看某个对象是否可达
GC Root:类加载器、Thread、本地变量表、static成员、常用引用、本地方法栈中的变量等
垃圾回收算法
-
标记清除
-
标记:遍历内存区域,对需要回收的对象打上标记。
-
清除:再次遍历内存,对已经标记过的内存进行回收。
缺点:
-
效率问题;遍历了两次内存空间(第一次标记,第二次清除)。
-
空间问题:容易产生大量内存空间碎片,空间不连续,当再需要一块比较大的内存时,无法找到一块满足要求的,因而不得不再次出发GC。
-
-
复制
将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复。
优点
- 相对于标记–清理算法解决了内存的空间碎片问题。
- 效率更高(清理内存时,记住首尾地址,一次性抹掉)。
缺点:
- 内存利用率不高,每次只能使用一半内存。
改进:
-
研究表明,新生代中的对象大都是“朝生夕死”的,即生命周期非常短而且对象活得越久则越难被回收。在发生GC时,需要回收的对象特别多,存活的特别少,因此需要搬移到另一块内存的对象非常少,所以不需要1:1划分内存空间。而是将整个新生代按照8 : 1 : 1的比例划分为三块,最大的称为Eden(伊甸园)区,较小的两块分别称为To Survivor和From Survivor。
首次GC时,只需要将Eden存活的对象复制到To。然后将Eden区整体回收。再次GC时,将Eden和To存活的复制到From,循环往复这个过程。这样每次新生代中可用的内存就占整个新生代的90%,大大提高了内存利用率。
但不能保证每次存活的对象就永远少于新生代整体的10%,此时复制过去是存不下的,因此这里会用到另一块内存,称为老年代,进行分配担保,将对象存储到老年代。若还不够,就会抛出OOM。
老年代:存放新生代中经过多次回收仍然存活的对象(默认15次)。
-
标记整理
- 标记:对需要回收的进行标记
- 整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存。
分代收集算法
Young区:复制算法
Old区:标记清除或标记整理
垃圾收集器
- Serial
- Serial Old
- ParNew
- Parallel :相比ParNew,更加关注吞吐量
- Parallel Old
- CMS :更加关注停顿时间
- G1:更加关注停顿时间:用户可以设置一个预期的停顿时间
垃圾收集器分类
-
串行收集器 -> Serial和 Serial Old
只能有一个垃圾回收线程执行,用户线程暂停。适用于内存比较小的嵌入式设备。
-
并行收集器(吞吐量优先) Parallel Scanvenge、 Parallel Old
多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。适用于科学计算、后台处理等若交互场景。
-
并发收集器(停顿时间优先) -> CMS、G1
用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。适用于相对时间有要求的场景,比如web。
吞吐量和停顿时间
停顿时间 -> 垃圾收集器进行垃圾回收终端应用执行响应的时间
吞吐量→>运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验;
高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
小结: 这两个指标也是评价垃圾回收器好处的标准,其实调优也就是在观察者两个变量。
如何选择收集器
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于100M,使用串行收集器
- 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
- 如果允许停顿时间超过1秒,选择并行或M自己选
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器
如何开启收集器
-
串行
-XX:+UseSerialGc -XX:+UseSerialOldGc
-
并行(吞吐量优先)
-XX:+UseParallelGc -XX:+UseParallelOldGc
-
并发收集器(响应时间优先)
-XX:+UseConcMarkSweepGc -XX:+UseG1Gc
JVM参数
JVM命令
-
jps: 当前的java进程
-
jinfo: 查看某个java进程目前的参数设置的情况
-
jstat: 查看java进程统计性能
-
jstack:查看当前java进程的堆栈信息
-
jmap:打印出堆转存储快照 jmap -heap PID
dump出堆内存相关信息:jmap -dump:format=b,file=heap.hprof PID
常用工具
- jconsole
- jvisualvm
- arthas
- mat/perfma:内存相关的信息
- gceasy.io/gcviewer