jvm学习

本系列文章主要是对学习《深入理解java虚拟机》的记录,以加深自己的理解,也方便自己后续复习回顾

前言

之前学习java,只是会用常用的语法、框架,但在开发过程中,总会遇到一些奇怪的现象和疑惑的地方。然后觉得必须深入理解java相关的实现。
到现在已经前前后后看了《深入理解java虚拟机》大概有四、五遍。前两遍基本上第五章以后就不怎么看得下去了,后面几遍才慢慢得能把整本书看完,部分重点的章节看了更多遍。现在就希望把学习理解到的jvm相关的知识记录一下,也希望自己在记录的过程中,能够认识理解的更深。

运行时数据区

jvm学习_第1张图片
  • 程序计数器
    线程独有的内存区域。
    感觉和CPU里的程序计数器的意义一样。cpu中的程序计数器通过计数来指定cpu要执行的指令。
    jvm执行的是字节码,虚拟机的当前线程根据计数来指定要执行的字节码。

根据虚拟机概念模型,字节码解释器通过改变计数器的值来选取下一条需要执行的字节码。

此内存区域为唯一一个虚拟机规范中没有规定任何OutOfMemoryError的区域。

  • 虚拟机栈
    线程独有。生命周期和当前线程相同。
    每个方法在执行时,都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    每个方法被调用执行完成的过程,对应着栈帧在虚拟机栈入栈、出栈的过程。

  • 本地方法栈
    和虚拟机栈类似,只不过本地方法栈存储的是当前线程调用的本地方法相关的信息。


  • 所有线程共享。
    几乎所有对象实例都在这里分配内存。GC主要是回收这里的内存。

  • 方法区
    所有线程共享。
    用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。

  • 运行时常量池
    属于方法区的一部分。
    编译生成的class文件不仅包含对类相关的基本信息的描述,还有常量池用于描述编译期生成的各种字面量和符号引用。当类被加载时,常量池中的信息被存在方法区里。

  • 直接内存
    并不属于jvm管理的内存区域。nio分配内存就是直接调用本地方法直接在堆外分配内存。
    如果直接内存申请的大小加上jvm分配的内存大于机器的总内存,就会OOM。

字面量: 字符串,一些数字类型值和final 修饰的常量等。
符号引用: 类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

类在内存的布局

hotspot的实现,对象在内存中分3块存储:对象头(Header),实例数据(Instance data),对齐填充(padding)。
对象头分两部分数据:一部分为32bit/64bit的Mark Word,用来存储对象的运行时数据,包括hash code,gc分代年龄,锁状态标识,线程持有的锁,偏向线程id等。另一部分为类型指针,用来确定当前对象为哪一个类的实例。
实例数据部分存储本对象及其继承下来的相关父类中的属性字段的值。
填充部分,对象内存块的大小必须是8字节的整数倍,如果不够进行填充。

jvm学习_第2张图片

垃圾回收

jvm回收的是哪些区域的内存?
和虚拟机相关的虚拟机栈,程序技术器,本地方法栈中的内存在改线程停止后,所占的内存就会被释放。
而方法区,堆内存占用只有在运行时知道,并且随着 程序的运行 占用内存也会随着变化。方法区中有类加载后存储的类信息描述,这一块内存可以被回收的不多。堆中的对象是主要可以回收的区域。
GC需要解决哪些问题?

  1. 有哪些对象是应该被回收的?
  2. 怎么对内存进行回收?

1. 有哪些对象可以被回收?
只有那些永远不会被引用的对象才可以被回收。
判断对象死亡的方法:

  • 引用计数
    原理:对象维护 一个引用计数变量。有被引用则计数加1。如果引用计数为0代表当前对象可以被回收。
    这种方法是多数人认为jvm实现的方法(起码我大学期间是这么认为),但是因为这种方法在判定循环引用(即对象a引用对象b,同时对象b引用对象a,除了它们彼此引用再没有别的引用关系)的实现逻辑上比价麻烦,所以实际jvm很少有使用。
  • 可达性分析
    原理: 从一系列GC ROOT对象开始向下搜索,搜索经过的对象添加到对应的引用链.如果一个对象没在任何引用链中,则可以被回收。
    (如下图,解决了循环引用的问题)
    jvm学习_第3张图片

    GC ROOT的选取
  • 虚拟机栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

