JVM

(1)虚拟机把描述类的字节码加载到内存,并对数据进⾏验证、准备、解析以及类初始化,最终形成可以被虚拟机直接使⽤的java类型(java.lang.Class对象)。(2)在java.lang.ClassLoader
(3)使用
1)load(String className)根据名字加载类,返回类的实例
2)defineClass(String name,byte[] b,int off ,int len)将一个字节流定义一个类
3)findClass(String name)查找一个类
4)findLoadedClass(String name)在已加载的类中,查找一个类
5)成员变量 ClassLoader parent;每个类加载器中都有个父级类加载器

1)启动类加载器(Bootstrap),系统类rt.jar
2)扩展类加载器(Extension),jre/lib/ext(Java9更名为Pltform加载器,扩展了功能)
3)应用类加载器(APP),classpath配置的jar
4)用户自定义加载器(Plugin),程序自定义

类加载器在加载一个类的时候会先去查找是否已经加载过了,加载过就会直接返回加载过的类,没有的话会去判断他的父加载器是否为空,为空的话表示是启动类加载器,不为空的话,就先用父加载器去加载,父类加载不到的话就用当前类加载器去加载。(启动类加载器是用C语言写的,没有具体的类名,所以为空)
类会由最顶层的加载器加载,若果没有,才由下级加载器加载
委托是单向的,确保上层核心的类的正确性(优点)
但是上级类加载器加载的类,无法访问下级类加载器所加载的类(缺点)

这个机制保证了类加载的顺序,同时也失去了灵活性。
双亲委托加载扩展(使得上层类加载器加载的类能使用下层类加载器加载到的类):
1)在启动参数中添加虚拟机参数:-Xbootclasspath/a:path,将类路径配置为Bootrtrap等级
2)使用ServiceLoader.load来加载底层加载器所加载的类(SPI机制)
3)使用URLClassLoader可以在运行时增加新的classpath路径
4)使用自定义的ClassLoader,可以通过重写loadClass和findClass方法动态加载字节码,就还可以在加载字节码过程中进行修改/校验等操作

1)继承ClassLoader类
2)重写findClass(String className)
3)使用时,默认先调用loadClass(className)来查看是否已经加载过,然后通过委托双亲加载,如果没有,再通过findClass加载返回
3-1)在findClass中,首先读取字节码文件
3-2)然后,调用definClass(className,bytes,off,len)将类注册到虚拟机中
3-3)可以重写loadClass方法突破双亲加载

同一个类可以被不同层级的加载器加载,且作为两个类对待

(1)加载:类的字节码加载到内存,并将这些数据转换成方法区中的运行时数据(静态变量,静态代码块,常量池等),在堆中生成一个Class类对象代表这个类,作为方法区数据的访问入口
(2)链接:将Java类的二进制代码合并到JVM的运行状态之中
2-1)验证:字节码是否满足规范要求
2-2)准备:分配内存,常量池初始化
2-3)解析:解析类/接口/字段/方法的符号引用
(3)初始化:执行类的初始化方法。执行类的构造器的过程,类构造器方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static块)中的语句产生的

类的加载与隔离
基于类加载器,在Java9诞生了OSGI

jvm内存分两大区域:线程私有内存(程序计数器,虚拟机栈,本地方法栈),多线程共享内存(堆,方法区)
(1)程序计数器(Program Counter Register):程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。
每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。
如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写 完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。
(2)虚拟机栈(JVM Stack):一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。线程私有,可以通过调节-Xss参数设置栈的大小。
局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占 用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定 好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多 数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,知道内存不足,此时,会抛出 OutOfMemoryError(内存溢出)
每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。
(3)本地方法栈(Native Method Statck):和虚拟栈相似,只不过它服务于本地方法,线程私有。唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的。
(4)堆区(Heap):存储对象的实例和数组。虚拟机启动时创建,线程共享,占地最大。是垃圾回收的主要内存区域。可以通过设置-Xms,-Xmx设置堆的大小。
一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主 流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常
(5)方法区(Method Area):线程共享的区域。用于存储加载过的类信息、常量、静态变量、编译产生的二进制等。方法区上 执行的垃圾收集是很少的,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。
在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符”abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。
(6)直接内存(Direct Memory):直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是 JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区 (Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。 由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

(1)JVM默认参数
1)跟操作系统,物理硬件相关
2)-XX:PrintFlagsFinal显示VM参数
(2)程序启动的两类参数
1)程序参数:程序的需要,存储在main函数的形参数组中
2)虚拟机参数:更改默认配置,指导进程运行(-X参数:不标准的,不是在所有的JVM通用;-XX参数:不稳定的,容易变更(可能某些版本不支持))
(3)参数:
3-1)-Xms:堆的初始大小
3-2)-Xmx:堆的最大值(创建的对象太多或者创建超大数组)
3-3)-Xss:最大栈值(StackOverFlowError:方法调用层数太多(死循环);循环调用的方法中,局部变量占用内存太
多;OOM:线程太多)
3-4)-XX:PermSize,-XX:PermMaxSize:jdk1.7以及以前设置永久区的初始值,最大值
-XX:MetaspaceSize,-XX:MaxMetaspaceSize:jdk1.8以及以后设置元数据区的初始值,最大值
避免使用import *导致引入太多用不到的类,导致内存溢出(OOM)

