Review:JVM篇

https://yanglinwei.blog.csdn.net/article/details/103822256

Java内存结构

在这里插入图片描述


Java堆:是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。所有的对象实例以及数组都要在堆上分配。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

  • 直接内存:直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但这部分区域也被频繁使用,而且也可能导致OutOfMemoryError异常。这种方式是使用Native函数库直接分配堆外内存,然后通过一个存储在java堆 中的DirectByteBuffer对象作为这块内存的引用进行操作。

Java栈:描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常。


本地方法栈:本地方法栈与虚拟机栈所发挥作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务。


方法区(非堆):方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它有个别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。

  • 运行时常量池:运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在加载后进入方法区的运行时常量池中存放。
  • 程序计数器:程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的信号指示器。是虚拟机中唯一没有规定OutOfMemoryError情况的区域。

执行引擎:虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。


垃圾收集系统:垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理。


垃圾回收机制:指不定时去堆内存中清理不可达对象,这些对象并不会马上就会直接回收。 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行。开发者唯一能做的就是通过调用System.gc 方法来"建议"执行垃圾收集器。finalize()方法在Object类中定义的,因此所有的类都继承了它,在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。


Java堆的内存模型:Java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”(Garbage Collected Heap),它的内存模型如下:

在这里插入图片描述

  • 堆分为新生代 ( Young )、老年代 ( Old ),比例为1:2。
  • 新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor(s0区)、To Survivor)(s1区),比例为8:1:1

如何判断对象存活

  • 引用计数法:如果一个对象没有被任何引用指向,则可视之为垃圾,主流不选这种算法,是因为它很难解决对象之间相互循环引用的问题。
  • 根搜索算法:通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

垃圾回收算法

  • 标记清除算法:一般应用于老年代,因为老年代的对象生命周期比较长(优点:内存不足时才回收,缺点:回收时,应用需要挂起)
  • 复制算法:当from域内存不够了,开始执行GC操作,这个时候,会把from域存活的对象拷贝到to域,然后直接把from域进行内存清理。Eden区:From区:To区域 = 8:1:1。也就是说始终有90%的空间是可以用来创建对象的,而剩下的10%用来存放回收后存活的对象。万一存活对象数量比较多,那么To域的内存可能不够存放,这个时候会借助老年代的空间

在这里插入图片描述

  • 标记压缩算法:标记压缩算法的优点是解决内存碎片问题。缺点是压缩阶段,由于移动了可用对象,需要去更新引用。
  • 分代算法:这种算法,根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清除-压缩”算法进行回收。

新生代Minor GC 和老年代Full GC区别:

  • 新生代 GC(Minor GC) :当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发。
  • 老年代 GC(Major GC / Full GC) :当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代,当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载其中。

JVM常用参数配置

在这里插入图片描述

举例1设置新生代比例参数):堆内存初始化值20m,堆内存最大值20m,新生代最大值可用1m,eden空间和from/to空间的比例为2/1

-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC

举例2(设置新生代与老年代比例参数):堆内存初始化值20m,堆内存最大值20m,新生代最大值可用1m,eden空间和from/to空间的比例为2/1,新生代和老年代的占比为1/2 。

-Xms20m -Xmx20m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC-XX:NewRatio=2

内存溢出与内存泄漏

  • 内存溢出:内存溢出是指程序在申请内存时,请求的内存超过了操作系统所能提供的最大内存限制。分为堆溢出(OutOfMemoryError,由于程序试图在堆中分配更多的内存空间,而系统无法满足这个请求)和栈溢出(StackOverflowError,由于方法调用层次太深,导致调用栈空间耗尽)。(记忆:杯子水满则溢)

  • 内存泄漏:内存泄漏是指程序在运行过程中,动态分配的内存由于某种原因未能被释放,导致系统中的可用内存逐渐减少。不会直接导致程序崩溃或抛出异常。相反,它会在程序运行时逐渐占用越来越多的内存,最终可能导致程序性能下降,甚至在长时间运行后导致系统资源耗尽。


垃圾收集器分类

  • 串行和并行收集器:串行是单线程,垃圾回收停止时间长。并行收集器是多线程,适合吞吐量大的系统。
  • serial收集器:垃圾回收时,必须停止其它工作线程,直至回收完为止。-XX:+UseSerialGC
  • ParNew收集器:新生代并行,老年代串行。-XX:+UseParNewGC(ParNew收集器)、XX:ParallelGCThreads(限制线程数量)
  • Parallel收集器:类似于ParNew,扫描并压缩对,使用与科学计算。-XX:+USeParNewGC
  • CMS收集器:一种以获取最短时间回收,停顿时间为目标的收集器。XX:+UseConcMarkSweepGC
  • G1收集器:堆被划分成 许多个连续的区域(region)。采用G1算法进行回收,吸收了CMS收集器特点。支持很大的堆,高吞吐量。XX:+UseG1GC 使用G1垃圾回收器。

调优建议

  • QPS:每秒查询率。压测工具:Jemeter、Badboy。相关工具:jconsole、VisualVM
  • 初始堆值和最大堆内存内存越大,吞吐量就越高。
  • 最好使用并行收集器,因为并行收集器速度比串行吞吐量高,速度快。
  • 设置堆内存新生代的比例和老年代的比例最好为1:2或者1:3
  • 减少GC对老年代的回收

Java字节码增强:指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改。Java字节码增强主要是为了减少冗余代码,提高性能等。

  • 可以通过:BCEL、ASM、CGLIB、javassist 来动态新增、修改类(改变其结构,优点像反射,但是比反射性能高)。
  • 应用场景:AOP技术、Lombok去除重复代码插件、动态修改class文件等。
  • 举例(protobuf format的ScriptEvaluator),如下图:

在这里插入图片描述


类加载过程

在这里插入图片描述

加载:当系统运行时,类加载器将.class文件的二进制数据从外部存储器(如光盘,硬盘)调入内存中,CPU再从内存中读取指令和数据进行运算,并将运算结果存入内存中。每个类都对应有一个Class类型的对象,Class类的构造方法是私有的,只有JVM能够创建。

在这里插入图片描述

验证:确保加载的类信息符合JVM规范,没有安全方面的问题

准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配

解析:虚拟机常量池的符号引用替换为字节引用过程

初始化:执行类构造器<clinit>()方法的过程


类加载器的层次结构

在这里插入图片描述

  • 引导类加载器(Bootstrap):由C++实现,没有父类。负责将<JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中;
  • ****扩展类加载器(Extension):由Java语言实现,父类加载器为null。****负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库;
  • 系统类加载器(Application):由Java语言实现,父类加载器为ExtClassLoader。它负责加载系统类路径java -classpath或-D java.class.path指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

双亲委派模式:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

在这里插入图片描述

其实这考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer。

举例(实时的自定义类加载器加载外部URL的jar包):

在这里插入图片描述


类加载器常用方法:loadClass(String)、findClass(String)、defineClass(byte[] b, int off, int len)、resolveClass(Class≺?≻ c)


热部署和热加载

  • 热部署:服务器运行时重新部署项目,直接重新加载整个应用,更多的是在生产环境使用。
  • 热加载:在运行时重新加载class,在运行时重新加载class,更多的是在开发环境使用。

热部署的三个步骤:销毁该自定义ClassLoader、更新class类文件、创建新的ClassLoader去加载更新后的class类文件**。**

猜你喜欢

转载自blog.csdn.net/qq_20042935/article/details/134580128