两个月,我也要进阿里!——(1)JVM长篇讲解

最近身边朋友很多去了美团、dd、阿里、拼多多等大厂,

在这里插入图片描述嫉妒使我丑陋

于是小祥给自己下了个目标——两个月,我要进阿里!

在这里插入图片描述

在这里插入图片描述

并且还和之前同事互勉 了一把!

在这里插入图片描述

不多废话了,一天一个知识点开始

1.JVM概述

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

2.JVM生命周期

在这里插入图片描述

生命周期一共分为加载、连接、初始化、使用、卸载五大阶段。

1.加载:类的加载过程主要完成三件事。
(1)通过类的全限定名来获取定义此类的二进制字节流。
(2)将这个类字节流代表的静态存储结构转为方法区的运行时数据结构。
(3)在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。这个过程主要是类加载器完成的。

2.连接:这个过程分三个阶段(校验,准备,解析)完成。
首先是校验,此阶段主要校验class文件包含的信息是否符合jvm的规范。具体的校验通过对文件格式,元数据,字节码,符号引用验证来完成。
然后是准备,此阶段为类变量分配内存,并将其初始化为默认值。
最后是解析,即把类型中的符号引用转换成为直接引用。具体的解析有4种,1.类或接口的解析,2.字段解析,3.类方法解析,4.接口方法解析。完成这3个阶段就完成了类的连接。

3.初始化:即执行类的构造器方法的过程。有5种方法可以完成初始化:1.调用new方法,2.使用Class类的newInstance方法(反射机制),3.使用Constructor类的newInstance方法(反射机制),4.使用Clone方法创建对象,5.使用(反)序列化机制创建对象

4.使用:完成类的初始化后,就可以对类进行实例化,在程序中进行使用了

5.卸载:当类被加载,连接和初始化后,它的生命周期就始了,当代表类的class对象不在被引用时,class对象就会结束生命周期,类在方法区内的数据就会被卸载。因此一个类何时结束生命,取决于代表它的class对象何时结束生命。

3.JVM内存结构

                                 JVM运行机制图

在这里插入图片描述
方法区和对是所有线程共享的内存区域
本地方法栈程序计数器运行是线程私有的内存区域。

(1)Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

(2)方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器

(3)JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

(4)本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

对于JVM内存,程序员只需要关注对象去哪儿——堆内存(包括新生代老年代,新生代又分为Enden区和两个Survivor区),堆内存用于存放对象,合理分区主要是为了提高垃圾回收效率,新创建的对象会在Enden区,经历Minor GC后会到Survivor区,经历一般15次GC还存活的话会进入老年代。

函数如何调用——栈内存用于运行线程,它们包含了方法里的临时数据、堆里其它对象引用的特定数据。

类去哪儿——方法区用来存储类型信息(运行时常量和静态变量)和方法代码和构造函数代码,通常也叫永久代,JDK8用元空间代替永久代

JVM资源一般分为两种CPU和内存,CPU代表着线程栈,内存代表着堆,而往往生产环境JVM问题一般都是这两种资源不足导致的,当线程栈使用不当的时候,通常会CPU爆满,当堆内存使用不当的时候,通常会出现内存溢出。

4.类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库

扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器

双亲委派模型是一种组织类加载器之间关系的一种规范,他的工作原理是:如果一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,才会交给子类加载器去尝试加载.
在这里插入图片描述
如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证,而且如果不使用这种双亲委派模型将会给虚拟机的安全带来隐患。所以,要让类对象进行比较有意义,前提是他们要被同一个类加载器加载。

5.Java对象结构

Java对象组成:对象头、实例数据、对齐填充。

对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。

实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)

对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)

6.Java对象创建过程

1.JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲)

2.为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)”

3.将除对象头外的对象内存空间初始化为0

4.对对象头进行必要设置

7.GC算法及垃圾回收器

常见的垃圾回收算法:标记-清除,复制,标记-压缩,分代收集

