class文件介绍

发布在 java, jvm

非原创声明

本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

*.class文件介绍

一般来讲.class文件是.java文件在编译器编译后生成的jvm能够运行的文件,*.class文件又常被称为字节码文件。java在创始之初,就提倡“一次编写,处处运行的概念”,在当今编程圈中这个概念早已不是什么特例。java通过将开发人员所编写的java代码编译成class文件,然后由jvm虚拟机在执行时将不分平台的class文件中的字节码,再翻译成机器码,交给硬件执行。java就是靠jvm虚拟机的这个设计来实现与平台无关的特性的。class文件不但与硬件平台和操作系统无关,也和具体的编程语言无关,就目前来说,如函数式编程语言scala与Groovy都可以通过自己的编译器将源代码编译成class文件,在jvm上运行。
综合来讲,class文件有以下两点特性:

  • 与硬件和操作系统平台无关
  • 与源码所使用的编程语言无关

class类文件的结构

每一个class文件都唯一对应着java类或接口枚举等定义信息,但类不一定都定义在class文件中,类可能是由类加载器动态生成的。

class文件所以被称之为字节码据我猜测可能是因为class文件以8位(1字节)为单位进行存储的二进制信息。字节码中各个数据项目严格按照顺序紧凑的排列,中间没有任何分割符。在需要存储整型或浮点型这些大于8位的数据项目时,则会使用Big-Endian的字节序进行存储,将最高位字节放在地址最低位,最低位字节放在地址最高位。

class文件格式采用表来存储数据,表中有无符号数和表两种数据类型。对于这两种数据类型说明如下:

  • 无符号数: 无符号数是基本的数据类型,可以用来表示数字、索引引用、数量值或者按照UTF-8编码构成的字符串。如u1,u2,u4,u8分别代表1,2,4,8个字节字节的无符号数。
  • : 表是由多个无符号数或者其它表作为数据项所组成的复合数据结构,表用于描述层次关系和复合的数据结构,整个class文件就是一张表。通常表以 _info 结尾。如表1就是一个class的表示例。
类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 asscess_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods method_count
u2 attributes_count 1
attribute_info attributes attributes_count

表 1 class文件格式

class文件的顺序和格式必须严格实现按照上述规则,否则jvm将不能识别执行。

Magic Number 魔数

魔数是一个和文件后缀名相似的用于文件格式识别的约定,一般规定文件内容开头的前几个字节为文件的魔数。不同于文件后缀名很容易被用户以重命名的方式进行更改,文件的魔数作为识别手段可以更安全的确定文件的可用性。

class文件使用前4个字节作为魔数,来确定.class文件是否是一个能够被虚拟机识别的文件,其值是 0xCAFFEBABE 。

版本

class文件的第5-6个字节代表的可执行该class文件的目标虚拟机的最低次版本号(Minor Version),第7-8个字节是主版本号(Major Version)。java虚拟机可以运行比当前虚拟机版本号低的class文件,拒绝运行版本号不合法,或比自己版本高的class文件。JDK1.1的版本号是45,之后的每个大版本发布都把主版本号加1,如JDK1.2主版本号是46,JDK8的版本号是52。
JDK在编译java文件是可以通过 javac -target 1.6 ...命令来指定编译后的class文件可以在1.6的虚拟机版本上运行。

常量池

常量池是class文件中第一个表类型的数据项目,常量池是class文件中的资源仓库,是class文件结构中与其它项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一。

class文件里紧随版本之后的数据项是常量池,由于常量池的数量是不固定的,所以在常量池的数据项之前放置的有一个u2类型的数据,代表常量池的大小。常量池大小的初始值是1,如常量池的大小的数据如果显示的是10,就代表该class文件中有9个常量。
常量池中主要存放两大类常量:Literal(字面常量)、Symbolic Reference(符号引用常量)。字面常量类似于java中常量的概念,如文本字符串、final关键字所声明的常量等。而符号引用常量则是编译中的概念,主要包括以下三种类型的常量。

  • 符号常量

    • 类和接口的全限定名(Fully Qualified Name)
    • 字段名称和描述符(Descriptor)
    • 方法的名称和描述符

    class文件中不会保存各个方法或字段在内存中的布局信息,而是在虚拟机加载class文件时进行动态的连接。虚拟机在运行class文件时从常量池中获取对应的符号引用,再在创建类或者运行时解析连接到具体的内存地址当中。
    常量池中的每一个常量都是一个表,在JDK8中有14种表结构的常量表。如表 2 所示。

