【JVM】LXF_深入理解JVM专题(1-4)


前言

========================================================================================================
我不能保证写的每个地方都是对的,但是至少保证每一句话、每一行代码都经过了认真的推敲、仔细的斟酌。每一篇文章的背后,希望都能看到自己对于技术、对于生活的态度。

我相信乔布斯说的,只有那些疯狂到认为自己可以改变世界的人才能真正地改变世界。面对压力,我可以挑灯夜战、不眠不休;面对困难,我愿意迎难而上、永不退缩!

其实我想说的是,我只是一个程序员,这就是我现在纯粹人生的全部。

=========================================================================================================

这篇文章主要用来梳理LXF_JVM专题的学习脉络。

深入理解JVM虚拟机开篇:JVM介绍与知识脉络梳理

博主参考了这篇文章:学习Java虚拟机没用? 听听当事人是怎么说的!

深入理解JVM虚拟机1:JVM内存模型 与 永久代的消失

深入理解JVM虚拟机2:JVM垃圾回收基本原理和算法

博主参考了以下四篇文章:

Java GC系列(1): 垃圾回收简介

  • Java关键术语
  • Java HotSpot 虚拟机
  • JVM体系结构
  • Java堆内存

Java GC系列(2): Java垃圾回收是如何工作的?

  • 启动Java垃圾回收
  • Java垃圾回收过程
  • 垃圾回收中实例的终结
  • 对象什么时候符合垃圾回收的条件?
  • GC Scope 示例程序
  • GC OutOfMemoryError 的示例程序

Java GC系列(3): 垃圾回收器种类

  • Java有四种类型的垃圾回收器(Serial Garbage Collector, GC):
    • 串行垃圾回收器(Serial GC)
    • 并行垃圾回收器(Parallel GC)
    • 并发标记扫描垃圾回收器(CMS GC)
    • G1垃圾回收器(G1 GC)
  • 垃圾回收的JVM配置
    • 运行的垃圾回收器类型
    • GC的优化配置
    • 使用JVM GC参数的例子

Java GC系列(3): 垃圾回收监视和分析

  • Java GC监视和分析工具
  • Java VisualVM
    • 启动VisualVM
    • 安装可视化GC插件
    • 监视GC

1. 各种GC的触发时机(When)

  • GC类型
  • 触发时机(条件)

2. JVM垃圾判定算法

  1. 引用计数算法(Reference Counting)
  2. 可达性分析算法(根搜索算法)

3. 四种引用

  1. 强引用
  2. 软引用
  3. 弱引用
  4. 虚引用

4. JVM垃圾回收算法

  1. 标记—清除算法(Mark-Sweep)
  2. 复制算法(Copying)
  3. 标记—整理算法(Mark-Compact)
  4. 分代收集算法(Generational Collection)
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.log4j.Logger;
 
public class HelloWorld {
    private static Logger LOGGER = Logger.getLogger(HelloWorld.class.getName());
    public void sayHello(String message) {
        SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.YYYY");
        String today = formatter.format(new Date());
        LOGGER.info(today + ": " + message);
    }
}

深入理解JVM虚拟机3:垃圾回收器详解

转自 – Javadoop主页

Javadoop主页:在这里,一位优秀的Java程序员写了很多高质量的文章

这篇文章主要是翻译《Memory Management in the Java HotSpot Virtual Machine》白皮书的前四章内容。

一. 垃圾收集概念

  • 垃圾收集器的理想特征
  • 设计上的权衡
  • 什么是stop the world?
  • 性能指标
  • 分代收集介绍

二. J2SE 5.0 HotSpot JVM中的垃圾收集器

  • HotSpot 分代
  • 垃圾回收类型
  • 快速分配
  • 串行收集器
  • 并行收集器
  • 并行压缩收集器
  • CMS收集器

三. G1 垃圾收集器介绍

1. G1 总览
2. G1 工作流程
  • 年轻代收集

  • Old GC / 并发标记周期

  • Full GC

    1.concurrent mode failure

    2.晋升失败

    3.疏散失败

    4.大对象分配失败

3. G1 参数配置和最佳实践
  • 参数介绍
  • Java GC监视和分析工具

深入理解JVM虚拟机4:Java class介绍与解析实践

《自己动手写Java虚拟机》,作者利用go语言实现了一个简单的JVM,虽然没有完整实现JVM的所有功能,但是对于一些对JVM稍感兴趣的人来说,可读性还是很高的。作者讲解的很详细,每个过程都分为了一章,其中一部分就是讲解如何解析Class文件。

这本书不太厚,很快就读完了,读完后,收获颇丰。但是纸上得来终觉浅,绝知此事要躬行,我便尝试着自己解析Class文件。go语言虽然很优秀,但是终究不熟练,尤其是不太习惯其把类型放在变量之后的语法,还是老老实实用java吧。

项目地址:https://github.com/HalfStackDeveloper/ClassReader

一. Class文件

1. 什么是Class文件?
2. 基本结构

在原文中这一段看的不清楚,拷贝过来。

是不是一脸懵逼,不过java虚拟机规范中给出了class文件的基本格式,只要按照这个格式去解析就可以了:

ClassFile {
       u4 magic;
       u2 minor_version;
       u2 major_version;
       u2 constant_pool_count;
       cp_info constant_pool[constant_pool_count-1];
       u2 access_flags;
       u2 this_class;
       u2 super_class;
       u2 interfaces_count;
       u2 interfaces[interfaces_count];
       u2 fields_count;
       field_info fields[fields_count];
       u2 methods_count;
      method_info methods[methods_count];
       u2 attributes_count;
       attribute_info attributes[attributes_count];
}
3. class 文件的结构
4. 解析
字段类型

原文中代码段不清晰:

public class U1 {
    public static short read(InputStream inputStream) {
        byte[] bytes = new byte[1];
        try {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        short value = (short) (bytes[0] & 0xFF);
        return value;
    }
}
 
public class U2 {
    public static int read(InputStream inputStream) {
        byte[] bytes = new byte[2];
        try {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        int num = 0;
        for (int i= 0; i < bytes.length; i++) {
            num <<= 8;
            num |= (bytes[i] & 0xff);
        }
        return num;
    }
}                                                                                                                                                                                   
 
public class U4 {
    public static long read(InputStream inputStream) {
        byte[] bytes = new byte[4];
        try {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        long num = 0;
        for (int i= 0; i < bytes.length; i++) {
            num <<= 8;
            num |= (bytes[i] & 0xff);
        }
        return num;
    }
}
常量池

首先是读取魔数之类的基本信息:

FileInputStream inputStream = new FileInputStream(file);
ClassFile classFile = new ClassFile();
classFile.magic = U4.read(inputStream);
classFile.minorVersion = U2.read(inputStream);
classFile.majorVersion = U2.read(inputStream);

java虚拟机规范给出了常量池中每一项的格式:

cp_info {
    u1 tag;
    u1 info[]; 
}

读取常量池的大小,初始化常量池:

// 解析常量池
int constant_pool_count = U2.read(inputStream);
ConstantPool constantPool = new ConstantPool(constant_pool_count);
constantPool.read(inputStream);

接下来再逐个读取每项内容,并存储到数组cpInfo中,这里需要注意的是,cpInfo[]下标从1开始,0无效,且真正的常量池大小为constant_pool_count-1。

public class ConstantPool {
    public int constant_pool_count;
    public ConstantInfo[] cpInfo;
 
    public ConstantPool(int count) {
        constant_pool_count = count;
        cpInfo = new ConstantInfo[constant_pool_count];
    }
 
    public void read(InputStream inputStream) {
        for (int i = 1; i < constant_pool_count; i++) {
            short tag = U1.read(inputStream);
            ConstantInfo constantInfo = ConstantInfo.getConstantInfo(tag);
            constantInfo.read(inputStream);
            cpInfo[i] = constantInfo;
            if (tag == ConstantInfo.CONSTANT_Double || tag == ConstantInfo.CONSTANT_Long) {
                i++;
            }
        }
    }
}

先来看看CONSTANT_Utf8格式,这一项里面存放的是UTF-8编码的字符串:

CONSTANT_Utf8_info { 
    u1 tag;
    u2 length;
    u1 bytes[length]; 
}

那么如何读取这一项呢?

public class ConstantUtf8 extends ConstantInfo {
    public String value;
 
    @Override
    public void read(InputStream inputStream) {
        int length = U2.read(inputStream);
        byte[] bytes = new byte[length];
        try {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            value = readUtf8(bytes);
        } catch (UTFDataFormatException e) {
            e.printStackTrace();
        }
    }
 
    private String readUtf8(byte[] bytearr) throws UTFDataFormatException {
        //copy from java.io.DataInputStream.readUTF()
    }
}

很简单,首先读取这一项的字节数组长度,接着调用readUtf8(),将字节数组转化为String字符串。

再来看看CONSTANT_Class这一项,这一项存储的是类或者接口的符号引用:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

注意这里的name_index并不是直接的字符串,而是指向常量池中cpInfo数组的name_index项,且cpInfo[name_index]一定是CONSTANT_Utf8格式。

public class ConstantClass extends ConstantInfo {
    public int nameIndex;
 
    @Override
    public void read(InputStream inputStream) {
        nameIndex = U2.read(inputStream);
    }
}

常量池解析完毕后,就可以供后面的数据使用了,比方说ClassFile中的this_class指向的就是常量池中格式为CONSTANT_Class的某一项,那么我们就可以读取出类名:

int classIndex = U2.read(inputStream);
ConstantClass clazz = (ConstantClass) constantPool.cpInfo[classIndex];
ConstantUtf8 className = (ConstantUtf8) constantPool.cpInfo[clazz.nameIndex];
classFile.className = className.value;
System.out.print("classname:" + classFile.className + "\n");
字节码指令

ClassFile中的method_info,其格式如下:

method_info {
    u2 access_flags;		// 访问标志
    u2 name_index;			// 方法名索引
    u2 descriptor_index;	// 方法描述符索引
    u2 attributes_count;	
    attribute_info attributes[attributes_count];	// 属性数组
}

属性的通用格式为:

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

Code属性的具体格式为:

Code_attribute {
    u2 attribute_name_index; u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length; 
    {
        u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

接下来我们就可以解析字节码了:

for (int j = 0; j < methodInfo.attributesCount; j++) {
    if (methodInfo.attributes[j] instanceof CodeAttribute) {
        CodeAttribute codeAttribute = (CodeAttribute) methodInfo.attributes[j];
        for (int m = 0; m < codeAttribute.codeLength; m++) {
            short code = codeAttribute.code[m];
            System.out.print(InstructionTable.getInstruction(code) + "\n");
        }
    }
}