(1)确定什么内存需要收集(Java基于对象的引用判定无用对象:零引用,互引用)
(2)确定回收的时机
(3)确定如何回收(速度快,影响小)

通过一系列的称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GCRoots没有任何引用链时,则证明此对象是不可用的。

(1)虚拟机中引用的对象
(2)方法区中类静态属性引用的对象
(3)方法区中常量引用的对象
(4)本地方法栈中引用的对象

(1)强引用:只要强对象还存在,对象就不会被回收
(2)软引用:有用但非必须的对象。在OOM之前,会把这些对象列为可回收对象。JDK提供了SoftReference类来实现软引用
(3)弱引用:描述非必须对象,比弱引用强度更弱些;被弱引用关联的对象在下次垃圾回收的时候就会被回收。JDK提供了WeakReference类来实现弱引用
(4)虚引用:最弱的引用关系。为一个对象设置虚引用的唯一目的就是能在这个对象被收集的时候收到一个系统通知,用于对象回收跟踪。JDK提供了PhantomReference实现虚引用。
弱引用,软引用适合用来保存可有可无的缓存数据

(1)引用计数法
1)每个对象都有一个引用计数器。有引用,计数器加一,当引用失效,计数器减一;计数器为零的对象将被回收
优点:简单,效率高
缺点:无法识别对象之间相互循环引用
(2)标记-清除
1)标记阶段:标记出所有需要回收的对象
2)回收阶段:统一回收所有被标记的对象
优点:简单
缺点:效率不高,产生内存碎片
(3)复制算法
1)将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
2)当这一块内存用完了,就将还存活的对象复制到另一块上面
3)然后把已使用过的内存空间一次清理掉
优点:简单,高效
缺点:可用内存减少,对象存活率高时复制操作较多
(4)标记-整理
1)标记阶段:标记出所有需要回收的对象
2)整理阶段:让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
优点:避免碎片产生;无需两块相同的内存
缺点:计算代价大,标记清除+碎片整理;更新引用地址
(5)分代收集(普遍采用的方法)
1)根据对象的存活周期,将内存划分为新生代和老年代
1-1)新生代:主要存放短暂生命周期的对象;新创建的对象都先放入新生代,大部分新创建的对象在第一次gc时被回收
1-2)老年代:一个对象经过几次gc仍然存活,则放入老年代;这些对象可以活很长时间,需要常驻内存的,可以减少回收的次数。
2)分代收集:只对各个年代特点采用合适的收集算法
2-1)新生代:复制算法(eden+survivor(from+to))
2-2)老年代:标记清楚/标记整理

-Xms:初始堆大小(默认:物理内存的1/64)
-Xmx:最大堆大小(默认:物理内存的1/4)
-Xmn:新生代大小
-XX:SurvivorRatio:设置eden/form/ro的比例 (默认:8:1:1)
-XX:NewRatio:设置老年代/新生代的比例(默认:2)
-XX:+PrintGC/-XX:+PrintGCDetailsdayinGC的过程信息
-XX:MaxTenuringThreshold:设置年轻代超过多少要进入老年代(默认:15)

在JVM中假设98%的时间用于GC且可用堆的大下小于2%时就会报OOM-heap异常;
堆内存参数调优:Xms=xmx=80%物理内存,xmn=1/4xmx
在tomcat运行的项目中,调整catalina.sh;运行jar包时候在运行jar包的命令后面加

(1)并行收集器:串行收集器使用一个单独的线程进行收集,GC 时服务有停顿时间
(2)串行收集器:次要回收中使用多线程来执行
(3)CMS 收集器是基于”标记—清除”算法实现的,经过多次标记才会被清除
(4)G1 从整体来看是基于”标记—整理”算法实现的收集器,从局部(两个 Region 之间)
上来看是基于”复制”算法实现的

Javap

