JVM浅谈

Posted by Haiming on April 29, 2020

参考:3y的和纯洁的微笑的文章。

1. 类加载机制

对于一个Java Bean:

image-20200429163949215

我们写一个测试类:

image-20200429164010307

我们都知道,使用javac来编译代码,使用java命令来运行代码。

1.1 java编译过程和语法糖的处理

javac是java源码的编译过程,编译由三个过程组成:

  1. 分析和输入到符号表
  2. 注解处理
  3. 语义分析和生成class文件

语法糖的处理:

我们在平时编写java程序的时候都会使用一些”语法糖“,那么这些语法糖在编译的时候都会被处理掉。比如”泛型“,其只会在java源码之中存在,编译之后会被替换成原来的原生类型,这个叫做”泛型擦除“。

1.2 JVM解析运行.class

我们通过javac命令,可以将.java生成.class文件。这些.class文件是不能直接运行的,其是由JVM来解析运行。

1.3 类加载过程

比如我们上面自己写了两个类,是只要一启动就会立刻加载到JVM之中么?

当然不是,只有五种情况需要立刻对类进行”初始化“:

  1. 创建类的实例,访问类的静态变量或者调用类的静态方法
  2. 反射的方式——我都要用你产生新的对象了,赶紧初始化
  3. 初始化某个类的子类,其父类也会被初始化
  4. Java虚拟机启动时候被标明启动类的类

1.4 将类加载到JVM和双亲委派模型

class文件是通过类加载器(ClassLoader)加载到jvm之中的。

Java默认三个类加载器:

img

  • 1)Bootstrap ClassLoader:负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
  • 2)Extension ClassLoader:负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目录下的jar包
  • 3)App ClassLoader:负责记载classpath中指定的jar包及目录中class

什么是双亲委派:

就是一个类加载器收到类加载的请求,那么会直接亲戚其父类去加载,如果父类加载不了,才会从上到下进行进一步的加载。

好处:

  • 防止内存之中出现多份同样的字节码,或者是程序员自己写的类替代了Java自带的类。

类加载器加载某个类成功之后,会将得到的java.lang.class类的实例缓存起来,下次再请求加载该类的过程之中就会直接使用缓存的类的实例。

1.4.1 类的加载过程

  1. 加载:查找并加载类的二进制数据,在Java堆之中也创建一个java.lang.class对象。
  2. 连接:
    1. 验证:文件格式,元数据,字节码,符号引用验证等等
    2. 准备,为类的静态变量分配内存,并且初始化为默认值
    3. 解析:将类之中的符号引用转换成直接引用
  3. 初始化:为类的静态变量赋予正确的初始值

1.4.2 JIT即时编译器

对于某些热点代码,如果还是在字节码之中逐条取出,逐条解释执行,这种速度太慢了。那么需要将Java字节码重新编译优化,直接生成机器码,让CPU直接执行。这个过程是通过计数器,当次数超过了一定的限制,就直接触发JIT编译。

2. 分配内存

img

  1. 堆:堆之中存放对象实例
  2. 方法区:存储已经被虚拟机加载的类信息,常量,静态变量等。
  3. 程序计数器:当前执行的行号指示器
  4. JVM栈:其描述的是Java方法执行的内存模型,每个方法被执行的时候都有一个栈帧,用于存储:
    1. 局部变量表:在当前方法之中生成的局部变量
    2. 操作栈:比如要执行某些操作,那么就将数据带到操作栈之中来操作,再写会对应的区域,比如局部变量表和堆等等。
    3. 动态链接:Java之中各个函数之间是要相互调用的,那么这里就要有区域来存储其符号引用
    4. 方法出口:要不就是正常的方法出口——返回,要不就是异常
  5. 本地方法栈:为了Java之中的Native方法服务。

2.1 举个例?

img

  1. class文件首先被加载到JVM之中,元空间之中存储着类的信息。

  2. JVM找到main(),为main()创建栈帧,并且执行main()
  3. 需要创建对象,那么就要加载这个类,需要JVM加载这个类,并且将其放在元空间之中
  4. 加载完这个类之后,会在堆之中给一个新的实例分配内存,然后调用构造函数来初始化类的实例,这个实例有指向方法区的类的类型信息
  5. 当使用某个函数的时候,会根据引用找到对应的对象,并且根据对象的引用定位找到方法区(元空间)之中的方法表,得到对应方法的地址。
  6. 为函数创建栈帧,开始运行。

3. GC

3.1 GC 对象

  1. 类对象——方法区/元空间
  2. JVM栈之中引用的对象
  3. 常量引用的对象——final引用的对象

3.2 两种常见的GC收集器

主要讲讲CMS收集器和G1收集器:

CMS收集器(concurrent mark sweep):

优点:并发收集,低停顿。

缺点:

  1. 产生大量碎片,并发阶段降低吞吐量

参考:https://juejin.im/post/5ea79c4d6fb9a0434d7095e2

  1. concurrent mode failure,cms正在回收的时候,年轻带空间满了,要将存活对象放入老年代,但是老年代还没有GC或者放不下,或者能放下但是没有连续空间存放;

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:

初始标记(CMS initial mark)

并发标记(CMS concurrent mark)

重新标记(CMS remark)

并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。 由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。

wpsCA6E.tmp

G1收集器

其内部虽然也是分代,但是代和代之间的界限不再明显。是将内部分成许多个小区域,且区域属于的代可以调换。

优点:

  1. 其采用”标记-整理“算法, 没有碎片

        2. 其建立了一个预测模型,可以预测多长时间之内做完GC
    

收集步骤:

要STW三次

  1. 初始标记,STW,触发一次普通Minor GC
  2. 并发标记,发现其区域全是垃圾,则直接回收全部区域。并且计算每个区域之中存活对象的比例
  3. 再标记,STW,标记并发阶段产生的新垃圾
  4. copy/ clean up:STW,将回收区域的存活对象拷贝到新区域,清空回收区域并且将其返回到空闲区域链表之中