JVM 学习笔记

*JVM 学习笔记*

    • Jdk和Jre和JVM的区别
    • JVM的内存区域
      • JVM 运行时数据区
        • 基本组件一
        • 基本组件二
      • 运行流程
      • Native关键字
      • 方法区
      • 栈帧
      • 本地方法栈
      • Java堆
      • 栈、堆、方法区的交互
      • 堆栈的区别
      • Java会存在内存泄漏吗?
      • 直接内存
      • 深拷贝和浅拷贝
    • 类的加载
      • 类加载器
      • 类装载方式(两种) :
      • 类装载的执行过程
      • 四种类加载器
      • 三层的ClassLoader:
      • 我们写的Hello.java编译成的Hello.class文件,它是如何被加载到JVM中的呢?
      • 双亲委派机制
      • JVM字节码执行引擎
      • 沙箱安全机制
        • 组成沙箱的基本组件:
    • GC(垃圾回收)
      • 垃圾收集系统
      • GC分区
      • 分代收集算法
      • 新生区
      • 老年期
      • 永久区(元空间)
      • JVM中的永久代中会发生垃圾回收吗
      • 怎么判断对象是否可以被回收?(引用计数器法)(可达性分析算法)
      • JVM 垃圾回收算法有哪些?
      • GC复制算法
      • GC标记清除-压缩算法
      • Minor GC、Major GC、Full GC是什么
      • Minor GC、Major GC、Full GC区别及触发条件
      • 为什么新生代要分Eden和两个 Survivor 区域?
      • 为什么要这样分代?
      • JVM 有哪些垃圾回收器?
      • 总结(GC算法)
      • 简述java内存分配与回收策率
    • JVM性能调优
      • 为什么要调优?
      • 调优指标
      • JVM调优工具
      • JVM调优经验
    • 引用

Jdk和Jre和JVM的区别

JVM 学习笔记_第1张图片

JVM的内存区域

JVM 学习笔记_第2张图片

JVM 运行时数据区

JVM 学习笔记_第3张图片

  • 方法区和堆 所有数据共享
  • 栈(虚拟机栈-本地方法栈)、计数器线程隔离

基本组件一

  • 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
为什么要线程计数器?因为线程是不具备记忆功能
  • Java 虚拟机栈(Java Virtual Machine Stacks):每个方法在执行的同时都会在Java 虚拟机栈中创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
栈帧就是Java虚拟机栈中的下一个单位
  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 CC++ 的代码
  • Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

基本组件二

JVM包含两个子系统和两个组件: 两个子系统为Class loader(类装载)、Execution engine(执行引擎); 两个组件为Runtime data area(运行时数据区,即最大的方块)、Native Interface(本地接口)。

  • Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到 - - Runtime data area中的method area。
  • Execution engine(执行引擎):执行classes中的指令。
  • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
  • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

运行流程

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

Native关键字

  • Function:
    – 来调用底层C、C++的库
  • Example:
    – private native void start();
    – 进入本地方法栈
    – 调用本地方法本地接口JNI

方法区

  • 所有定义的方法的信息在此区域
  • 方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 它有个别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。
静态变量、常量、类信息(构造函数,接口代码)、运行时的常量池存在方法区中,
但是实例变量存在堆内存中,和方法区无关

  • Function:
  1. 主管程序的运行,生命周期和线程同步;
  2. Java虚拟机是线程私有的,它的生命周期和线程相同。
  3. 线程结束,栈内存释放,不存在垃圾回收问题
  4. 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  5. 栈:先进后出,后进先出
  6. 虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,出口等。

栈帧

  • 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)
  • 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
  • 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
  • 出口:出口是什呢,出口正常的话就是return 不正常的话就是抛出异常落

本地方法栈

  • 本地方法栈很好理解,他很栈很像,只不过方法上带了 native 关键字的栈字
  • 它是虚拟机栈为虚拟机执行Java方法(也就是字节码)的服务方法
  • native关键字的方法是看不到的,必须要去oracle官网去下载才可以看的到,而且native关键字修饰的大部分源码都是C和C++的代码。
  • 本地方法栈中就是C和C++的代码

Java堆

  • Java堆(Java Heap)是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。
  • 此内存区域的唯一目的就是存放对象实例。
  • 在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
  • java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。
  • 从内存回收角度来看java堆可分为:新生代和老生代。
  • 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
  • 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。
  • 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

栈、堆、方法区的交互

JVM 学习笔记_第4张图片

  • Person:存放在元空间,也可以说方法区;
  • p:存放在Java栈的局部变量表中;
  • new Person():存放在Java堆中;
  • 栈指向堆是什么意思?就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址。

堆栈的区别

JVM 学习笔记_第5张图片
静态变量放在方法区, 静态的对象还是放在堆。

Java会存在内存泄漏吗?

  • 内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。

  • 理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。

  • 但是,即使这样,Java也还是存在着内存泄漏的情况。

  • Java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露。

  • 尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。

直接内存

