JVM笔记

2018-12-14  本文已影响0人  斯文遮阳

一、类加载机制

Java字节码

我们编写好的代码是 .java文件,并不能交给机器直接执行, 需要将其编译成为.class文件,这个文件即是Java字节码。它是一个以“cafe babe”打头的十六进制表示的二进制流。静态编译器如何把源码转化成字节码呢?如下图所示:


字节码生成过程

词法解析是通过空格分隔出单词、操作符、控制符等信息,将其形成 token信息流,传递给语法解析器;在语法解析时,把词法解析得到的 token信息流按照Java语法规则组装成一棵语法树;在语义分析阶段, 需要检查关键字的使用是否合理、类型是否匹配、作用域是否正确等;当语义分析完成之后,即可生成字节码。

字节码必须通过类加载过程加载到JVM环境后,才可以执行。执行有三种模式:

混合执行模式的优势在于解释器在启动时先解释执行,省去编译时间。 随着时间推进,JVM 通过热点代码统计分析,识别高频的方法、循环体、公共模块等,基于JlT动态编译技术将热点代码转换成机器码,缓存起来并直接交给CPU执行。这也是为什么机器在热机状态可以承受的负载要大于冷机状态(刚启动时)的原因。

类加载过程

类加载是一个将.class字节码文件实例化成Class对象并进行相关初始化的过程。在这个过程中,JVM会初始化继承树上还没有被初始化过的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。某些类在使用时,也可以按需由类加载器进行加载。类加载包括加载、链接、初始化三个过程。

双亲委派模型

JVM在加载类时,使用的是双亲委派模型(Parents Delegation Model),如下图:


双亲委派模型
  1. 第一层:Bootstrap ClassLoader,它是在JVM启动时创建的,通常由C/C++实现。Bootstrap是最根基的类加载器,负责装载最核心的Java类,比如Object、System、String等
  2. 第二层:在JDK9 版本中,称为Platform ClassLoader,即平台类加载器,用以加载一 些扩展的系统类,比如 XML、加密、压缩相关的功能类等 。JDK9之前的加载器是Extension ClassLoader
  3. 第三层:应用类加载器,主要是加载用户定义的CLASSPATH路径下的类

低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,需要向上逐级询问是否已加载此类,直至Bootstrap ClassLoader;然后向下逐级尝试是否能够加载此类。如果都为否,则通知发起加载请求的当前类加载器,可以加载。

双亲委派模型的关键点:

查看Bootstrap所有已经加载的类库以及本地类加载器的代码:

    // 查看Bootstrap所有已经加载的类库
    URL[] urLs = sun.misc.Launcher.getBootstrapClassPath() .getURLs() ; 
    for (java.口et.URL url : urLs) {
        System .out .println(url.toExternalForm());
    }


    // AppClassLoader
    ClassLoader c = TestClass.class.getClassLoader();
    // ExtClassLoader
    ClassLoader cl= c.getParent(); 
    // null (Bootstrap不存在于JVM体系内)
    ClassLoader c2 = cl.getParent();

解决类冲突时,如果想在启动时观察加载了哪个jar包中的哪个类,可以增加参数:-XX:+TraceClassLoading

自定义类加载器

什么时候需要自定义类加载器?

自定义类加载起的步骤:继承 ClassLoader;重写findClass()方法;调用defineClass()方法。例如:

    public class CustomClassLoader extends ClassLoader { 
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte(] result = getClassFromCustomPath(name); 
                if (result == null) {
                    throw new FileNotFoundException();
                } else {
                    return defineClass(name, result, 0, result.length);
                } 
            } catch (Exception e) {
                e.printStackTrace();
            }
            throw new ClassNotFoundException(name);
        }

        private byte[] getClassFromCustomPath(String name) {
            // 自定义加载类的路径
        }
    }

二、JVM内存结构

内存是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM高效稳定运行。JVM内存结构如下图所示:

JVM内存结构

Heap(堆区)

堆区存储着JVM内存中几乎所有的实例对象,被各子线程共享使用,由垃圾回收器自动回收。堆是OOM(out of memory)的主要发源地。

堆区

