概览:

1:源码到类文件(.java文件--->.class文件)

      Xx.java--->词法分析器---->tokens流----->语法分析器----->语法树/抽象语法树----->语义分析----->注解抽象语法树------>字节码生成器----->Xx.class文件

2:类文件到虚拟机(.class文件加载到JVM)

      a:装载

           1)通过类的全限定名获取此类的二进制流

           2)将这个字节流解析成方法区的运行时数据结构

           3)在java堆中生成一个代表这个类的java.lang.class对象,作为对方法区中这些数据的访问入口

      b:链接

             1)验证,验证文件格式,验证元数据,验证字节码,验证符号引用

             2)准备,为类的静态变量分配内存空间,并将其初始化为默认值

             3)解析,将类中的符号引用转换成直接引用

      c:初始化

             1)对类的静态变量,静态代码块执行初始化操作

     JVM基本分析

 

类装载器(ClassLoader)

分类:

       JVM基本分析 

加载原则:

          检查某个类是否加载:自底向上,从Custom ClassLoader逐层检查,只要有某个ClassLoader加载,就视为已加载此类,保证所有classLoader只加载一次

           加载顺序:自顶向下,由上层来逐层尝试加载此类

双亲委派机制:

          如果有一个类加载器在接收到加载类的请求时候,它首先将请求委托给父类加载器去完成,依次递归,如果父类加载器可以完成加载,就成功返回,只有父类无法完成加载时候自己才去加载

          优势:java类随着类加载器一起具备了带有优先级的层级关系,保证了只有一个类

          破坏:继承ClassLoader类,然后重写loadClass()方法

运行时数据区:

         JVM基本分析

          方法区(非堆)[JDK8中Metaspace  JDK6,7中Perm Space]:

                   各个线程共享的内存区域,在虚拟机启动时候创建,用于存放已被虚拟机加载的类信息,当方法区无法满足内存需求时候抛出OOM错误,还包含常量池(用于存放编译时期生成的各种

                   字面量和符号引用)

                   此时也就是JVM加载class文件的状态第一步(转换为二进制流)和第二步(转换成方法区的运行时数据结构)

                  JVM基本分析

                                         方法区中指向堆(在类中创建对象):

                                         JVM基本分析

          堆(Heap):

                        java虚拟机所管理内存中最大的一块,在虚拟机启动时候创建,被所有线程共享,对象实例和数组都在堆上分配

                        对应装载阶段的第三步(在堆中生成代表这个类的java.lang.class对象信息,作为访问方法区中这些方法的入口)

                       Java对象的内存布局:分为对象头,实例数据,对齐填充

                       JVM基本分析

                          堆指向方法区

                       JVM基本分析

 

           虚拟机栈(Stack):

                       是一个线程执行的区域,线程私有,保存着一个线程方法中的调用状态,每个被线程执行的方法都会创建一个栈帧(一个方法的运行空间),调用一个方法就会向栈中压入一个栈帧,一个方法完成就                         从栈中弹出栈帧

                        栈帧包括:局部变量表,操作数栈,指向运行时常量池的引用,方法返回地址和附加信息

                                          局部变量表:方法中定义的局部变量和方法的参数,不可直接使用,需要通过相关指令加载到操作数栈中使用

                                          操作数栈:以压栈和出栈的方式存储操作数

                                          动态连接:每个栈帧中包含一个指向运行时常量池中该栈帧所属方法的引用

                                          方法返回地址:方法开始后只有两种方式退出,一种遇到方法返回的字节码指令,一种是遇到异常,并且这个异常没有在方法体内处理

                                         栈中引用指向堆(方法中创建对象):

                                        JVM基本分析

            程序计数器(The PC Register):

                       由于java虚拟机的多线程是通过线程切换,并分配处理器执行时间的方式来实现,任意时刻一个处理器只会执行一条线程中的指令,因此为了切换后能恢复到正确位置,每个线程都有一个

                       程序计数器(线程私有),如果执行的是Native方法,则该值为空

             本地方法栈(Native Method Stacks)

                        如果当前线程执行的方法是Native的,则会在本地方法栈中执行

 

 

