虚拟机类加载机制
1、概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
虚拟机中,类加载(检查)的主体部分是在运行期而不是编译期,这是动态类型语言的关键特征。如JavaScript、Python、Ruby等都是动态语言
如下简单的代码:
obj.println("hello world");
当然这样的代码在类的检查中已经不合格,但是在JavaScript这样的动态语言中,若obj本身是没有类型的,而不会确定方法所在的具体类型(即方法接受者不固定)。“变量无类型而变量值才有类型”这个特点也是动态类型语言的一个重要特征。Python就是这样的动态语言。
Java、C++就是常用的静态类型语言,其编译期就进行类型检查过程。如上代码,Java语言在编译期间已将println(String)方法完整的符号应用生成出来,作为方法调用指定的参数存储在Class文件中。这个符号引用包含了此方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,虚拟机可以翻译出这个方法的直接应用。
2、类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:
图、类的生命周期
必须对类进行“初始化”的几种情况:
1、遇到new、getstatic、putstatic、incokestatic这4个字节码指令时,若类没有进行初始化,则需要先触发其初始化。
new:对使用new关键字实例化对象的时候
getstatic、putstatic: 读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
incokestatic: 调用一个类的静态方法的时候
new一个实例对象的例子:
//jvm执行new命令的时候,创建对象,分配内存,初始化内存空间的值为零,并开始按程序员的意愿执行init方法,即构造器方法
Person person2 = new Person(8); //为Person对象的age属性初始化了一个值:8
Person person3= new Person(8,"肖梦含"); //为Person对象的age、name属性初始化了值:8、肖梦含
getstatic获取静态字段 (很大可能举例错误,以后真正深入理解再举例子)
packagecom.xiaoxiao.newInit;public classTestStudent {public static void main(String[] args) throwsException {
Object object1= Student.class; //通过Student的class字段(或者说静态变量)获取其对象//备注: jvm书上说的是,当你new出对象时,Student对象还包含【到类型数据的指针】,其可获取类信息,而Student.class不就是获取类信息吗?
System.out.println(object1);
String className= Student.class.getName();
System.out.println(className);
System.out.println("----------------------这是一首简单的小情歌---------------------");
Student student= newStudent();
student.setAge(5);
System.out.println(student.getAge());//获取静态字段的值
Object object2 = student.getClass();//通过指针student,调用方法getClass()获取其对象
System.out.println(object2);
System.out.println(object1== object2); //true,说明Student.class和student.getClass()都是获取对象
}
}
2、使用java.lang.reflect包的方法对类进行反射调用的时候
//the keyword void,array,enum,The primiteve Java Type(8大基础类型)都是 a class
Class> classReference = Student.class; //获取对象get the Class object using a class literal
Class> classReferenceOfotheMethod = new Student().getClass(); //先实例化,再调用其getClass()方法获取对象
Constructor> constructor1 = classReference.getConstructor(double.class);//要指定构造器参数的类型(前提是Studen类具有double类型参数的构造器)
3、当初始化一个类的时候, 如果发现其父类还没有进行初始化,则需要先触发
子类继承父类,并编写构造器方法,然后实例化子类,查看结果。
定义分类:Person类,定义无参构造器与有参构造器
packagecom.xiaoxiao.newInit;public classPerson {
String name;intage;publicPerson() {
System.out.println("这是一首简单的小情歌");
System.out.println("并不是所有的牛奶都叫特仑苏");
}public Person(intage, String name){this.age =age;this.name =name;
}public Person(intage){this.age =age;
}
@OverridepublicString toString() {return "Person [name=" + name + ", age=" + age + "]";
}
}
定义子类PersonSon,继承上面的父类Person,如下所示:
packagecom.xiaoxiao.newInit;public class PersonChild extendsPerson{
String address= "龙港大道高湖小区";publicPersonChild() {
System.out.println("-------------这是一首简单的小情歌,并不是所有的牛奶都叫特仑苏----------");
System.out.println("我是儿子,我是继承者");
}publicPersonChild(String address){this.address =address;
}
@OverridepublicString toString() {return "PersonChild [address=" + address + "]";
}
}
测试:
packagecom.xiaoxiao.newInit;public classTestOom {public static voidmain(String[] args) {
PersonChild personChild= newPersonChild();
System.out.println(personChild);
}
}
控制台输出结果如下:
这是一首简单的小情歌
并不是所有的牛奶都叫特仑苏-------------这是一首简单的小情歌,并不是所有的牛奶都叫特仑苏----------我是儿子,我是继承者
PersonChild [address=龙港大道高湖小区]
解释如下:
使用new关键字实例化(子类)对象的时候,即new字节码指令出现之后,虚拟机执行情况如下:
优先初始化父类,调用其默认的无参的构造器,输出“这是一首简单的小情歌”、“并不是所有的牛奶都加特仑苏”;
然后初始化子类,调用其默认的无擦构造器,输出“-------这是一首简单的小情歌-------”、“我是儿子”;
若子类、父类都有实例化数据,优先使用子类的实例化数据,输出有
PersonChild [address=龙港大道高湖小区]
若父类无实例化数据,则使用父类的实例化数据,输出有
Person [name=null, age=0]
3、当虚拟机启动时,用户需要指定一个要执行的类(包括main()方法的那个类),虚拟机会初始化这个主类。
例子如下:
packagecom.xiaoxiao.newInit;public classTestOom {public static voidmain(String[] args) {
TestOom testOom= newTestOom();
PersonChild personChild= newPersonChild();
System.out.println(personChild);
System.out.println("这是一首简单的小情歌,并不是所有的牛奶都叫特仑苏");
}publicTestOom() {
System.out.println("这才是真正的大Boss:"+"main方法的主类优先执行类加载过程中的初始化工作。"+"\n");
}
}
结果如下:
这才是真正的大Boss:main方法的主类优先执行类加载过程中的初始化工作。
这是一首简单的小情歌
并不是所有的牛奶都叫特仑苏-------------这是一首简单的小情歌,并不是所有的牛奶都叫特仑苏----------我是儿子,我是继承者
Person [name=null, age=0]
这是一首简单的小情歌,并不是所有的牛奶都叫特仑苏
3、类加载的过程
类加载包含:加载、验证、准备、解析和初始化这五个阶段
3.1、加载
“加载”是“类加载”(Class Loading)过程的一个阶段,在加载阶段,虚拟机需要完成以下3件事:
通过一个类的全限定名来获取定义此类的二进制字节流(并未指明二进制字节流要从一个Class文件中获取)
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存(Java Heap)中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
对于数组类而言,情况有不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但是数组类的元素类型(Element Type,指的是去掉所有维度的类型)最终还是要靠类加载器去创建。
3.2、验证
这一阶段的目的是 为了确保Class文件的字节流中包含的细腻符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段的工作量在虚拟机的类加载子系统中占了相当大的一部分。
验证阶段大致包含4个动作:
文件格式解析: 主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合一个java类型信息的要求。故后面3个阶段全部基于方法区的存储结构进行的,不会再直接操作字节流。
元数据验证:主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
字节码验证:主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。即对类的方法体进行校验分析。
符号引用验证:主要是将符号引用转化为直接引用的时候,对类自身以外(常量中的各种符合引用)的信息进行匹配性校验,这一转化动作发生在连接的第三个阶段——解析阶段,如确认方法体内的字段描述符和简单名称是否存在
3.3、准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段(如上代码示例),这些变量所使用的内存将在方法中进行分配。
注意: 这里内存分配的仅仅是类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。而初试值赋值一般为零值。
public static int value = 123
如上: 变量value在准备阶段的初试值为0,而不是123。因为这个时候还没有执行Java方法,而把value赋值为123的putstatic指令是程序在编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。
final static double pi = 3.1415926
当然,pi一般通过Math.PI来获取。这里由于是final static修饰,其在编译期便已分配了
Java基本数据类型的零值 :
3.4、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic Reference):符号引用以一组符号来描述引用的目标,符号可以是任何形式地字面量,只要使用时能无歧义地定位到目标即可。
直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
解析动作主要针对这7类符号进行解析:
类或接口的解析
字段解析
类方法解析
接口方法解析
方法类型
方法句柄
调用点限定符
3.5、初始化
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过制定的主观计划(static修饰的变量值、那些构造器方法输出)去初始化类变量和其他资源,或者从另一个角度说:初始化阶段是执行()方法的过程。
4、类加载器
虚拟机设计团队将“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放在Java虚拟机外部来实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
java自带的三种类加载器分别是:bootstrap启动类加载器、扩展类加载器和应用加载器也叫系统加载器。动类加载器加载java home中lib目录下的类,扩展加载器负责加载ext目录下的类,应用加载器加载classpath指定目录下的类。
比如Tomcat在Java虚拟机的基础上实现了类加载器功能:如下图所示
其中 WebApp类加载器通常会存在多个实例,每个Web应用程序对应一个WebApp类加载器,每一个Jsp文件对应一个Jsp加载器。
java的类加载使用双亲委派模式,即一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。
这种双亲委派模式的好处:一是避免类的重复加载,另外也避免了java的核心API被篡改。
备注:参照周志明的《深入理解Java虚拟机》
Socket 编程
套接字使用TCP提供了两台计算机之间的通信机制。 客户端程序创建一个套接字,并尝试连接服务器的套接字。
当连接建立时,服务器会创建一个 Socket 对象。客户端和服务器现在可以通过对 Socket 对象的写入和读取来进行通信。
java.net.Socket 类代表一个套接字,并且 java.net.ServerSocket 类为服务器程序提供了一种来监听客户端,并与他们建立连接的机制。
以下步骤在两台计算机之间使用套接字建立TCP连接时会出现:
服务器实例化一个 ServerSocket 对象,表示通过服务器上的端口通信。
服务器调用 ServerSocket 类的 accept() 方法,该方法将一直等待,直到客户端连接到服务器上给定的端口。
服务器正在等待时,一个客户端实例化一个 Socket 对象,指定服务器名称和端口号来请求连接。
Socket 类的构造函数试图将客户端连接到指定的服务器和端口号。如果通信被建立,则在客户端创建一个 Socket 对象能够与服务器进行通信。
在服务器端,accept() 方法返回服务器上一个新的 socket 引用,该 socket 连接到客户端的 socket。
连接建立后,通过使用 I/O 流在进行通信,每一个socket都有一个输出流和一个输入流,客户端的输出流连接到服务器端的输入流,而客户端的输入流连接到服务器端的输出流。
TCP 是一个双向的通信协议,因此数据可以通过两个数据流在同一时间发送.以下是一些类提供的一套完整的有用的方法来实现 socket。