堆区分为两块:新生代和老年代。新生代 = 1个Eden区 + 2个Survivor区,默认情况按照8:1:1的比例分配。绝大部分对象在 Eden 区生成,当Eden 区装填满的时候,会触发 Young Garbage Collection,即YGC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收,依然存活的对象会被移送到 Survivor 区。Survivor区分为S0和Sl两块内存空间,送到哪块空间呢?每次YGC的时候,将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果YGC要移送的对象大于Survivor区容量的上限 ,则直接移交给老年代。对象也并不是一直在新生代的 Survivor区交换来交换去,每个对象都有一个计数器,每次YGC 都会加1。计数器的值到达某个阐值的时候(-XX:MaxTenuringThreshold可以设置该值),对象从新生代晋升至老年代。如果该参数配置为1,那么从新生代的 Eden 区直接移至老年代,默认值是15。该过程如下图所示:

堆区对象申请

Metaspace(元空间)

Metaspaces是hotspot对Method Area的实现(即方法区,是JVM规范层面的)。在JDK8版本中,元空间的前身Perm区已经被淘汰。在JDK7及之前的版本中,只有Hotspot才有Perm区,译为永久代,它在启动时固定大小,很难进行调优,并且FGC时会移动类元信息。在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。

区别于永久代,元空间在本地内存中分配,而不是在虚拟机中。在JDK8里,Perm区中的所有内容中字符串常量移至堆内存,其他内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间内。

元空间的内存管理由元空间虚拟机来完成。每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元空间。当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收。元空间虚拟机采用了组块分配的形式,同时区块的大小由类加载器类型决定。类信息并不是固定大小,因此有可能分配的空闲区块和类需要的区块大小不同,这种情况下可能导致碎片存在。

JVM Stack (虚拟机栈)

JVM中的虚拟机栈是描述Java方法执行的内存区域,它是线程私有的。每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程。在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。而 StackOverflowError表示请求的栈溢出,导致内存耗尽(通常出现在递归方法中)。

虚拟机栈通过压栈和出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上;如果执行过程中出现异常,会进行异常回溯,返回地址通过异常处理表确定。栈帧包括局部变量表、操作枝、动态连接、方法返回地址等。虚拟机栈的示意图如下:

虚拟机栈

Native Method Stack(本地方法栈)

虚拟机栈“主内”,本地方法栈“主外”,这个“内外”是针对JVM来说的,本地方法栈为Native方法服务。本地方法可以通过JNI(Java Native Interface )来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和JVM相同的能力和权限。当大量本地方法出现时,势必会削弱JVM对系统的控制力,因为它的出错信息都比较黑盒。对于内存不足的情况,本地方法栈还是会抛出native heap OutOfMemory。

这里介绍一下JNI类本地方法,最著名的本地方法应该是System. currentTimeMillis() 。JNI使Java深度使用操作系统的特性功能,复用非Java代码。但是在项目过程中,如果大量使用其他语言来实现JNI,就会丧失跨平台特性,威胁到程序运行的稳定性。假如需要与本地代码交互,就可以用中间标准框架进行解辑,这样即使本地方法崩溃也不至于影响到JVM的稳定。当然,如果要求极高的执行效率、偏底层的跨进程操作等,可以考虑设计为JNI调用方式。

三、对象实例化

对象存储信息

对象存储信息
  1. 对象头
    对象头占用12个字节,存储内容包括对象标记(markOop )和类元信息( klassOop )。 对象标记存储对象本身运行时的数据,如哈希码、GC标记、锁信息、线程关联信息等,这部分数据在 64 位JVM上占用8个字节,称为"Mark World"。为了存储更多的状态信息,对象标记的存储格式是非固定的(具休与JVM的实现有关)。类元信息存储的是对象指向它的类元数据(即Klass)的首地址,占用4个字节,与引用对象开销一致。
  2. 实例数据
    存储本类对象的实例成员变量和所有可见的父类成员变量。例如:Integer 的实例成员只有一个private int value,占用4个字节,所以加上对象头为16个字节。其他的如MAX VALUE、 MIN_VALUE等都是静态成员变量,在类加载时就分配了内存,与实例对象容量无关。此外,类定义中的方法代码不占用实例对象的任何空间。IntegerCache是Integer的静态内部类,容量占用也与实例对象无关。
  3. 对齐填充
    对象的存储空间分配单位是8个字节。如果一个占用大小为16个字节的对象,增加一个成员变量byte类型,此时需要占用17个字节,但是也会分配24个字节进行对齐填充操作。

