1. jvm的组成:

类加载器(ClassLoader)
运行时数据区(Runtime Data Area)
执行引擎(Execution Engine)
本地库接口(Native Interface)
image.png
首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,
运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),
将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能

1. 运行时数据区:

image.png

  1. 程序计数器:记录线程执行到哪了(如果是Native方法,计数器的值为空)
  2. java虚拟机栈:(java方法)栈帧先进后出

image.pngimage.png
方法执行的过程:
方法被调用前:创建栈帧
方法执行:栈帧入栈
方法执行后:栈帧出栈
Java虚拟机栈描述的是Java方法的内存模型:每个方法在执行时都会创建一个栈帧,存储*局部变量表、操作数栈、动态链接、方法出口信息
,每一个方法从调用到结束,就对应这一个栈帧在虚拟机栈中的进栈和出栈过程。局部变量表保存了各种基本数据类(int、double、char、byte等)、对象引用(不是对象本身)和returnAddress类型(指向了一条字节码地址)。

  • 线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError;(深度溢出sof)
  • 虚拟机栈扩展时无法申请到足够的内存,抛出OutOfMemoryError。(内存溢出oom)
  1. 本地虚拟机栈:

上述虚拟机栈为JVM执行Java方法服务,本地方法则为执行Native服务。其他和虚拟机栈类似,也会抛出StackOverflowError、OutOfMemoryError。

2. Java堆

常说的“栈内存”、“堆内存”,其中前者指的是虚拟机栈,后者说的就是Java堆了。Java堆是被线程共享的。在虚拟机启动时被创建。Java堆是Java虚拟机所管理的内存中最大的一块。Java堆的作用是存放对象实例,Java堆可以处于物理上不连续的内存空间中,只要求逻辑上连续即可。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作”GC堆”,从内存回收的角度看,现在收集器都基本采用分代回收的算法 所以Java堆呢还可以细分为:新生代、老年代。 在细致一点的有:Eden空间、From Survivor空间、To Survivor空间。
image.png

image.png

3. 方法区

也被称为永久代 1.7
Perm : 永久代
也被称为元数据空间 1.8
MetaSpace: 元数据空间
是线程共享的区域。存储已被虚拟机加载的类信息 、 常量、 静态变量、即使编译器编译后的代码等数据。方法区无法满足内存分配需求时,抛出OutOfMemoryError。JVM规范被没要求这个区域需要实现垃圾收集,因为这个区域回收主要针对的是类和常量池的信息回收,回收结果往往难以令人满意。
运行时常量池:是方法区的一部分。Java语言不要求常量只能在编译期产生,换言之,在运行期间也能将新的常量放入。
方法区空间大小设置:
-XX:PermSize 方法区的初始值大小
-XX:MaxPermSize 方法区的最大值
1.8之后设置:
-XX:MetaspaceSize 方法区的初始值大小
-XX:MaxMetaspaceSize 方法区的最大值
Cglib动态代理可以动态创建代理类,这些代理类的Class会动态的加载入内存中,存入到方法区。所以当我们把方法区内存调小后便可能会产生方法区内存溢出,1.8之前的JDK我们可以称方法区为永久代 :PermSpace 1.8之后方法区改为MetaSpace 元空间。

2.GC垃圾回收机制

简介:
说起垃圾收集(Garbage Clollection , GC),大家肯定都不陌生,目前内存的动态分配与内存回收技术已经非常成熟,那么我们为什么还要去了解GC和内存分配呢?原因很简单:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时。 我们就需要对这些自动化的技术实施必要的监控和条件。
在我们的java运行时内存当中,程序计数器、Java虚拟机栈、本地方法栈 3个区域都是随线程生而生,随线程灭而灭,因此这个区域的内存分配和回收都具备了确定性,所以在这几个区域不需要太多的考虑垃圾回收问题,因为方法结束了,内存自然就回收了。但Java堆不一样,它是所有线程共享的区域,我们只有在程序处于运行期间时才能知道会创建哪些对象,这个区域的内存分配和内存回收都是动态的,所以垃圾收集器主要关注的就是这部分的内存。

1. 判断对象已死吗

如何判断对象是否死亡,主要有两种算法: 引用计数法可达性分析算法

3. 垃圾收集算法

1. 标记-清除算法

标记—清除算法是最基础的收集算法,过程分为标记和清除两个阶段,首先标记出需要回收的对象,之后由虚拟机统一回收已标记的对象。这种算法的主要不足有两个:
1、效率问题,标记和清除的效率都不高
2、空间问题,对象被回收之后会产生大量不连续的内存碎片,当需要分配较大对象时,由于找不到合适的空闲内存而不得不再次触发垃圾回收动作
image.png

  1. 复制算法