类型 标识 描述 
CONSTANT_utf8_info 1  UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 长整型字面量
CONSTANT_Double_info 双精度浮点型字面量
CONSTANT_Class_info 类或接口的符号引用
CONSTANT_String_info 字符串类型字面量
CONSTANT_Fieldref_info 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MothodType_info 16 标志方法类型
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

表 2 常量池数据项目类型表

这14种常量结构表开始的第一个字节都是一个u1类型的标识位,其值就是表2中每项常量表类型所对应的标识列的值,代表当前常量属于哪种常量类型。这14种常量类型各自有自己不同的表结构,详情如表 3 所示。

常量 项目 类型 描述
CONSTANT_Utf8_info tag u1 值为1
length u2 UTF-8编码的字符串占用的字节数
bytes u1 utf-8编码的字符串
CONSTANT_Integer_info tag u1 值为3
bytes u4 按照Big-Endian存储的int值
CONSTANT_Float_info tag u1 4
bytes u4 按照Big-Endian存储的float值
CONSTANT_Long_info tag u1 5
bytes u8 按照Big-Endian存储的long值
CONSTANT_Double_info tag u1 6
bytes u8 按照Big-Endian存储的long值double值
CONSTANT_Class_info tag u1 7
index u2 指向全限定名常量项的索引
CONSTANT_String_info tag u1 8
index u2 指向字符串常量的索引
CONSTANT_Fieldref_info tag u1 9
index u2 指向声明字段的类或接口描述符CONSTANT_Class_info的索引值
index u2 指向CONSTANT_NameAndType_info的索引值
CONSTANT_Methodref_info tag u1 10
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引值
index u2 指向CONSTANT_NameAndType_info的索引值
CONSTANT_InterfaceMethodref_info tag u1 11
index u2 指向声明方法的接口描述符CONSTANT_Class_info的索引值
index u2 指向CONSTANT_NameAndType_info的索引值
CONSTANT_NameAndType_info tag u1 12
index u2 指向该字段或方法名称常量的索引值
index u2 指向该字段或方法描述符常量的索引值
CONSTANT_MethodHandle_info tag u1 15
reference_kind u1 值必须1~9,它决定了方法句柄的的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_index u2 对常量池的有效索引
CONSTANT_MethodType_info tag u1 16
description_index u2 对常量池中方法描述符的有效索引常量池在该处的索引必须是CONSTANT_Utf8_info的结构,表示方法的描述符。
CONSTANT_InvokeDynamic_info tag u1 18
bootstap_method_attr_index u2 对当前class文件中引导方法表的bootstap_methods[]数组的有效索引
name_and_type_index u2 对当前常量池的有效索引,常量池在此处必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述。

表 3 常量池中14中常量的结构总表

在表 3 中可以看到class文件所支持的所有的常量的结构信息,另外由于class中的类名、方法、字段都要用CONSTANT_Utf8_info型的常量来描述名称,所以CONSTANT_Utf8_info的最大长度也是java中类名、方法名或字段的最大长度65535,如果超出这个最大长度,便会无法编译。

访问标识

在class文件中位于常量池之后的是访问权限标识。class文件的访问标识是用于修饰这个class文件所代表的的类或接口本身的一些属性。访问标识是用一个u2类型的数据来代表的,如0x0001代表public,0x0020代表这是一个接口。具体的访问标识和标志值的对应关系见表4.访问标识是按位进行标识的,一个u2类型的数据有16位,目前其中的8位都被赋予了实际意义。

标识名称 标识值 意义
ACC_PUBLIC 0X0001 public的访问权限
ACC_FINAL 0x0010 如果该class文件代表一个类,那么该类是final修饰的(只能类才能拥有此标识)
ACC_SUPER 0x0020 使用invokespecial字节码指令的新语义
ACC_INTERFACE 0x0200 接口
ACC_ABSTRACT 0x0400 abstract(只有接口和抽象类有效)
ACC_SYNTHETIC 0x10000 本类不是用户代码所产生的类
ACC_ANNOTATION 0x2000 注解
ACC_ENUM 0x4000 枚举

表4 访问标识和标志值的对应关系表

类索引

类索引是一个u2类型的数据,用于确定这个类的全限定名。其索引值指向一个类型为CONSTANT_Class_info的常量。

父类索引

父类索引指向着当前类所继承的类的父类的全限定名,其类型信息与类索引相同。所有java类都有父类索引。

接口索引