示例:

class Demo1 {
    //对象头最小占用空间12个字节

    //下方4个byte类型分配后,对象占用大小是4个字节
    byte bl; 
    byte b2; 
    byte b3; 
    byte b4;

    //下方每个引用变量占用是4个字节,共20个字节
    Ob]ect obj1;
    Ob]ect obj2;
    Ob]ect obj3;
    Ob]ect obj4;
    Ob]ect obj5;

    //实例占用空间并非计算在本对象内,依然只计算引用变量大小4个字节 
    Demo2 ol = new Demo2();
    Demo2 o2 = new Demo2();

    //综上,Demo1对象占用: 12B + (1B × 4) + (4B × 5) + (4B × 2) = 44字节,取8的倍数为48字节
}

class Demo2 {
    //double类型占用8个字节,但此处是数组引用变量,所以只占4个字节,而不是8*1000
    //这个数组引用的是double[]类型,指向实际分配的数组空间首地址。在new对象时,已经实际分配空间
    double[] d = new double[1000];
}

对象创建过程

  1. 确认类元信息是否存在
    当JVM接收到new指令时,首先在metaspace内检查需要创建的类元信息是否存在。若不存在,那么在双亲委派模型下,使用当前类加载器以 ClassLoader+包名+类名为Key进行查找对应的.class文件。如果没有找到文件,则抛出ClassNotFoundException异常;如果找到,则进行类加载,并生成对应的Class类对象。
  2. 分配对象内存
    首先计算对象占用空间大小,如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节,接着在堆中划分一块内存给新对象。在分配内存空间时,需要进行同步操作,比如果用CAS (Compare And Swap)失败重试、区域加锁等方式保证分配操作的原子性。
  3. 设定默认值
    成员变量值都需要设定为默认值,即各种不同形式的零值。
  4. 设置对象头
    设置新对象的哈希码、GC信息、锁信息、对象所属的类元信息等。这个过程的具体设置方式取决于JVM的实现。
  5. 执行init方法
    初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

四、垃圾回收

垃圾回收算法

Java会对内存进行自动分配与回收管理,其中后者是通过JVM的垃圾回收(GC)来实现的。如何判断对象是否可回收?JVM引入了GC Roots。如果一个对象与GC Roots之间没有直接或间接的引用关系,比如某个失去任何引用的对象,或者两个互相环岛状循环引用的对象等,这些对象就是可以被回收的。什么对象可以作为 GC Roots 呢?比如类静态属性中引用的对象、常量引用的对象、虚拟机栈中引用的对象、本地方法栈中引用的对象等。

常见的垃圾回收算法:

垃圾回收器

G1 GC日志

五、JVM调优

JVM问题处理

JVM问题总体概括

问题分类:
当应用系统运行缓慢,页面加载时间变长,后台长时间无影响时,都可以参考以下归类的解决方法。绝大部分的JAVA程序运行时异常都是Full GC、OOM、线程过多。主要分这么几大类:

紧急处理原则:
问题发生后,第一时间是快速保留问题现场供后面排查定位,然后尽快恢复服务。保留现场的具体操作:

JVM常用参数

-Xms设置堆的最小空间大小
-Xmx设置堆的最大空间大小
-XX:NewSize设置新生代最小空间大小
-XX:MaxNewSize设置新生代最大空间大小
-XX:MetaspaceSize设置metaspace初始化空间大小
-XX:MaxMetaspaceSize设置metaspace最大空间大小
-XX:PermSize设置永久代最小空间大小(已废弃)
-XX:MaxPermSize设置永久代最大空间大小(已废弃)
-Xss设置每个线程的堆栈大小

参考文献:

上一篇 下一篇

猜你喜欢

热点阅读