MathDemo.java
JVM(Java Virtual Machine)是Java虚拟机的缩写,是Java程序运行的环境。JVM是一种能够解释Java字节码并将其转换为机器指令的软件。
ProcessOn Flowchart
首先通过编译器把 Java 代码转换成字节码
类加载器(ClassLoader) 再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内
而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行
因此需要特定的命令解析器执行引擎(ExecutionEngine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他 语言的本地库接口(Native Interface)来实现整个程序的功能。
两个子系统为Class loader(类装载)、 Execution engine(执行引擎);
两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
JVM具有以下重要内容:
1.类装载子系统(ClassLoader):
负责从系统文件或网络中加载class信息到内存中。根据给定的全限定名类名(如: java.lang.Object)来装载class文件到Runtime data area(运行时数据区)中的method area(方法区)。
2.字节码执行引擎:
将字节码转换为机器指令并执行,负责执行那些被加载的类的方法。
当代码在运行时,执行引擎会首先去找到方法区中被加载的类的类型信息,然后再找到对应的方法进行执行。
3.运行时数据区:
这是JVM的重要组成部分,它包含了程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区等。
运行时数据区中,又分为线程私有和线程共享的区域
线程共享区域包含: 堆区和方法区
线程私有区域包含: 栈、本地方法栈、程序计数器
6.垃圾回收器:自动回收不再使用的对象,以便释放内存空间。
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初 始化,最终形成可以被虚拟机直接使用的java类型。
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也 是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时 候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊 的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种 :
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用 类装载器加载对应的类到jvm中
2.显式装载, 通过class.forname() 等方法,显式加载需要的类
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证 程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候 才加载。这当然就是为了节省内存开销。
主要有一下四种类加载器:
1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被 java程序直接引用。
2. 扩展类加载器(extensions class loader): 它用来加载 Java 的扩展库。 Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找 并加载 Java 类。
3. 系统类加载器(system class loader ):它根据 Java 应用的类路径 (CLASSPATH )来加载 Java类。一般来说,Java 应用的类都是由它来 完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取 它。
4. 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
类装载分为以下 5个步骤:
加载:根据查找路径找到相应的 class 文件然后导入;
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为 一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作。
在介绍双亲委派模型之前先说下类加载器。
对于任意一个类,都需要由加载它的 类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一 个独立的类名称空间。
类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。
双亲委派模型:
在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。
如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题
比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。
双亲委派模型可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。
这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加 的指针指向这个新的内存,使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来 的对象也会相应的改变。
深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。
1,当我们某个java程序,启动main方法以后,JVM的线程栈,会开辟一个空间,用来存放这个程序运行相关的局部变量
2,当main方法开始运行,这个线程栈会为main方法创建一个区域,称为栈帧,这个里面存放了main方法内部的局部变量,比如上面的mathDemo
3,当执行到compute方法之后,同样的也会为这个方法创建一个栈帧,存放compute方法对应的局部变量,也就是上面的 a,b,c
4,线程栈的数据结构,符合栈的特点,先进后出,main方法在下面,compute方法在上面,当compute方法使用完以后,先被销毁
5,为每个方法生成的栈帧中,其实还分成4个区,局部变量表,操作数栈,动态链接,方法出口,这四个区域其实就是跟代码执行的过程相关联
程序计数器是为了记录某个线程执行的步骤,因为Java是多线程的,如果某个线程A在执行的时候,被别的线程B抢先执行,那么,当再次执行线程A的时候,通过程序计数器,就不会再去重复执行线程A之前执行过的代码,保证代码能够正常执行。
我们编译好的字节码文件,比如MathDemo.class,是存放在方法区的,这个字节码文件由字节码执行引擎来执行,所以,程序计数器其实是由字节码执行引擎来修改的
操作数栈就是Java代码在运行过程中,存放一些值、运算结果的临时内存空间
定义是:将符号引用转变为直接引用。底层主要是一些c++实现的语法
这里要去理解什么是符号,什么是符号引用、直接引用
符号其实就是我们Java中的一些方法名、变量等
符号引用指的是 :当程序运行到调用某些方法的时候,会去找对应方法的代码(也就是字节码),这些代码存放在内存区域里面的方法区,
在方法区存放的具体位置,就是直接引用,这个被调用的方法名,就是符号引用。
所以,动态链接里面存放的主要就是一些方法区中的代码地址。
主要是记录这个方法执行完以后,后面应该继续执行什么内容。
mathDemo是main方法中的一个局部变量,它的值是new的一个对象,new出来的对象其实是放在堆中的,所以main方法中的局部变量其实就是存放的一个引用地址,地址执行堆中的实际的对象。
所以在栈中,就存放了很多的一些地址引用,这些地址引用指向堆中的具体对象
方法区其实主要存放的是常量、静态变量、类信息
比如之前类中的initData就是一个常量,user就是一个静态变量,这个user其实也是一个地址,指向堆中的new User()对象。
所以这个方法区中,其实也是存放了一些地址引用,这些地址也是指向堆中的具体对象
我们通过新建一个线程去调用start()方法的时候,查看start()方法的源码发现,在代码中存在一个start0()这个方法
这个start0()其实就是一个本地方法,它是由native修饰的,表示底层由C或者C++实现,那么这些代码在执行的时候,在内存中也需要一些空间来存放对应的一些代码数据,那这些空间其实就是本地方法栈。
GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问 题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,
Java 提供的 GC 功能可以自动监测 对象是否超过作用域从而达到自动回收内存的目的
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及 使用情况。
通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式 确定哪些对象是"可达的",哪些对象是"不可达的"。
当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
可以。
程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不 保证GC一定会执行。
垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。
一般有两种方法来判断:
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被 回收了。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。
如果你仔细查看垃圾收集器的输出信息,就会发现永久代 也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
GC Roots就是将Eden区的对象作为根节点,然后依次去分析这个对象中所有用到的其他对象,只要是这个对象还在被引用,就会被标记为非垃圾对象,其余的未标记的就是垃圾对象,这种算法就是可达性分析算法。
GC Roots 根节点:线程栈的本地变量,静态变量,本地方法栈的变量等 ,就比如示例类中的 mathDemo,user等
可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等
安装 Visual GC插件 ,可以观察内存运行的情况
VisualVM界面,工具》插件》已下载》添加插件,选择已下载的插件,然后点击安装,直到安装完成。
然后重启VisualVM
然后测试如下代码:
结合图形,了解内存中堆的结构
先说一下刚才的循环案例,最终报了一个内存溢出的错误 OutOfMemoryError
为什么会报OOM,老年代放满了老年对象后,在报OOM之前,会做一个full gc ,也就是全局的一个垃圾回收,但是这些对象全部都在使用,没法回收,老年代没有空间,所以最终才报了OOM错误
所以,可以说,调优的目的是减少gc,减少full gc ,但是其实最终的目的是减少STW(stop the world)
stw表示停止用户线程,只要做gc后,JVM就会进行STW机制,停掉用户线程
比如,用户在进行下单的操作,点击按钮,那这个操作其实都是在后台有对应的代码执行逻辑,这些都可以成为是用户线程,gc是Java程序的后台线程,那么gc在执行的时候,会停掉用户的线程,也就是STW,那用户下单的时候,如果Java后台正好执行gc了,用户会有卡顿的感觉,这种现象对于用户的体验是不太友好的,所以要去减少STW的出现,也就是调优
如果不做STW机制,设想一下,当系统在做gc的时候,程序继续运行,那么会出现什么现象?
gc在做GC Roots 根节点判断的时候,程序继续运行,当程序执行完以后,这个线程中对应的其他的变量、栈空间等内容会全部被清空,因为这个时候线程已经随着程序执行结束而消失了,那么刚才gc 判断出来的 GC Roots 根节点的状态也就发生了改变,原来是非垃圾,现在变成垃圾对象,那不可能让gc再次去做GC Roots 这个工作,所以,就设计出了这种机制,当系统在做gc的时候,将用户线程先停止,然后当gc完成后,再次运行
问题:电商项目上线之后,平时没问题,流量加大后,会频繁的出现full gc,频繁的full gc会让用户体验很差,怎么解决?
ProcessOn Flowchart