直接内存是基于物理内存和Java虚拟机内存的中间内存。

深拷贝和浅拷贝

  • 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
  • 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
  • 浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
  • 深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。

类的加载

类加载器

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。

  • Java中的所有类,都需要由类加载器装载到JVM中才能运行。
  • 类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。
  • Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。

类装载方式(两种) :

  • 隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中
  • 显式装载, 通过class.forname()等方法,显式加载需要的类

类装载的执行过程

类装载分为以下 5 个步骤:

  • 加载:根据查找路径找到相应的 class 文件然后导入;
  • 验证:检查加载的 class 文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
  • 初始化:对静态变量和静态代码块执行初始化工作。

四种类加载器

  • 启动类加载器(Bootstrap ClassLoader): 用来加载java核心类库,无法被java程序直接引用。
  • 扩展类加载器(extensions class loader): 它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
  • 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。

三层的ClassLoader:

JVM中提供了三层的ClassLoader:

  • Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
  • ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
  • AppClassLoader:主要负责加载应用程序的主函数类

我们写的Hello.java编译成的Hello.class文件,它是如何被加载到JVM中的呢?

  • 在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。

双亲委派机制

JVM 学习笔记_第6张图片

从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。

JVM字节码执行引擎

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

  • “虚拟机”是一个相对于“物理机”的概念,虚拟机的字节码是不能直接在物理机上运行的,需要JVM字节码执行引擎- 编译成机器码后才可在物理机上执行。

沙箱安全机制

沙箱机制就是讲Java代码限定在虚拟机JVM特定的运行范围中,并且严格限制代码对本地资源的访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。

组成沙箱的基本组件:

1.字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范。可以帮助Java程序实现内存保护 。核心类不经过字节码校验
2.类装载器:其中类装载器在3个方面对Java沙箱起作用

  • 防止恶意代码干涉善意代码(双亲委派机制)
  • 守护被信任的类库边界
  • 它将代码归入保护域,确定了代码可以进行哪些操作

GC(垃圾回收)

  • GC 是垃圾收集的意思(Gabage Collection),

  • Function:内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。

  • 优点:JVM的垃圾回收器都不需要我们手动处理无引用的对象了,这个就是最大的优点

  • 缺点:程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。

  • 原理 :对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。

  • 如何手动回收:程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。

  • 垃圾回收主要是对新生代; GC两种:轻GC,重GC(全局GC);

垃圾收集系统

JVM 学习笔记_第7张图片
最小的方块是常量池;

  • 程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。
  • 垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理
  • 有一部分原因就是因为Java垃圾回收系统的强大导致Java领先市场

GC分区

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。而新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

分代收集算法

当前商业虚拟机都采用 分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代和 永久代。

新生区

伊甸园区、幸存者区

  • 新生代中一般保存新出现的对象,所以每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

老年期

  • 老年代中一般保存存活了很久的对象,他们存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。

永久区(元空间)

  • 逻辑上存在,实际上不存在
  • 不纯在垃圾回收
  • 永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。

JVM中的永久代中会发生垃圾回收吗

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区
(注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)

怎么判断对象是否可以被回收?(引用计数器法)(可达性分析算法)

一般有两种方法来判断:

  • 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;(这个已经淘汰了)
  • 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。(市场上用的非常非常广泛)

JVM 垃圾回收算法有哪些?

  • 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
  • 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
  • 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
  • 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

GC复制算法

JVM 学习笔记_第8张图片
一般用于新生区(对象存活率比较低的地方)

GC标记清除-压缩算法

JVM 学习笔记_第9张图片

  • 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。

  • 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
    JVM 学习笔记_第10张图片

  • 优点:解决了标记-清理算法存在的内存碎片问题。

  • 缺点:仍需要进行局部对象移动,一定程度上降低了效率。

Minor GC、Major GC、Full GC是什么

  • Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。(一般采用复制算法回收垃圾)
  • Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。(可采用标记清楚法和标记整理法)
  • Full GC是清理整个堆空间,包括年轻代和老年代

Minor GC、Major GC、Full GC区别及触发条件

  • Minor GC 触发条件一般为:
    – eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
    – 新创建的对象大小 > Eden所剩空间时触发Minor GC

  • Major GC和Full GC 触发条件一般为: Major GC通常是跟full GC是等价的
    – 每次晋升到老年代的对象平均大小>老年代剩余空间
    – MinorGC后存活的对象超过了老年代剩余空间
    – 永久代空间不足
    – 执行System.gc()
    – CMS GC异常
    – 堆内存分配很大的对象

为什么新生代要分Eden和两个 Survivor 区域?

  • 防止频繁触发Major GC; 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。

  • Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。

  • 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)

为什么要这样分代?

  • 其实主要原因就是可以根据各个年代的特点进行对象分区存储,更便于回收,采用最适当的收集算法
    – 新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
    – 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。
  • 新生代又分为Eden和Survivor (From与To,这里简称一个区)两个区。加上老年代就这三个区。数据会首先分配到Eden区当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代(大对象是指需要大量连续内存空间的java对象)。当Eden没有足够空间的时候就会触发jvm发起一次Minor GC,。如果对象经过一次Minor-GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代中了,当然晋升老年代的年龄是可以设置的。