java不支持多重继承,所以只有一个父类索引。但一个java类或接口可以实现多个其它接口,所以class的接口索引是一个数量并不固定的集合。
在声明接口索引之前,class文件首先会声明一个u2类型的数据代表接口索引的总数量,其后面紧跟者就是接口索引集合,如果该类没有实现任何接口,那么接口索引的总数量的值就是0。接口索引同样也指向常量池中的CONSTANT_Class_info类型的常量,使用CONSTANT_Utf8_info的字符串常量来表示接口的全限定名。

字段表集合

20170701 00:10:00 编辑到字段表集合

字段集合用于描述接口或类中所声明的全局变量。如public,static、final等词都是用来描述某个字段的修饰词。因为java中可用来修饰字段的修饰符有限,所以class文件对于字段的修饰符的表示是通过使用标识位来实现的。表5详细分析了字段表的数据结构。

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attrobute_info attributes attributes_count

表5 字段表的数据结构

access_flags标识位中存放的是字段的修饰符,使用标志位进行标识区分。具体区分方式如表6字段访问标识表所示。

标识名称 标识值 含义
ACC_PUBLIC 0x0001 public
ACC_PRIVATE 0x0002 private
ACC_PROTECTED 0x0004 protected
ACC_STATIC 0x0008 static
ACC_FINAL 0x0010 final
ACC_VOLATILE 0x0040 volatile
ACC_TRANSIENT 0x0080 transient
ACC_SYNTHETIC 0x1000 该字段是否由编译器自动生成的
ACC_ENUM 0x4000 字段是否是enum

表6 字段访问标识对应表

name_index 和 descriptor_index 都是对常量池索引的引用,name_index引用的是字段的名称,descriptor_index引用的是字段和方法的描述符。

20170801 00:10:00 编辑到字段表集合

引用

本文是对class文件的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java Virtual Machine Specification》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到 weiwei02@vip.qq.com

from weiwei.wang 20170625

评论和共享

java内存模型

发布在 java, jvm

非原创声明

本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

JAVA内存模型

犹记得大学时操作系统课上,我们迷茫的眼神注视着带着厚眼镜教授向我们一遍遍的强调,一个程序最少有一个进程组成,进程是操作系统提供独立资源供应用程序运行的基本单位。另外老师向我们讲到,为了更好的提高计算机的并行计算能力,计算机科学家们又设计了线程。线程是比进程更小的单位,一个进程可以由多个线程组成。同时线程也是在得到cpu时间片时可运行的最小的单位。尤记得在这些理论基础下,我慢慢的学会了使用C在LINUX环境下使用多进程和多线程进行编程。这些并发API都是LINUX提供的标准API,程序在编译链接之后可以直接调用通过系统内核创建进程或线程,在类UNIX操作系统下这样去做可以让程序有更高的性能。但是这种编程方式所编写出来的代码是与操作系统绑定的,我在linux下明明可以完美运行的代码,在windows下连编译甚至都做不到。除非是用 windows API重新把与操作系统进行交互的那些代码给替换掉,否则就不要想着让程序去跨平台运行了。
在进行并发编程时相对于C,我更喜欢java的编程体验。java消除操作系统之间的差异,原生的对多线程应用提供了很好的支持,特别是在jdk1.5之后,jdk还提供了currency包,更好的让程序员们无需去关注并发的难点与细节,更专心的关注应用的业务需求实现。
在多线程编程中最长遇到的问题就是线程安全问题,其中又以内存中的数据安全问题最为常见(这个数据安全指的是发生脏读、幻读等并发编程中会遇到的错误)。为了消除不同硬件和操作系统对内存操作的差异,在硬件设备和操作系统的内存模型之上,java虚拟机规范定义了一种java内存模型(JAVA Memory Model,简称JMM),将内存分为了工作内存和主内存。JMM主要定义了JVM中在内存中操作变量的规则和细节,用来解决在并发竞争对变量操作时可能会发生的各种问题。JMM完全兼容CPU的多级cache机制,并且支持编译器的代码重排序。
注意:JMM内存模型的概念不同于jvm中6大内存区域的概念,两者不可强行混为一谈。

主内存和工作内存

JMM规定了所有的全局变量都存于主内存(Main Memory)中,一般情况下这些全局变量都会被保存到堆里。每个线程都有自己的工作内存(Working Memory),工作内存中会保存当前线程所需用到的主内存中全局变量的拷贝和自己的局部变量。一般情况下工作内存指的是栈内存,从物理上来讲,工作内存一般情况下都会工作于cpu的cache里。一个线程对全局变量的任何操作必须在自己的工作内存中进行,不允许直接操作工作内存。不同线程不能访问对方的工作内容,只能通过主内存进行数据交换。