2 垃圾收集算法

  • 标记-清除
    先标记出哪些对象可以回收,然后再清除可回收对象
    回收前内存状态:

    jvm学习_第4张图片

    回收后内存状态:
    jvm学习_第5张图片

    特点
    标记,清除效率都不高
    回收后很多垃圾碎片

  • 复制
    将内存分为两块,回收时将存活的对象完全复制到另一块内存中,然后将之前的内存清空。
    回收前内存状态:

    jvm学习_第6张图片

    回收后内存状态:
    jvm学习_第7张图片

    特点
    实现效率高
    需要更多的额外内存

  • 标记-整理
    先标记哪些对象可以回收,然后将可回收对象移动到内存的一端
    回收前内存状态:

    jvm学习_第8张图片

    回收后内存状态:
    jvm学习_第9张图片

3 分代收集
实际各种jvm都是用分代收集算法来进行垃圾回收。
jvm把堆内存分为新生代(对象存活率低,每次垃圾回收此区域大部分对象都被回收)和老年代(对象存活率高,每次垃圾回收此区域很少对象被回收),jvm根据对象特点在相应区域分配和回收对象内存。
新生代内存分为:EDEN区和2个SURVIVOR区,EDEN/SURVIVOR=8/1,采用复制算法进行垃圾回收。

下面举实例来说明内存分配与回收的过程
内存配置说明:
EDEN:8M, SURVIVOR:1M
老年代:40M

jvm学习_第10张图片

a) 新建对象a,b,c,需要内存1m,2m,3m
b) jvm在EDEN区给a,b,c分配内存,运行一段时间后,对象b,c不可达,处于可回收状态
c) 新建对象d,需要内存4m
d) 此时EDEN区只剩4m内存不足以为对象d分配内存,触发minor gc
e) EDEN存活的对象为a,把对象##

运行时数据区

a复制到SURVIVOR_a,把EDEN区域清空
f) 把对象d分配到EDEN区,此时EDEN占用4m,SURVIIVOR_a占1m
g) 新建对象e,需要对象5m
接下来分两种情况
1 如果PretenureSizeThreshold<5m
对象e被直接分配到老年代

2 如果PretenureSizeThreshold>5m
a) EDEN区剩余内存不足以分配,触发minor gc
b) 把EDEN和SURVIVOR_a区中的存活对象复制到SURVIVOR_b,然后将EDEN和SURVIVOR_a清空
c) SURVIVOR_b不足以存放复制来的对象,直接把对象d移到老年代
d) 把对象e分配在EDEN

说明
1 如果对象在SURVIVOR中经过多次(默认配置为15次)minor gc,没有被回收,该对象会被移到老年代。对象年纪(经历过的gc次数)信息在对象头中存储
2 如果老年代中的内存不足以分配会触发full gc,如果full gc后内存仍不足,会OOM
3 一般来说,minor gc的频率更高,时间更短。full gc的频率更低,花费时间更长。

类文件结构

现在基于jvm平台的语言不仅有java,还有groovy,scala和google最近一直在推的kotlin等。
所有这些语言的语法和所用的编译器可能都不同,但只要它们编译生成的class文件(字节码)符合规范,就能在虚拟机上运行。

class文件是一组以8位为单位的2进制数据流。
class文件中有两种数据类型:无符号数和表。class文件的数据项如下:


jvm学习_第11张图片

常量项的结构如下:

jvm学习_第12张图片
jvm学习_第13张图片

举例说明
以最简单的Hello World代码为例,分析编译生成的class文件,来学习class的文件结构。

Hello.java文件如下:

public class Hello{
    public static void main(String[] args){
        System.out.println("Hello World!");
    }
}

Hello.class文件如下:


jvm学习_第14张图片

class 文件分析

  1. magic code (u4)
    文件最头4个字节为magic code:CAFE BABE。
    用来标识此文件为可以被虚拟机接收的class文件。
  2. version
    接下来4字节为版本号:0000(副版本) 0034(主版本)。
    代表class版本号为:52.0,对应jdk1.8。
  3. 常量池数量
    接下来2字节为001d(29)。
    代表常量池有28项常量。第0项常量预留,用来表达不指向任何常量的含义。
  4. 常量解析
    接下来字段为28个常量的定义。
  1. 第1个常量
    第一个字节为0A,代表为Method_ref info。
    根据上图常量结构,得知method_ref 表中,接下来两个U2分别指向两个常量索引0006(const_pool的第6个常量)和 000F(const_pool的第15个常量),分别代表指向声明方法的类描述符和指向名称及类型的描述符。
    ** 结合下面javap 生成的文件 ,我们可以找到#6,#15,然后依次找到最终含义 **
    2)第2个常量
    第一个字节 为09,代表为Field_ref info。
    ...............后续常量解析和上面同理。

