jvm调优

jvm调优

1. 什么是jvm
2. jvm的发展史
3. jvm内存区域划分
4. jvm gc的基本原理和算法
5. jvm内存分配策略
6. 常见的jvm分析工具

1.什么是jvm

jvm->Java Virtual Machine ,是java的跨平台,一次编译,到处执行。
每一种操作系统,执行相关程序的时候,因为操作系统环境的不同,会造成代码不能跨平台执行。而java可以做到,原因在哪里?就在于不同操作系统有不同版本的jvm
语言的执行过程:
源代码(.java)----->编译(字节码 .class)----->解释(成为机器码,01010100110)---->机器码

2. jvm的发展史

1、Sun Classic
不过只能使用纯解释器方式来执行Java代码,如果要使用JIT编译器(JIT Compiler(Just-in-time Compiler) 即时编译器),就要使用第三方外挂,一旦使用了JIT编译器,JIT编译器就完全接管了虚拟机的执行系统,解释器便不再工作了。
此时的编译器不智能,编译器不得不对每一个方法、每一行代码都进行编译,而无论它们执行的频率是否具有编译的价值。
2、Exact VM
JDK 1.2时,曾在Solaris平台上发布过一款名为Exact VM的虚拟机,它的执行系统已经具备现代高性能虚拟机的雏形:如支持编译器与解释器混合工作模式。
Exact VM因它使用准确式内存管理(Exact Memory Management,也可以叫Non-Conservative/Accurate Memory Management)而得名。
虚拟机可以知道内存中某个位置的数据具体是什么类型。譬如内存中有一个32位的整数123456,它到底是一个reference类型指向123456的内存地址还是一个数值为123456的整数,虚拟机将有能力分辨出来,这样才能在GC(垃圾收集)的时候准确判断堆上的数据是否还可能被使用。
由于使用了准确式内存管理,Exact VM可以抛弃以前Classic VM基于handle的对象查找方式每次定位对象都少了一次间接查找的开销,提升执行性能。
基于handle(句柄)的对象查找:当123456指向的对象经过一次垃圾回收后,内存地址需要重新规整。内存地址发生了变化为654321,不能将内存中所有的值为123456数据都改为654321。使用句柄来管理对象内存地址的变动,所以定位对象时先去句柄查下实际的地址再去查找对象本身的属性。类似于对象的户口变更登记册。
句柄可以理解为:引用的引用。指针的指针。
3、Sun HotSpot VM
01、可以通过执行计数器找出最具有编译价值的代码,根据执行计数器判断是否达到阈值,没到就解释执行, 否则提交编译请求,通知JIT编译器以方法为单位进行编译。
所以:如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。
OSR:由于代码块可能是在解释执行过程中直接切换到本地代码执行,所以也叫做栈上替换(OSR, OnStackReplacement)
02、通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,即时编译的时间压力也相对减小,这样有助于引入更多的代码优化技术,输出质量更高的本地代码[机器执行码]

3. jvm内存区域划分

jvm运行时内存区域分为了两种:线程隔离线程共享

线程隔离分为:栈(虚拟机栈、本地方法栈)、程序计数器
线程共享分为:堆和方法区(在jdk1.8之后,方法区被称之为Metaspace

在这里插入图片描述

3.1 各区域介绍

3.1.1 程序计数器

程序计数器(Program Counter Register)是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,所以程序计数器这类内存区域为“线程私有”的内存。
如果线程正在执行的是Native方法,这个计数器值则为空(Undefined)。
native方法 是与C++联合开发的时候用的!使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。

3.1.2 栈

所谓“栈”包括:java虚拟机栈、本地方法栈;他们作用相似,区别只是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。程序员人为的分为“堆栈”中的“栈”。
栈里存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和指向了一条字节码指令的地址。
每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表所需的内存空间在编译期间完成分配,其中64位的long和double类型的数据会占2个局部变量空间,其余的数据类型只占用1个。当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
数栈也要操作栈,主要是在方法计算时存放的栈。

3.1.3 堆

Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块,此内存区域就是存放对象实例,几乎所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的主要区域;内存回收的角度来看Java堆中还可以细分为:新生代和老年代;新生代细致一点的有Eden空间From Survivor空间To Survivor空间。这两块survivor空间大小一致。
在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过**-Xmx设置最大内存和-Xms设置初始内存**)
java -Xms10m -Xmx100m Hello