工作内存在对主内存中变量做拷贝时,如果变量是基本类型的,则会拷贝其值;如果是引用类型的,那么仅仅会拷贝其引用

JMM内存操作

JMM定义了8种主内存与工作内存之间的具体交互操作,虚拟机在实现JMM时必须保证每种内存操作都是原子的。表1详细列出了JMM的8种内存操作。

指令 操作名 作用区域 描述
lock 锁定 主内存 把一个变量标识为一个线程独占的状态
unlock 解锁 主内存 把一个处于锁定状态的变量释放出来
read 读取 主内存 把一个变量的值从主内存读取到工作内存中,以便于随后的load指令使用
load 载入 工作内存 把read指令从主内存中读取的变量值放入到工作内存的变量副本中
use 使用 工作内存 将工作内存中一个变量的值传递给执行引擎
assign 赋值 工作内存 把一个从执行引擎接收到的值赋给工作内存的变量
store 存储 工作内存 把一个工作内存中变量的值传送到主内存中,以便于随后的write指令使用
write 写入 主内存 把store指令从工作内存传出的变量的值写入到主内存的变量中

表1 JMM内存操作指令表

20170702 01:48 编辑到JMM内存操作指令表

JMM规定了在使用以上8种内存操作时必须遵守以下规则:

  • read,load或store,write必须成对出现。如:执行read操作从主内存读取一个变量后,必须在工作内存使用load载入这个变量。两者之间的顺序不可错,但两者之间可以穿插其它指令。同理,在工作内存中对一个变量使用了store指令,必须在主内存中使用write指令进行写入。
  • 不允许一个线程丢弃它最近做的assign操作。变量在工作内存中改变了之后必须将这个变化同步到主内存中。
  • 不允许一个线程没有发生过assign操作就将数据从工作内存同步到主内存。
  • 只能在主内存中新建全局变量,不能在工作内存中直接使用一个未被初始化的变量。可以使用assign和load指令在工作内存对一个变量进行初始化操作。
  • 一个变量在同一时刻只能被一个线程执行lock操作。
  • 如果对一个变量执行lock,那将清空工作内存中此变量的值。在执行引擎使用这个变量之前,需要重新load或assign重新初始化变量的值。
  • unlock只能解锁被本线程锁定的变量。
  • 对一个变量执行unlock之前,必须将此变量同步回主内存。

volatile 关键字

volatile是java语言所提供的关键字,使java最轻量级的内存同步的措施。与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。

java中锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。
volatile 变量具有 synchronized 的可见性特性,但是不具备锁的原子特性。所以即便volatile没有不一致的问题,但volatile变量在并发的运算下并不是原子操作,所以依然可能会有安全问题。

package com.github.weiwei02.jvm.jmm.volatile_test;

/**
* volatile 线程安全性测试
* @author Wang Weiwei <email>weiwei02@vip.qq.com / weiwei.wang@100credit.com</email>
* @version 1.0
* @sine 2017/7/2
*/
public class VolatileTest {
public static volatile int race = 0;
public static final int THREAD_COUNT=20;

public static void main(String[] args) {
    Thread threads[] = new Thread[THREAD_COUNT];
    System.out.println(race);
    for (int i =0; i < THREAD_COUNT; i++){
        threads[i] = new Thread(() -> {
            for (int j = 0; j < 1000 ; j++) {
                increase();
            }
        });
        threads[i].start();
    }


    //等待所有累加线程都结束
    while (Thread.activeCount() > 1){
        Thread.yield();
    }

    //等待所有线程执行完毕之后,打印race的最终值
    System.out.println(race);
}

private static void increase() {
    race++;
}
}

代码1 volatile 线程安全测试

程序的执行结果如图2所示。
volatile 线程安全测试
图2 volatile 线程安全测试