用javap 命令可以对class文件进行分析

javap Hello.class 
Compiled from "Hello.java"
public class Hello {
  public Hello();
  public static void main(java.lang.String[]);
}

Classfile /home/fll/code/javaTest/Hello.class
  Last modified 2017-5-31; size 416 bytes
  MD5 checksum 7c04c33532f23f7d4aca1d0ec468a57f
  Compiled from "Hello.java"
public class Hello
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // Hello World!
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // Hello
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Hello.java
  #15 = NameAndType        #7:#8          // "":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               Hello World!
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               Hello
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public Hello();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 8
}
SourceFile: "Hello.java"

类加载机制

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,解析和初始化,并最终形成可以被虚拟机直接使用的java类型

类的生命周期

jvm学习_第15张图片

类初始化时机

  1. 遇到new,getstatic,putstatic或invokestatic指令时。(使用new 实例化对象,读取或设置类的静态字段_** 被final 修饰定义时赋初值除外**,调用类的静态方法)
  2. 通过反射调用一个未初始化的类
  3. 初始化一个类时,要先初始化其父类
  4. 虚拟机启动时,会初始化Main主类

类加载全过程
包括加载、验证、准备、解析和初始化整个过程。

  • 加载
    1. 通过类的全限定名来加载类的二进制字节流(不限定来源,可以是jar,war,反射生成,只要结构符合类文件结构)
    2. 将这个类代表的静态结构转换为方法区的运行时结构
    3. 在内存中生成代表这个类的Class对象,作为方法区这个类的各种数据的入口。
  • 验证
    确保加载的字节流符合当前虚拟机的要求,不会危及运行安全。
  • 准备
    为类变量在内存方法区中分配内存并设置初始值。(这里的初始值并不是指程序制定的默认值,而是指数据类型的零值)
private static int a =1;//此阶段后a会被设置初值为0,后续初始化后才会被赋值为1
  • 解析
    将常量池内的符号引用替换为直接引用
  • 初始化
    执行类构造器()方法
    ()由编译器收集类变量赋值语句和静态语句块,合并而成。收集顺序和语句在源代码文件中的出现顺序一致。
    静态语句块可以放在变量定义前,但语句内对变量的操作只能有赋值
/**
*可以正确执行,输出i值为2
**/
public class CliTest {
    static {
        i = 4;
    }
    private static int i =2;

    public static void print(){
        System.out.println(i);
    }

    public static void main(String[] args) {
        print();
    }
}
/**
*不能正确执行,报非法向前引用
**/
public class CliTest {
  static {
      i = 4;
      i++;
  }
  private static int i =2;

  public static void print(){
      System.out.println(i);
  }

  public static void main(String[] args) {
      print();
  }
}

虚拟机会保证一个类的()方法在多线程环境下被正确执行。
多线程下,一个线程进入执行()方法,其它线程会阻塞、等待。(但静态语句块只会被执行一次,即使阻塞解除,其它线程也不会再执行静态语句块)

类加载器

通过类全限定名加载类二进制字节流的动作是放在java虚拟机外实现得。我们可以通过java程序实现自己的类加载器。
类的唯一性,由加载这个类的加载器和类本身确定

双亲委派模型

类加载器种类:

  • 启动类加载器(Bootstrap ClassLoader) :属于java虚拟机的一部分。负责加载存放在\lib 或 -Xbootclasspath指定路径下的符合条件(仅通过文件名识别,如rt.jar)的类文件。
    ** 启动类加载器无法被程序直接引用。如果自定义类加载器,需要把加载请求委托给启动类加载器,直接用null代替即可 **
  • 扩展类加载器(Extension ClassLoader):由ExtClassLoader实现。负责加载存放在\lib\ext 或 java.ext.dirs指定的目录下的类文件。开发者可以直接使用。
  • 应用程序类加载器(Application ClassLoader):由AppClassLoader实现。用来加载用户类路径上的类文件。如果用户没有自定义类加载器默认用的就是这个类加载器。

双亲委派模型

jvm学习_第16张图片

除了启动类加载器都有自己的父类加载器。当一个类加载器收到类加载请求时,首先自己不会加载该类,而是把请求委派给自己的父类加载器。父加载器也会将请求委派给它的父类加载器,直到最终委派到启动类加载器。只有当父加载器反馈自己无法加载该类时,子类才会尝试去加载。


内存模型

jvm学习_第17张图片

你可能感兴趣的:(jvm学习)