3.1.4 方法区

方法区又叫静态区:用于存储已被虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆);
对于HotSpot虚拟机是使用永久代来实现方法区
Java虚拟机规范对方法区的限制非常宽松,除了不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,条件相当苛刻。
在jdk1.7中永久代的配置参数-XX:PermSize5m(初始化永久代内存大小),-XX:MaxPermSize10m(最大永久代内存大小)
在jdk1.8中Metaspace的配置参数:-XX:MetaspaceSize=10m(初始化大小),-XX:MaxMetaspaceSize=10m(最大大小)
java中的常量池技术,是为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个),在需要重复创建相等变量时节省了很多时间。

3.1.5 OOM异常

OOM,即out of memory ,内存溢出异常,包括如下
1.程序计数器
没有指定任何OutOfMemoryError情况
2.java虚拟机栈\本地方法栈区域
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
3.堆
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
报错后dump出信息: -XX:+HeapDumpOnOutOfMemoryError
-Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError
4.方法区
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

最常见的OOM异常时栈溢出和堆(内存)溢出,栈溢出最常见的原因是递归使用不合理,堆溢出的最常见原因是无限创建对象

4. 对象的引用/可达性分析

4.1对象的四种引用

java中的引用分为:强引用、软引用、弱引用、虚引用(幽灵引用或者幻影引用),这4种引用强度依次逐渐减弱。
强引用:在程序代码之中正常的类似于“Person p = new Person()”这类的引用;垃圾收集器不会回收掉被强引用的对象。
软引用:有用但非必须的对象,jdk中提供了SoftReference类来实现软引用;系统在发生内存溢出异常之前,会把只被软引用的对象进行回收。
用途?可以做缓存。
弱引用:非必须的对象,jdk中提供了WeakReference类来实现软引用,比软引用弱一些;垃圾回收不论内存是否不足都会回收只被弱引用关联的对象。
虚引用:对被引用对象的生存时间不影响;无法通过虚引用来取得一个对象实例;为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知;jdk提供PhantomReference类来实现虚引用。


/**

 * Java引用分为:强引用>软引用>弱引用>虚引用

 *

 */

public class ReferenceTest {