这个程序执行的结果应该是20000,可实际执行所得到的结果只有18439.并且多次执行本程序都会得到不一样的结果。所以会出现这种情况的原因是 race++ 这个操作并不是原子操作,volatile关键字只能保证在使用race变量时,工作线程能够从主内存中拿取最新的race的值,并不能保证进行过++操作之后写回到主内存期间其它线程没有对race变量进行修改。
volatile关键字只能保证可见性,只有在以下两种条件下,我们可以认为volatile是线程安全的;

  1. 运算结果并不依赖变量的当前值,或者能够保证只有一个线程修改变量的值。
  2. 变量不需要与其它的变脸共同参与不变约束。

    当不符合这两种条件时,想要保证volatile变量的原子性,只能通过 机制来实现。

    volatile变量的第二个重要作用是禁止编译器进行指令重排序。普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。从硬件层面上来讲,是指CPU允许多条指令不按程序规定的顺序分开发送到个相应的电路单元来处理。CPU必须要正确的处理指令的依赖情况,不能将指令任意的排序。比如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3将地址B中的值减去3。在这一系列指令中,指令1,2之间是有依赖关系的,不能进行重排序。但指令3的执行顺序可以改到指令1之前也可以放到指令1之后,只要能够保证在执行完毕这三条指令程序中A,B的值是正确的即可。volatile在某些情况下性能高于锁,由于虚拟机对锁实行有多种消除和优化,我们很难衡量volatile会比锁快多少。

原子性、可见性和有序性

20170803 23:11:51 原子性、可见性和有序性
JAVA内存模型的核心问题时为了解决多线程应用中的原子性、可见性与有序性的问题,保证了这三点才能使多线程应用有数据安全性可言。

  • 原子性 (Atomicity)
    由java内存模型来直接保证原子性的操作包括read、load、assign、use、store和write。对于一个变量的基本读写操作都是具有原子性的。如果需要对多种这几种基本读写操作的操作集合保证原子性,可以使用lock和unlock。
  • 可见性(Visibility)
    可见性是指当变量的值被一个线程修改了之后,其它线程能够立即知道这个修改。JAVA内存模型时通过变量内容修改后立刻同步到主内存,读取变量的值之前先从内存刷新这种方式来保证可见性。JAVA中除了volatile之外还有synchronized、final两个关键字能够实现内存可见性。synchronized的可见性是由”对一个变量执行unlock操作之前,必须先把此变量同步会主内存中(执行store,write操作)”这条规则获得的。final的可见性是由于被final修饰的变量在构造器中一旦被初始化完成,并且构造器没有吧this的引用传递出去(this引用是一件非常危险的事,其它线程可能通过这个引用访问到初始化一半的对象),,那在其它线程中就能看见final变量的值。
  • 有序性
    如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句指的是“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句指的是“指令重排序”现象和“工作内存与主内存同步延迟”现象。

先行发生原则

20170804 02:02:23 先行发生原则

先行发生原则(Happens-before)是JAVA内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,操作A造成的影响就能被操作B观察到。JAVA只能怪的先行发生原则无需任何同步协助,可以直接编码使用。以下列出JAVA中的先行发生原则,如果两个操作之间的关系不在下面的规则中,且无法通过下面的规则推导出来,虚拟机就能随意的对他们进行指令重排序。

  • 程序次序规则(Program Order Rule)
    :在一个线程内,按照程序的代码顺序,书写在前的操作先于书写在后面的操作发生。准确的说,应该是控制流顺序而不是程序代码顺序,因为需要考虑分支、循环等结构。

  • 管程锁定规则(Monitor Lock Rule)
    : 一个unlocak操作先行发生于后面对同一个锁的lock操作。

  • volatile变量规则(Volatile Variable Rule)
    :对一个volatile的写操作,先行发生于后面对这个变量的读操作。

  • 线程启动规则(Thread Start Rule)
    :线程的start方法先行发生于此线程的每一个动作。

  • 线程终止规则(Thread Termination Rule)
    : 线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.jion()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  • 线程中断原则(Thread Interruption Rule)
    :对线程的interrupt()方法的调用先行发生于被中断的线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

  • 对象终结规则(Finalizer Rule)
    :一个对象的初始化完成(够构造函数执行结束)先行发生于它的fnalize()方法开始。

  • 传递性(Transitivity)
    :如果A操作先行发生于B操作,B操作先行发生于C操作,就可以得出A先行发生于C的结论。

    时间先后顺序与先行发生原则之间基本没有关系,所以当我们需要衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。

    20170804 23:59:46 先行发生原则

引用

本文是对class文件的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java Virtual Machine Specification》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》

关于

原文链接 https://weiwei02.github.io/2017/07/02/jvm/001-java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/
我的github: https://github.com/weiwei02/
我相信技术能够改变世界。

评论和共享

  • 第 1 页 共 1 页
Copyrights © 2017 weiwei02. All Rights Reserved. github空间地址: https://weiwei02.github.io/ 国内空间地址: https://weiwei02.cording.me/
作者的图片

weiwei02

技术,改变世界


软件工程师


北京,海淀
国外主站 国内主站