内存模型:

             1)堆区

                         3):Old区

                          4):Young区

                                              5):Survivor区

                                              6):Eden区,Eden:s0:s1=8:1:1

             2)非堆区 

             JVM基本分析   

             一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大对象才会被直接分配到Old区,当Eden区内存使用量达到一个设定的值,这时候就需要对Eden区域空间进行清理(GC)

              在Young区的GC称为Minor GC,Old区的GC称为Major GC,两者同时发生称为Full GC(Full GC=Minor GC+Major GC)

              survivor区域:

                             分为S0和S1区域(From区域和To区域),在同一个时间点上,S0和S1只有一个区域内有数据,每次GC,S0中对象年龄+1,存活对象复制到S1区域中,在S1区域中,如果对象

                             年龄达到设置的阈值,则分配到Old区域,未达到年龄时候分配到S0区域

               Old区域:

                             Old区域一般是年龄比较大,或者相对超过某个阈值的对象

1:为什么需要Survivor区,只保留Eden区会怎么样?

如果没有Survivor区,新对象到了Eden区域后,在Eden进行Minor GC时候存活对象达到阈值时候会放入Old区域,这样会导致Old区域很快被填满,当Old区域满了时候触发Major GC,这样会导致频繁的

Full GC,这样会影响程序的执行和响应速度,如果增大Old区域的空间,虽然降低了Full GC频率,但一旦发生Full GC执行所需时间更长,所以Survivor存在就是为了减少被送到Old区的对象,减少Full GC

 2:为什么需要两个Survivor区域

解决了碎片化,如果只有一个Survivor,当Eden将对象存放到Survivor,当下次GC时候,Survivor中回收了部分对象,Eden再次存放对象进来时候使用的空间就是不连续

 

GC算法(垃圾回收算法)

1:引用计数

              对于某个对象,只要程序中持有该对象的引用,则说明该对象不是垃圾

              缺点:无法解决循环引用

2:可达性分析

               通过GC Root开始向下寻找,看某个对象是否科大

               GC Root包括:栈,常量引用,static成员,Thread等

垃圾回收算法:

                    1:标记清除(Mark--Sweep)

                     找到内存中需要回收的对象,并且将他们标记出来,然后删除掉被标记的对象

                     缺点:产生大量不连续的内存碎片,空间碎片太多可能会导致在下次分配较大对象时候,无法找到足够的连续内存而不得不触发GC

                     2:复制算法(Copying)

                     将内存划分成两块内存区域,一次使用其中一块

                     缺点:空间利用率低

                     3:标记-整理(Mark-Compact)

                     找到内存中需要回收对象,进行标记,然后让所有存活对象向一端移动,然后清理掉边界外的对象

                     4:分代收集算法

                     Young区域,复制算法(对象在被分配后,生命周期较短,使用复制效率高)

                      Old区域,标记清除或标记整理(old区域对象存活时间长)

 