常用的垃圾回收集器:Serial收集器,ParNew收集器,Paralle收集器,Paralle Old收集器,Cms收集器,G1收集器

GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。
Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用。 垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。
垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。
在这里插入图片描述

下面讲解几个垃圾回收算法

  1. 引用计数算法
    给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不再被使用的,垃圾收集器将回收该对象使用的内存。引用计数算法实现简单,效率很高,微软的COM技术、ActionScript、Python等都使用了引用计数算法进行内存管理,但是引用计数算法对于对象之间相互循环引用问题难以解决,因此java并没有使用引用计数算法。

  2. 根搜索算法:
    通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。

在Java语言里,可作为GC Roots对象的包括如下几种:
(1)System Class,像rt.jar里面的java.util.*

(2)Thread,开始状态的线程

(3)虚拟机栈(栈桢中的本地变量表)中的引用的对象

(4)方法区中的类静态属性引用的对象

(5)方法区中的常量引用的对象

(6)本地方法栈中JNI的引用的对象

  1. 标记-清除算法
    . 在这里插入图片描述
    最基础的垃圾收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。
    标记-清除算法的缺点有两个:
    首先,效率问题,标记和清除效率都不高。
    其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

  2. 复制算法: 将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效
    复制算法的缺点显而易见,可使用的内存降为原来一半

  3. **标记-清除-整理算法:**标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。
    标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。
    复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高。

  4. 分代算法:
    根据对象的存活周期的不同将内存划分为几块。一般把java堆分为新生代老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须使用==“标记-整理”算法==进行回收。

相关的JVM参数:
-Xms / -Xmx — 堆的初始大小 / 堆的最大大小
-Xmn — 堆中年轻代的大小
-XX:-DisableExplicitGC — 让System.gc()不产生任何作用
-XX:+PrintGCDetails — 打印GC的细节
-XX:+PrintGCDateStamps — 打印GC操作的时间戳
-XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
-XX:NewRatio — 可以设置老生代和新生代的比例
-XX:PrintTenuringDistribution — 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
-XX:TargetSurvivorRatio:设置幸存区的目标使用率

8.JVM调优

1、初始化内存最大内存尽量保持一致,避免内存不够用继续扩充内存。最大内存不要超过物理内存,例如内存8g,你可以设置最大内存4g/6g但是不能超过8g否则加载类的时候没有空间会报错。

2、gc/full gc频率不要太高、每次gc时间不要太长、根据系统应用来定。

摘录GC日志一部分(前部分为年轻代gc回收;后部分为full gc回收):
2020-05-22T10:43:18.093+0800: 25.395: 
[GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs] 
2020-05-22T10:43:18.160+0800: 25.462: 
[Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K)
 [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]

通过上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen属于Parallel收集器。其中PSYoungGen表示gc回收前后年轻代的内存变化;ParOldGen表示gc回收前后老年代的内存变化;PSPermGen表示gc回收前后永久区的内存变化。young gc 主要是针对年轻代进行内存回收比较频繁,耗时短;full gc 会对整个堆内存进行回城,耗时长,因此一般尽量减少full gc的次数

常用调优工具:

jps:查看所有的jvm进程,包括进程ID,进程启动的路径等等。

jstack:观察jvm中当前所有线程的运行情况和线程当前状态。

jstat:利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对进程的classloader,compiler,gc情况;

jmap:监视进程运行中的jvm物理内存的占用情况,该进程内存内,所有对象的情况,例如产生了哪些对象,对象数量;

jinfo:观察进程运行环境参数,包括Java System属性和JVM命令行参数。

9JVM常会问的面试题

1.jvm的初始化步骤?类的加载过程
2.jvm的内存结构?每块内存分别存的什么信息
3.双亲委派模型
4.GC算法,如何调优
5.FULL GC的条件

在这里插入图片描述

看完了铁汁麻烦给点个赞👍!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值