(1)jps:查看当前系统运行的Java进程(进程号+名字;-m进程参数;-l程序的全路径;-v:传递给Java的main函数的函数参数)
(2)jstat:查看堆信息
(3)jinfo:查看虚拟机参数(也可以修改某些参数)
(4)jstack:查看线程的堆栈信息(查看线程拥有的锁,分析死锁的原因)
(5)jstatd:查看远程的Java进程
(6)jcmd:jdk7新增;可以查看Java进程,导出进程信息,执行GC等操作。

(1)jconsole(jconsole)
(2)jvisualvm(visual VM)
(3)jmc(Mission Control)
查看堆内存,线程,加载类,cpu,dump等信息,检查死锁,内存溢出的原因

jmap:统计堆内对象实例;生成堆内存dump文件
jhat:jdk9被visualVm代替,生成的分析结果默认通过7000端口访问查看,支持OQL
visual Vm分析:将dump生成的文件导入visual Vm查看(实例信息,支持OQL)

(1)是一个为应用程序植入管理功能的框架
(2)用户可以在任何应用程序中使用这些代理和服务实现管理
23,如和判断一个对象是否存活?(或者 GC 对象的判定方
法)

  1. 新⽣代对象每次经历⼀次minor gc,年龄会加1,当达到年龄阈值
    (默认为15岁)会直接进⼊⽼年代;
  2. ⼤对象直接进⼊⽼年代;
  3. 新⽣代复制算法需要⼀个survivor区进⾏轮换备份,如果出现⼤量对
    象在minor gc后仍然存活的情况时,就需要⽼年代进⾏分配担保,让
    survivor⽆法容纳的对象直接进⼊⽼年代;
  4. 如果在Survivor空间中相同年龄所有对象⼤⼩的总和⼤于Survivor空
    间的⼀半,年龄⼤于或等于该年龄的对象就可以直接进⼊年⽼代。

(1)调⽤System.gc时,系统建议执⾏Full GC,但是不必然执⾏
(2)⽼年代空间不⾜
(3)⽅法去空间不⾜
(4)通过Minor GC后进⼊⽼年代的平均⼤⼩⼤于⽼年代的可⽤内存
(5)由Eden区、From Space区向To Space区复制时,对象⼤⼩⼤于
To Space可⽤内存,则把该对象转存到⽼年代,且⽼年代的可⽤内存⼩
于该对象⼤⼩

  1. 调优时机:
    a. heap 内存(⽼年代)持续上涨达到设置的最⼤内存值;
    b. Full GC 次数频繁;
    c. GC 停顿时间过⻓(超过1秒);
    d. 应⽤出现OutOfMemory 等内存异常;
    e. 应⽤中有使⽤本地缓存且占⽤⼤量内存空间;
    f. 系统吞吐量与响应性能不⾼或下降。
  2. 调优原则:
    a. 多数的Java应⽤不需要在服务器上进⾏JVM优化;
    b. 多数导致GC问题的Java应⽤,都不是因为我们参数设置错误,
    ⽽是代码问题;
    c. 在应⽤上线之前,先考虑将机器的JVM参数设置到最优(最适
    合);
    d. 减少创建对象的数量;
    e. 减少使⽤全局变量和⼤对象;
    f. JVM优化是到最后不得已才采⽤的⼿段;
    g. 在实际使⽤中,分析GC情况优化代码⽐优化JVM参数更好;
  3. 调优⽬标:
    a. GC低停顿;
    b. GC低频率;
    c. 低内存占⽤;
    d. ⾼吞吐量;
  4. 调优步骤:
    a. 分析GC⽇志及dump⽂件,判断是否需要优化,确定瓶颈问题
    点;
    b. 确定jvm调优量化⽬标;
    c. 确定jvm调优参数(根据历史jvm参数来调整);
    d. 调优⼀台服务器,对⽐观察调优前后的差异;
    e. 不断的分析和调整,知道找到合适的jvm参数配置;
    f. 找到最合适的参数,将这些参数应⽤到所有服务器,并进⾏后续
    跟踪。

  5. Serial New收集器是针对新⽣代的收集器,采⽤的是复制算法;

  6. Parallel New(并⾏)收集器,新⽣代采⽤复制算法,⽼年代采⽤标记整理;
  7. Parallel Scavenge(并⾏)收集器,针对新⽣代,采⽤复制收集算法;
  8. Serial Old(串⾏)收集器,新⽣代采⽤复制,⽼年代采⽤标记清理;
  9. Parallel Old(并⾏)收集器,针对⽼年代,标记整理;
  10. CMS收集器,基于标记清理;
  11. G1收集器(JDK):整体上是基于标记清理,局部采⽤复制;
    综上:新⽣代基本采⽤复制算法,⽼年代采⽤标记整理算法。cms采⽤标记清理;

Original: https://www.cnblogs.com/excellencesy/p/14322264.html
Author: song.yan
Title: JVM

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/533747/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球