为了解决效率问题,复制算法出现了。算法的基本思路是:将内存划分为大小相等的两部分,每次只使用其中一半,当第一块内存用完了,就把存活的对象复制到另一块内存上,然后清除剩余可回收的对象,这样就解决了内存碎片问题。我们只需要移动堆顶指针,按顺序分配内存即可,简单高效。但是算法的缺点也很明显:
1、它浪费了一半的内存,这太要命了。
2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。


2. 复制算法

为了解决效率问题,复制算法出现了。算法的基本思路是:将内存划分为大小相等的两部分,每次只使用其中一半,当第一块内存用完了,就把存活的对象复制到另一块内存上,然后清除剩余可回收的对象,这样就解决了内存碎片问题。我们只需要移动堆顶指针,按顺序分配内存即可,简单高效。但是算法的缺点也很明显: 1、它浪费了一半的内存,这太要命了。 2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。
这种收集算法经常被采用到新生代,因为新生代中的对象 绝大部分都是 朝生夕死,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上,最后清理Eden和刚才用过的Survivor空间,HotSpot默认的空间比例是 8:1 ,如图:
分配担保: 我们没办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够时,需要依赖其他内存(这里指老年代)进行分配担保。
JVM虚拟机 - 图8

3. 标记-整理算法

根据老年代的特点,有人提出了另一种改进后的“标记—清除”算法:标记—整理算法。标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
不难看出,标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价,可谓是一举两得。
image.png

4. 分代收集算法

现代商业虚拟机垃圾收集大多采用分代收集算法。主要思路是根据对象存活生命周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,然后根据各个年代的特点采用最合适的收集算法。新生代中,对象的存活率比较低,所以选用复制算法,老年代中对象存活率高且没有额外空间对它进行分配担保,所以使用“标记-清除”或“标记-整理”算法进行回收
image.png
现代商业虚拟机垃圾收集大多采用分代收集算法:
image.png
image.png

4. 垃圾收集器 HotSpot JDK1.8(自己阅读)

垃圾回收时会出现:暂时性的停止
image.png

并行收集器

并发清除收集器

串行收集器

(搭配使用)
image.png

5. JVM加载机制:

1. 概括:

  • 负责将 Class 加载到 JVM 中
  • 审查每个类由谁加载(父优先的等级加载机制)
  • 将 Class 字节码重新解析成 JVM 统一要求的对象格式

    2. 类加载的时期:

    1.在遇到 new、putstatic、getstatic、invokestatic 字节码指令时,如果类尚未初始化,则需要先触发初始化。 new User(); 当对static属性进行获取或赋值的时候
    2.对类进行反射调用时,如果类还没有初始化,则需要先触发初始化。
    3.初始化一个类时,如果其父类还没有初始化,则需要先初始化父类。
    4.虚拟机启动时,用于需要指定一个包含 main() 方法的主类,虚拟机会先初始化这个主类。

    3. 加载类的过程

    类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)
    image.png
    加载:(使用IO方式去读取.class文件)二进制流
    基本概念:该过程完成查找并加载类的class文件。该class文件可以来自本地磁盘或者网络等。Java规范中并没有规定 Class 对象的存放位置,对于Hot Spot 虚拟机来说,Class 对象虽然是对象,但却是存放在方法区中。
    验证:
    基本概念:确保类型的正确性,比如class文件的格式是否正确、语义是否符合语法规定、字节码是否可以被JVM安全执行等
    1. 验证魔数 0xcafebabe 证明是.class
    2. 验证虚拟机版本 1.7/1.8
    3. 语法是否符合规定
    验证总体上分为4个阶段: 文件格式验证、元数据验证、字节码验证、符号引用验证。
    准备:
    方法区:
    .class 对象 static
    基本概念:为类的静态变量分配内存,并赋初值。基本类型的变量赋值为初始值,比如int类型的赋值为0,引用类型赋值为null。
    static String str = “小明”;
    static int i = 255
    static String str = null
    static int i = 0
    “准备”阶段是正式为类变量(仅仅是类变量,即 static 修饰的变量)分配内存并设置类变量初始值(除了 final 变量初始值是数据类型的零值,并不是类构造器 方法中的初始值)的阶段,这些变量所使用的内存都将在方法区中进行。
    解析:
    基本概念:将符号引用转为直接引用。比如方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接引用方法。
    ”解析“阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,
    主要针对 类或接口、字段、类方法、接口方法、方法类型、方法句柄 和 调用限定符 7类符号引用进行。
    初始化:
    基本概念:初始化,则是为标记为常量值的字段赋值的过程。换句话说,只对static修饰的变量或语句块进行初始化。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
    class Person {
    static String str = “小明”
    static int i = 1
    static {
    // 在初始化的时候执行一次
    }
    }
    new Person()
    Person.class
    main

    6. 类加载器

    “将class文件加载进JVM的方法区,并在方法区中创建一个java.lang.Class对象作为外界访问这个类的接口。”实现这一动作的代码模块称为类加载器。
    image.png

    7. 双亲委派模型

    保证类被同一个类加载器对象
    image.png破坏双亲委派机制:热启动, 插件

    8. JVM性能监控与故障处理工具

    有没有解决过生产环境的问题?
    内存溢出问题的排查: jmap进行堆内存拍快照,使用可视化工具Jprofiter进行

    JProfiler:虚拟机堆转储快照分析工具

    jstack:java堆栈跟踪工具

    image.png
    image.png
    CPU使用率百分百的排查:
    排查死锁 :
    image.png