    public static void main(String[] args) {

        System.out.println("===========强引用========");

        //强引用

        Person p = new Person();

        System.gc();//手动执行垃圾回收

        System.out.println(p);

        //软引用

        System.out.println("===========软引用========");

        SoftReference<Person> sp = new SoftReference<Person>(new Person());

        System.gc();

        System.out.println(sp.get());

        System.out.println("---------------软引用在内存溢出的表现-------------------------");

        try {

            List<HeapOOM.OOMObject> list = new ArrayList<HeapOOM.OOMObject>();



            while (true) {

                list.add(new HeapOOM.OOMObject());

            }

        } finally {

            System.out.println("内存溢出之后的软引用是否存在:");

            System.out.println(sp.get());

            System.out.println("---------------软引用在内存溢出的表现-------------------------");

            //弱引用

            System.out.println("===========弱引用========");

            WeakReference<Person> wp = new WeakReference<Person>(new Person());

            System.gc();

            System.out.println(wp.get());

            System.out.println("===========虚引用========");

            //虚引用

            ReferenceQueue<Person> referenceQueue = new ReferenceQueue<Person>();

            Person person = new Person();

            PhantomReference<Person> pp = new PhantomReference<Person>(person, referenceQueue);

            person = null;

            System.out.println(referenceQueue.poll());

            System.gc();

            System.out.println(pp.get());

            try {

                //gc后等1秒看结果

                Thread.sleep(1000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            System.out.println(referenceQueue.poll());

            System.out.println("===================================");

            Properties properties = System.getProperties();

            for (Map.Entry<Object, Object> me : properties.entrySet()) {

                System.out.println(me.getKey() + "=" + me.getValue());

            }

            System.out.println("=================获取传递个JVM的参数=========================");

            System.out.println(System.getProperty("zookeeper.root.logger"));//-Dzookeeper.root.logger=INFO,stdout,R





            Person p1 = new Person();

            Person nP = p1;//

            p1 = null;

            System.out.println(nP);

        }

    }

}



class Person {

    String name = "张三";



    @Override

    public String toString() {

        return name;

    }

}

4.2 引用计数器法/可达性分析

4.2.1 引用计数器法

引用计数算法基本思想:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

        Person p1 = new Person();
        System.out.println(p1);
        Person p2 = p1;
        System.out.println(p2);
        p1 = null;
        System.out.println(p2);
        

Person p1 指向了对象new Person();所以当前对象new Person();对应的引用计数器+1,Person p2 = p1;
自然当前对象new Person();对应的引用计数器在+1,为2。当p1 = null;当前对象new Person();失去一个引用,计数结果-1,当计数结果为0的话,则证明当前对象不被引用,则可以被垃圾回收掉。

4.2.2 可达性分析

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(即不可达)时,则证明此对象是不可用的。
常见的gc root对象:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中JNI(Java Native Interface即一般说的Native方法)引用的对象。

在这里插入图片描述
那么可达性分析比引用计数器法的优势在哪呢?
我觉得主要是效率的问题,如上图,object5本身应该被回收,但由于它被两个无效引用所引用,只有object 6和7被回收完之后才能对5进行回收,只能由下到上依次回收,效率很低,而可达性分析就没有这个缺点,直接将5,6,7直接回收。

5. gc回收过程

5.1 垃圾回收前的垂死挣扎

如果我们判定一个对象不可达,就应该将该对象进行gc垃圾回收掉,但是jvm在进行垃圾回收之前会对这些对象进行一轮的筛选,如果相关对象此时重新和引用链的对象建立起了关联,那么是可以逃脱被gc掉的命运,但是不是所有的对象都有着特权,只有我们在编写类的时候,复写Object类中的一个方法finalize(),也就是说在该方法重重新建立了引用,就可以起死回生。

5.2 两次标记回收的过程

不可达的对象真正死亡需要两次标记:
当不可达时标记第一次标记,当对象覆盖finalize()方法并且finalize()方法没有被虚拟机调用过,此对象将会放置在一个叫做F-Queue的队列之中,稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去触发这个方法,但并不承诺会等待它运行结束再执行垃圾回收。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中重新与引用链上的任何一个对象建立关联那么他被移除出“即将回收”的集合,否则就被回收了。

5.3 垃圾回收算法

  1. 标记清除算法
    最基础的收集算法是“标记-清除”(Mark-Sweep)算法,此方法分为两个阶段:标记、清除
    标记要清除的对象,统一清除;
    不足有两个:
    一个是效率问题,标记和清除两个过程的效率都不高;
    另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
    在这里插入图片描述
    因为经过标记清除之后,会有大量的内存碎片,所以边对空间问题有了一个修正,标记整理算法:
    第一步和第二步和标记清除算法一样,只不过为了产生更多的连续的可用空间,讲对象整理成连续的。
    这种算法因为运行的效率不高的原因,一般都用在对老年代或者永久代的空间进行垃圾整理。
    在这里插入图片描述

  2. 复制算法
    复制算法:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
    优点:无内存碎片,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。缺点:实际可用内存缩小为了原来的一半。
    在这里插入图片描述
    现代商业虚拟都采用这个复制算法来回收新生代中的对象。
    新生代垃圾回收的过程:
    1、将内存分为一块较大的Eden空间和两块较小的Survivor空间;
    2、每次使用Eden和其中一块Survivor。
    3、当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,并清理掉Eden和刚才用过的Survivor空间。
    HotSpot虚拟机默认Eden和Survivor的大小比例是8:1:1;浪费10%。
    新生代垃圾回收过程示意图:在这里插入图片描述

5.4 三种gc方式

Minor GC:新生代的垃圾回收过程
Major GC:老年代或者永久代执行的gc,称之为Major GC
Full GC:Minor GC + Major GC
(关注DOTA2的人对这些名词都不会陌生吧,知识总是相通的,人生无时无刻不在学习,大家共勉)

5.5 GC内存分配策略

对象在Eden分配:大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,此时对象会进入survivor区,当对象满足一些条件后会进入老年代。
对象进入老年代有三种策略:

  1. 长期存活的对象直接进入老年代
    虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
  2. 在survivor中相同年龄的对象总体积超过survivor一半区域是,大于等于该年龄的对象直接晋升到老年代,无须等到MaxTenuringThreshold中要求的年龄。
  3. 大对象直接在老年代中被创建

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

5.6 GC空间担保问题

1、在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代的所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。
2、如果不成立,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。
3、如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

5.7 常见的垃圾收集器

5.7.1 Serial

Serial收集器是最基础、历史最悠久的适合新生代的收集器。
特点:单线程、stop-the-world 、复制算法
缺点:影响用户响应时间
优点:回收时简单高效、对于限定单个cpu环境下,serial收集器由于没有线程交互的开销,专心做垃圾收集,可以获得最高的单线程收集效率。
所以:serial 收集器 对于运行在client模式下的虚拟机来说,是一个很好的选择。
serialOld收集器是Serial的老年代收集器,采用“标记-整理”

5.7.2 ParNew

ParNew收集器其实是Serial的多线程版本,除了他是使用多条线程来进行垃圾回收之外和Serial是完全一样的。新生代收集器
特点:多线程、stop-the-world
缺点:单个cpu下,运行效果甚至没Serial好。
优点:回收时简单高效、对于限定多个cpu环境下,效果比serial好。
所以:parnew收集器是运行在server模式下的首选收集器。

5.7.3 parallel scanvenge

Parallel Scanvenge收集器是一个新生代收集器,采用复制算法。
特点:收集新生代,复制算法,多线程,高吞吐、自适应
1、与其它的收集器侧重垃圾回收时用户的停顿时间不同,它主要侧重与吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
停顿时间越短就越适合需要与用户交互的程序,高吞吐量则是可以高效率地利用cpu时间尽快完成任务。
2、他有一个自适应开关(-XX:+UseAdaptiveSizePolicy):打开后,用户只需要把基本的内存数据(堆最大,初始量)设置好,然后设置更关注最大停顿时间或者更关注吞吐量,收集器会把细节参数自动调节。
Parallel Old 老年代收集器,采用标记-整理算法。

5.7.4 ConcMarkSweep/CMS

CMS(concurrent mark sweep)收集器是一个以获取最短回收停顿时间为目标的老年代收集器。
特点:并发收集、低停顿。
基于 标记-清除算法实现,但是整个过程比较复杂一些。过程分为4步:
1、初始标记:仅仅标记GCRoot能直接关联到的对象。速度很快,“stop the world”
2、并发标记:GCRoot Tracing。耗时长和用户线程同步。
3、重新标记:修正并发标记时,由于用户程序运行导致的标记变动。“stop the world”停顿稍长一些。
4、并发清除:耗时长,和用户线程同步。
缺点:吞吐量会变低、浮动垃圾无法处理、标记-清除的碎片(设置参数是 fullgc前开启碎片整理功能,gc停顿时间延长)。
可以兼容的新生代收集器:ParNew和Serial

5.7.5 G1

G1(Garbage-First)收集器是当今收集器领域最前沿成果之一。2004年sun发表第一篇G1论文,10年后才开发出G1的商用版本。
hotspot开发团队赋予它的使命:未来替调CMS收集器。
特点:
1、并行与并发:利用多cpu缩短stop-the-world的时间,使用并发方式解决其它收集器需要停顿的gc动作。
2、分代收集:新老代收集区分对待。
3、空间整合:G1从整理看是基于标记-整理,但是局部看是基于复制算法实现的,不会产生碎片。
4、可预测的停顿:能够让使用者指定在M毫秒的时间片段上,消耗在垃圾回收的时间不得超过N毫秒。
过程:初始标记、并发标记、最终标记、筛选回放。前三个和CMS一致,筛选回放是根据用户设置的停顿目标来选择回收价值最高的进行回收。
G1在jdk1.9之后才能够使用

6.常见的jvm分析工具

6.1 CLI(Command Line interface)

jps(java process status)
jstatd
jmap
jinfo
jstack
jstat

6.2 GUI(Graph User Interface)

jconsole
jvisualvm

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值