*
JVM:
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。(字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在)。
Java 程序从源代码到运行一般有下面 3 步:
JVM内存布局(HotSpot JDK1.8):
JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法栈】、线程共享区域【 堆、元空间(方法区)】。
1.堆:
new 0bject()所有的对象都是存在此区域,此区域也是JVM中最大的一块区域,JVM垃圾回收就是针对此区域。
2.Java虚拟机栈:
a)局部变量表:存放方法参数和局部变量(8大基础数据类型,对象的引用 ) ;
b)操作栈:每个方法会生成一个 先进后出 的操作栈;
c)动态连接:指向运行时常量池的方法引用。
d)方法返回地址:PC寄存器的地址(返回值的地址)。
3. 本地方法栈:
和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法(native方法 c/c++实现的)栈是给本地方法使用的。
4. 程序计数器:
(用来记录线程执行的行号)
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。 正在执行
java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
5. 元空间(JDK1.8):
(JDK1.7时叫方法区(永久代实现))
DK 1.7 方法区:运行时常量信息、字符串常量池、类的元信息等;
JDK 1.8 元空间: 直接在本地内存(好处:可以不受 JVM -Xmx 大小的制约),并且将字符串常量池移动到堆。
堆划分:
新生代(复制算法):第一次创建的对象都会分配到此区域(一般占堆的1/3空间)。
老年代:新生代经历了一定的垃圾回收之后,依然存活下来的对象会移动到老年代;大对象在创建的时候也会直接放到老年代。HotSort默认的执行次数是15,经历15次GC就会从新生代转移到老年代。
为什么大对象会直接进入老年代?
核心原因是因为大对象的初始化比较耗时,如果频繁的创建和消耗会带来一定的性能开销,因此最好的实现方式是将它存入GC频率更低的老年代。
新生代区域划分:
- Eden: 80%内存。
- So:10%内存。
- S1:10%内存。
新生代内存的利用率就可以达到90%。
JVM参数调优:
(在Idea安装路径下的vmoptions里修改)
—Xss:规定了每个线程虚拟机栈的大小。
一Xmx10m:堆最大容量 mx ( memory max)
一Xms10m: 堆最小容量的设置 ms (memory start)
—D: 设置应用程序的参数。
一般将 -Xms 和 -Xmx 的大小设置相同,这样可以防止堆扩容所造成抖动。
JVM类加载机制:
(类的生命周期:)
加载:
( 类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例)
- 根据一个类的全限名来获取定义此类的二进制字节流。(实现这个代码模块就是类加载器)
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
验证:
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
有文件格式验证,元数据验证,字节码验证,符号引用验证。
准备:
准备阶段是正式为类变量分配内存并设置类变量初始值(被static修饰的变量)的阶段,这些变 量所使用的内存都将在方法区中进行分配。
解析:
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的:
- CONSTANT_Class_info
- CONSTANT_Field_info
- CONSTANT_Method_info
等类型的常量。
符号引用:
(类,方法的完全限定名)
类和结构的完全限定名,字段的名称和描述符,方法的名称和描述符。
直接引用:
(将符号引用加载到内存中(根据引用指向内存中的对象))
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
初始化:
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。(此步骤开始将执行权从JVM转移到自己写的程序,开始执行构造函数)
JVM双亲委派模型:
站在 Java 虚拟机的⻆度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的 一部分;另外一种就是其他所有的类 加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
站在Java 开发人员的⻆度来看,类加载器就应当划分得更细致一 些。 JDK 1.2 以来,Java一直保持着三层类加载器、双亲委派的类加载架构器。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求√该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
双亲委派模型的优点:
- 唯一性(父类执行加载一次)
- 安全性(会往上找而上层的类是系统提供的,避免加载自定义的类,从而一定程度上保证了安全性)
破坏双亲委派模型:
(双亲委派模型被破坏总共发生过 3 次:)
- JDK 1.2提出的双亲委派模型,为了兼容老代码,因此在 JDK 1.2的时候已经出现了破坏双亲委派模型的场景。
- 是因为双亲委派模型自身的缺点而导致的,比如在父类当中要调用子类的方法是没办法实现(如JDBC的SPI的加载是父类的加载器去请求子类的加载器去加载类)。
- 人们对于热更新的追求,导致了双亲委派模型的又一次破坏。
垃圾回收:
- 判别死亡对象(垃圾):
a.引用计数器算法:
给每个对象创建一个计数器,当有程序引用此类的时候计数器+1,不使用的时候计数器-1,当计数器为0时,则表示此对象没人用,那么就可以将它归为死亡对象,等待垃圾回收器的回收。
引用计数器算法缺点: 会有循环引用的问题。
(HotSpot默认的垃圾回收器使用的不是引用计数器算法,而是可达性分析算法。)
b.可达性分析算法:
为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC
roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
4种GC Roots的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;(因为是全局的)
- 方法区中常量引用的对象;(因为是全局的)
- 本地方法栈中JNI(Native方法)引用的对象;
垃圾回收算法:
1. 标记清除法:
最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如图
缺点:
(有内存碎片)
从图中可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
2. 复制算法:
为了解决 标记清除算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:
缺点:
这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半 (内存使用率低)。且存活对象增多的话,Copying 算法的效率会大大降低。
3. 标记整理算法:
结合了以上两个算法,为了避免缺陷而提出。标记阶段和标记清除 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:
垃圾回收器:
(7种)
Serial:(复制算法)
这个收集器是一个单线程的串行收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个 CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所 有的工作线程,直到它收集结束(Stop The World)(STW时间越短说明算法越好)。
Serial Old 收集器(老年代串行GC收集器):
(标记整理算法)
ParNew:
Serial收集器的多线程版本。
Parallel Scavenge 并行GC收集器(新生代):
(以吞吐量作为主要依据进行垃圾回收,所以需要的STW时间久)
( 复制算法)
使用场景:(适用于后端系统)
Parallel Old并行GC收集器(老年代):
(标记整理算法)
CMS收集器:
(年老代垃圾收集器 多线程的标记-清除算法)
优点:(更少的STW)CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。
使用场景:BS系统(用户交互系统)
分4个过程:
- 初始标记(需要STW)
- 并发标记
- 重新标记(需要STW,针对的是第2步用户线程产生的垃圾)
- 并发清除
G1 收集器:
(STW时间更短。效率更高)
(JDK11 默认的垃圾回收器)
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收集器两个最突出的改进是:
- 基于标记-整理算法,不产生内存碎片。
- 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
STW的时间长短是评价垃圾回收器好坏的依据
JMM(提升JVM性能)
JMM定义了一种java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
大体: JMM 就是一组规则,这组规则意在解决在并发编程可能出现的线 程安全问题, JMM (Java Memory Model)是 Java 内存模型, JMM 定义了程 序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存读 取变量这样的底层细节.并提供了内置解决方案(happen-before 原则)及其外部可使用的同步手段(synchronized/volatile 等),确保了程序执行在多线程 环境中的应有的原子性,可视性及其有序性。
JMM 规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自 己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的 主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进 行,而不能直接读写主内存中的变量(volatile 变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内 存来完成。
JMM规定的操作内存的8种方法(了解):
JMM 3大特征:
- 原子性(要么成功 要么失败)
- 可见性
- 有序性