jvm学习

2017-06-27  本文已影响0人  范小闫

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

前言

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

运行时数据区

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

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

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

类在内存的布局

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

垃圾回收

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

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

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

2 垃圾收集算法

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

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

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,把对象## <p id="runningData">运行时数据区</p>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文件的数据项如下:


常量项的结构如下:

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

Hello.java文件如下:

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

Hello.class文件如下:


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."<init>":()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               <init>
   #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          // "<init>":()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."<init>":()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类型

类的生命周期

类初始化时机

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

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

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();
  }
}

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

类加载器

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

双亲委派模型

类加载器种类:

双亲委派模型


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

内存模型

上一篇 下一篇

猜你喜欢

热点阅读