垃圾收集器:

                        JVM基本分析

                        新生代:

                        Serial:最基本,发展历史最长最久的收集器,单线程,只会使用一个CPU或者一个线程去工作,进行GC时候需要暂停其他线程,使用复制算法,适合新生代

                                     优点:简单高效,很高的单线程收集效率 

                                      缺点:需要暂停所有线程

                        ParNew:Serial的多线程版本,使用复制算法,适合新生代

                                        优点:多CPU时候,比Serial高效

                                        缺点:暂停所有线程,单CPU比Serial效率差

                        Parallel Scavenge:和ParNew类似,但是更关注系统吞吐量,吞吐量=运行用户代码的时间/(运行用户代码时间+垃圾收集时间)

                         

 

                         老年代:

                          Serial Old:单线程,采用标记--整理算法                 

                           Parallel Old:多线程,使用标记--整理算法

                           CMS:以获取最短回收停顿时间为目标的收集器,采用标记--清除算法,分为四步

                                       1:初始标记--->标记GC Root能关联到的对象,速度很快,STW(Stop The World)

                                        2:并发标记--->进行GC Roots Tracing

                                        3:重新标记---->修改并发标记因用户程序变动的内容 STW

                                         4:并发清除---->

                                         优点:并发收集,低停顿

                                          缺点:产生大量空间碎片,并发阶段会降低吞吐量

 

                            G1收集器:并行和并发,分代收集,空间整合(标记--整理算法,不会导致空间碎片),可预测的停顿(比CMS更先进地方在于能让使用者明确一个长度为M毫秒的时间片段内,消耗在垃圾收集                                                  上的时间不超过N毫秒)

                                               Java堆的内存布局和其他不同,将整个Java堆划分成大小相等的Region,新生代和老年代不再是物理隔离

                                              分为下面几步:

                                               1:初始标记--->GC Roots能关联到的对象  STW

                                               2:并发标记---->GC Roots可达性分析,找出存活对象

                                               3:最终标记---->修正并发标记阶段用户程序的并发执行导致变动的数据,STW

                                               4:筛选回收----->对各个Region的回收价值和成本排序,根据用户所期望的GC停顿时间制定回收计划

                                            

常用命令:

 jps------>查看java进程

jinfo------>实时查看和调整JVM配置参数

                   jinfo -flag MaxHeapSize 42368

                    jinfo -flag useG1GC 42368
                    jinfo -flag [+|-] 42368

jstate----->查看虚拟机性能统计信息

查看类状态信息

JVM基本分析

 

查看GC信息

JVM基本分析

 

jstack------>查看堆栈信息

JVM基本分析

jmap------->生成堆转储快照

打印堆内存相关信息

JVM基本分析

 

常用工具:jconsole(JDK自带工具),jvisualvm,Arthas,Mat,GC日志分析工具(http://gceasy.io     GCViewer)

 

JVM性能优化:

发现问题:GC频繁,死锁,OOM,线程池不够用,CPU负载过高

排查问题:打印出GC日志,查看minor gc、major gc结合工具

                   jstack查看线程堆栈信息

                   dump出堆文件,使用工具分析

                   使用jvisualvm或者arthas实时查看JVM状态

                    使用jps,jinfo,jmap命令

解决方案:适当增大内存大小,选择合适的垃圾收集器

                   使用zk,redis实现分布式锁

                    设置本地,nginx缓存,减少服务端的访问

                     后台代码优化

 

内存泄漏:对象无法得到及时回收,持续占用内存空间,从而造成内存空间浪费

内存溢出:可能是大对象导致

 

直接内存:java堆外的,直接向系统申请的空间,访问速度优于java堆

不可达对象一定被回收?

在可达性分析中不可达的对象并非一定被回收,只是暂时处于被回收状态,真正被回收需要经历两次标记过程,可达性分析中不可达对象被第一次标记并且进行一次筛选,然后被判断要

执行的对象会被放在一个队列中进行二次标记,除非这个对象与引用链上任何一个对象关联,否则会被真正回收

回收无用的类:

                    满足下列三个条件才能算是无用的类

                    1:所有该类的实例被回收

                     2:加载该类的ClassLoader被回收

                     3:该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类方法

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

相关文章:

  • 2021-12-23
  • 2022-12-23
  • 2021-10-13
  • 2022-01-26
  • 2022-01-04
  • 2022-12-23
  • 2021-05-29
  • 2021-12-09
猜你喜欢
  • 2021-08-08
  • 2021-07-02
  • 2021-12-22
  • 2021-07-24
  • 2021-06-01
  • 2021-11-12
  • 2021-10-16
相关资源
相似解决方案