JVM 有哪些垃圾回收器?

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。

JVM 学习笔记_第11张图片

  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  • G1(Garbage First)收集器 ( 标记整理 + 复制算法来回收垃圾 ): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

总结(GC算法)

内存效率:复制>标记清除>标记压缩
内存整齐度:复制=压缩>清除
内存利用率:压缩=清除>复制

年轻代:复制
老年代:清除+压缩

简述java内存分配与回收策率

  • 所谓自动内存管理,最终要解决的也就是内存分配和内存回收两个问题。
  • 对象的内存分配通常是在 Java 堆上分配(随着虚拟机优化技术的诞生,某些场景下也会在栈上分配,后面会详细介绍),对象主要分配在新生代的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数情况下也会直接在老年代上分配。总的来说分配规则不是百分百固定的,其细节取决于哪一种垃圾收集器组合以及虚拟机相关参数有关,

JVM性能调优

  • JVM调优目标:使用较小的内存占用来获得较高的吞吐量或者较低的延迟。

为什么要调优?

  • 程序在上线前的测试或运行中有时会出现一些大大小小的JVM问题,比如cpu load过高、请求延迟、tps降低等,甚至出现内存泄漏(每次垃圾收集使用的时间越来越长,垃圾收集频率越来越高,每次垃圾收集清理掉的垃圾数据越来越少)、内存溢出导致系统崩溃,因此需要对JVM进行调优,使得程序在正常运行的前提下,获得更高的用户体验和运行效率。

调优指标

  • 内存占用:程序正常运行需要的内存大小。

  • 延迟:由于垃圾收集而引起的程序停顿时间。

  • 吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值。

当然,和CAP原则一样,同时满足一个程序内存占用小、延迟低、高吞吐量是不可能的,程序的目标不同,调优时所考虑的方向也不同,在调优之前,必须要结合实际场景,有明确的的优化目标,找到性能瓶颈,对瓶颈有针对性的优化,最后进行测试,通过各种监控工具确认调优后的结果是否符合目标。

JVM调优工具

调优可以依赖、参考的数据有系统运行日志、堆栈错误信息、gc日志、线程快照、堆转储快照等。

  • ① 系统运行日志:系统运行日志就是在程序代码中打印出的日志,描述了代码级别的系统运行轨迹(执行的方法、入参、返回值等),一般系统出现问题,系统运行日志是首先要查看的日志。

  • ② 堆栈错误信息:当系统出现异常后,可以根据堆栈信息初步定位问题所在,- 比如根据“java.lang.OutOfMemoryError: Java heap space”可以判断是堆内存溢出;根据“java.lang.StackOverflowError”可以判断是栈溢出;根据“java.lang.OutOfMemoryError: PermGen space”可以判断是方法区溢出等。

  • ③ GC日志:程序启动时用 -XX:+PrintGCDetails 和 -Xloggc:/data/jvm/gc.log 可以在程序运行时把gc的详细过程记录下来,或者直接配置“-verbose:gc”参数把gc日志打印到控制台,通过记录的gc日志可以分析每块内存区域gc的频率、时间等,从而发现问题,进行有针对性的优化。

  • ④ 线程快照:顾名思义,根据线程快照可以看到线程在某一时刻的状态,当系统中可能存在请求超时、死循环、死锁等情况是,可以根据线程快照来进一步确定问题。通过执行虚拟机自带的“jstack pid”命令,可以dump出当前进程中线程的快照信息,更详细的使用和分析网上有很多例,这篇文章写到这里已经很长了就不过多叙述了,贴一篇博客供参考:http://www.cnblogs.com/kongzhongqijing/articles/3630264.html

  • ⑤ 堆转储快照:程序启动时可以使用 “-XX:+HeapDumpOnOutOfMemory” 和 “-XX:HeapDumpPath=/data/jvm/dumpfile.hprof”,当程序发生内存溢出时,把当时的内存快照以文件形式进行转储(也可以直接用jmap命令转储程序运行时任意时刻的内存快照),事后对当时的内存使用情况进行分析。

JVM调优经验

VM配置方面,一般情况可以先用默认配置(基本的一些初始参数可以保证一般的应用跑的比较稳定了),在测试中根据系统运行状况(会话并发情况、会话时间等),结合gc日志、内存监控、使用的垃圾收集器等进行合理的调整,当老年代内存过小时可能引起频繁Full GC,当内存过大时Full GC时间会特别长。

引用

1.《深入理解JVM》
2.《JVM》–狂神说(bilibili)
3.《通俗易懂的双亲委派机制》–IT烂笔头(CSDN
4.《百度百科》
5.《Java虚拟机》 –小杰要吃鱼(掘金)
6.《Java虚拟机》 –胡玉洋(CSDN)

你可能感兴趣的:(JVM,java)