image.png
2.image.png

  1. image.png

9. JVM调优小结

JVM参数调优实际上没有具体的答案,要根据不同的实战场景进行对应的设置,还需要不断的调试和磨合,设置的不好,JVM不断执行Full GC,导致整个系统变得很慢,网站停滞时间能达10秒以上,这种情况如果没隔几分钟就来一次,自己都受不了。这种停滞在测试的时候看不出来,只有网站pv达到数十万/天的时候问题就暴露出来了。
所以对于JVM调优的话术,我们可以这么说,结合我们公司之前的经验,对于JVM调优我们可以从下面方向进行分析:
互联网项目 64 Linux Centos6.5 8GB

1:如果服务器硬件性能足够,建议采用64位操作系统,Linux下64位的jdk比32位jdk要慢一些,但是吃得内存更多,吞吐量更大。

2:XMX和XMS设置一样大,MaxPermSize和MinPermSize设置一样大,这样可以减轻伸缩堆大小带来的压力。

3: -Xmn年轻代的大小, 并行:吞吐量 并发:低延迟
-XX:NewRadio年轻代和年老代的比值,
Sun建议 年轻代与年老代的比例:3/8


4: 垃圾回收器的选择:

响应时间优先的应用:并发收集器 ParNew + CMS(老年代) 或者 G1
+ Serial Old (STW)

吞吐量优先的应用:并行收集器 Parallel Scavenge + Parallel Old
使用并发收集器,肯定就是追求最小的响应时间,所以应该减少年轻代,加大年老代,这样可以利用年老代的并发CMS收集器来减少响应时间。
使用并发收集器,一般是最求吞吐量优先的应用,会加大年轻代,缩小年老代。这样可以在年轻代回收掉大部分短期对象,减少中期对象,而老年代只存少部分长时间存活的对象。
(年老代的并发收集器使用标记,清除算法,所以不会对堆进行压缩.当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象.但是,当堆空间较小时,运行一段时间以后,就会出现”内存碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记,清除方式进行回收.如果出现”碎片”,可能需要进行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩.
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

5:调试的时候设置一些打印参数
如: -XX:+PrintClassHistogram
-XX:+HeapOnOutOfMerroryError
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:log/gc.log
这样可以让jvm虚拟机打印出类加载的情况,堆转储的快照,GC的详细回收日志 等等日志信息

6:当系统发生停顿的时候可能是GC的问题也可能是程序的问题,还有内存飙高,系统响应慢的时候,多利用jvm的监控工具实时注意jvm虚拟机的情况。 如可以通过jmap转储堆内存情况,通过jstack可以打印出线程的快照,在通过JProfiler或者JVisoulVM的分析工具进行分析。 — 这里可以加入JVM调优案例

7:仔细了解自己的应用,如果用了缓存,那么年老代应该大一些

8:垃圾回收时promotion failed是个很头痛的问题,一般可能是两种原因产生
第一个原因是救助空间不够,救助空间里的对象还不应该被移动到年老代,但年轻代又有很多对象需要放入救助空间;第二个原因是年老代没有足够的空间接纳来自年轻代的对象;这两种情况都会转向Full GC,网站停顿时间较长。
第一个原因我的最终解决办法是去掉救助空间,
设置-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0即可

第二个原因我的解决办法是设置CMSInitiatingOccupancyFraction为某个值(假设70),这样年老代空间到70%时就开始执行CMS,年老代有足够的空间接纳来自年轻代的对象。