【java学习】类 JVM底层机制

1,JVM

①JVM可以用软件/硬件实现。
②字节码是虚拟机的机器码。
③JVM将代码程序与各操作系统和硬件分开,JVM的存在使java可以跨平台。

2,类文件(.class,字节码文件)

1)文件内容

class文件是以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在class文件之中,中间没有添加任何分隔符(以保证整个Class文件中存储的内容全部是程序运行的必要数据,没有空隙)。
当遇到需要占用8位字节以上空间的数据时,则会按照高位在前的方式分割成若干上8位字节进行存储。

①大小端(Endian)

表示数据在存储器重的存放顺序。
i>大端模式
指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;
ii>小端模式
指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。
iii>应用场景
1、不同端模式的处理器进行数据传递时必须要考虑端模式的不同
2、在网络上传输数据时,由于数据传输的两端对应不同的硬件平台,采用的存储字节顺序可能不一致。所以在TCP/IP协议规定了在网络上必须采用网络字节顺序,也就是大端模式。对于char型数据只占一个字节,无所谓大端和小端。而对于非char类型数据,必须在数据发送到网络上之前将其转换成大端模式。接收网络数据时按符合接受主机的环境接收。

②java大小端

ava由于虚拟机的关系,屏蔽了大小端问题,需要知道的话可用 ByteOrder.nativeOrder() 查询。
存储字节次序默认为大端模式。可设置字节存储次序为小端模式。

2)文件结构

①(1-4)魔数

每个Class文件的头4个字节称为魔数(Magic Number),用来确定是Class文件。
用于身份识别,识别文件类型,因为扩展名是可以随意改动的。 入gif或者jpeg等文件头中存有魔数。Class文件魔数以0xCAFEBABE开头。

②(5-8)版本号

紧接着魔数之后的4个字节为:Class文件的版本号。
(5-6)字节为次版本号(Minor Version),
(7-8)为主版本号(Major Version)。

③(9…)常量池入口

紧接着版本号之后为常量池入口。常量池可以理解为Class文件种的资源仓库。
u2:常量池容量计数值(constant_pool_count),从1开始。(因为常量池中数量不固定)。0表示某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。

常量池两大类常量:
字面量(Literal):文本字符串、final常量值等
符号引用(Symbolic References):
包括三类:类和即可的全限定名,字段的名称和描述符,方法的名称和描述符。(javac编译时,JVM加载Class文件时进行动态连接时调用。)

④u2 访问标志

紧接着的2个字节代表访问标志。用于识别一些类或者接口层次的访问信息。
包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类,是否声明为final;是否是注解、枚举;这个类是否由用户代码产生等。

⑤(…)+ 类索引(u2) + 父类索引(u2) + 接口索引集合(一组u2类型的数据集合)

除了Object外,所有java类都有弗雷,其父类索引都不为0。
接口索引:implements,多个,从左到右排列在接口索引集合中。

3,类的加载机制

1)概念

①类加载机制

类的加载就是VM通过一个类的全限定名来获取描述此类的二进制字节流,对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是VM的类加载机制。

②类加载器

完成这个加载动作的就是类加载器。

类和类加载器息息相关,判定两个类是否相等,只有在这两个类被同一个类加载器加载的情况下才有意义,否则即便是两个类来自同一个Class文件,被不同类加载器加载,它们也是不相等的。

注:这里的相等性包含Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果以及Instance关键字对对象所属关系的判定结果等。

类加载器可以分为三类:
启动类加载器(Bootstrap ClassLoader):负责加载<JAVA_HOME>\lib目录下或者被-Xbootclasspath参数所指定的路径的,并且是被虚拟机所识别的库到内存中。
扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录下或者被java.ext.dirs系统变量所指定的路径的所有类库到内存中。
应用类加载器(Application ClassLoader):负责加载用户类路径上的指定类库,如果应用程序中没有实现自己的类加载器,一般就是这个类加载器去加载应用程序中的类库。
这么多类加载器,那么当类在加载的时候会使用哪个加载器呢??

这个时候就要提到类加载器的双亲委派模型,流程图如下所示:
这里写图片描述
双亲委派模型的整个工作流程非常的简单,如下所示:

如果一个类加载器收到了加载类的请求,它不会自己立即去加载类,它会先去请求父类加载器,每个层次的类加载器都是如此。层层传递,直到传递到最高层的类加载器,只有当 父类加载器反馈自己无法加载这个类,才会有当前子类加载器去加载该类。

关于双亲委派机制,在ClassLoader源码里也可以看出,如下所示:

public abstract class ClassLoader {
    
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            //首先,检查该类是否已经被加载
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //先调用父类加载器去加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    //如果父类加载器没有加载到该类,则自己去执行加载
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }
}

为什么要这么做呢??

这是为了要让越基础的类由越高层的类加载器加载,例如Object类,无论哪个类加载器去尝试加载这个类,最终都会传递给最高层的类加载器去加载,前面我们也说过,类的相等性是由 类与其类加载器共同判定的,这样Object类无论在何种类加载器环境下都是同一个类。

相反如果没有双亲委派模型,那么每个类加载器都会去加载Object,那么系统中就会出现多个不同的Object类了,如此一来系统的最基础的行为也就无法保证了。

③动态加载

java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。这样的策略增加了类加载时开销,但提高了java的灵活性。
java动态扩展的语言特性:依赖运行期动态加载和动态连接这个特点实现的。

2)类的生命周期

生命周期

VM将描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被VM直接使用的Java类型。
类型的加载、连接、初始化过程都是在程序运行期间完成的,这样做虽然在加载种增加了性能开销,但是提高了Java应用程序的灵活性(可以动态扩展的语言特性)。
类加载过程:

①加载(Loading)

i>通过一个类的全限定名来获取定义此类的二进制字节流。
二进制字节流不仅可以从Class文件中获取,还可以通过其它方式:
ZIP包获取:成为JAR、EAR、WAR格式的基础
网络获取:Applet
运行时计算生成:如动态代理技术中,java.lang.reflect.Proxy中,用ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
其它文件生成:JSP文件生成对应的Class类。
数据库读取:如某些中间件服务器(SAP Netweaver)可以选择把程序安装到数据库完成程序代码在集群间的分发。
ii>将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
iii>在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

②验证(Verification)

确保Class文件的字节流中包含的信息符合JVM要求并确保安全。
i>文件格式验证:针对文件结构验证;
ii>元数据验证:对字节码信息进行语义分析,保证其描述的信息符合Java语言规范要求。
iii>字节码验证:对类的方法体校验分析,确保其逻辑不会危害JVM。
如:在操作栈放置int数据,使用时按long加载进入本地变量表。把父类对象赋值给子类。
iv>符号引用验证
在解析阶段发生,是对类自身的信息进行匹配性校验,以而保证解析正常执行。

③准备(Preparation)

为类变量分配内存并设置类变量初始值(一般是数据类型的零值,初始化阶段才完成真正的赋值)的阶段,这些变量所使用的内存都将在方法区中进行分配。(近包括类中static修饰的变量,不包括实例变量;实例变量将回在对象实例化时随着对象一起分配在Java堆中)

④解析(Resolution)

解析和初始化顺序前后不定,某些情况下会支持java语言的运行时绑定(动态绑定或晚期绑定)。
解析是JVM将常量池内的符号引用替换为直接引用的过程。

⑤初始化(Initialization)

JVM规定5种情况下必须对类进行初始化:
i>遇到new、getstatic、putstatic或invokestatic这4条字节码指令时。常见场景为:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法时。
ii>使用java.lang.reflect包的方法对类进行反射调用时
iii>初始化一个类时,发现父类还未初始化,需先触发其父类的初始化。
iv>JVM启动时,用户需要指定一个要执行的主类(包含main()),JVM先初始化这个类。
v>使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有初始化,则需初始化。

⑥使用(Using)

⑦卸载(Unloading)

3)对象的创建

①VM遇到new指令
如:Person person = new Person()
②检查指令的参数是否能在常量池中定位到一个类的符号引用
如:Person.class
③检查这个符号引用代表的类是否被加载、解析和初始化过(如果没有,先执行相应的类加载过程)
④VM为新对象分配内存
所需内存大小在加载完成后便可确定。在堆内存里开辟内存空间,并分配内存地址。
⑤init方法
在堆内存里建立对象的属性,并进行默认的初始化。
对属性进行显示初始化。
对对象进行构造代码块初始化。
调用对象的构造函数进行初始化。
⑥将对象的地址赋值给person变量。

4)java对象的生命周期

①加载

将类的信息加载到JVM的方法区,然后在堆区中实例化一个java.lang.Class对象,作为方法去中这个类的信息入口。

②连接

验证:验证类是否合法。
准备:为静态变量分配内存并设置JVM默认值,非静态变量不会分配内存。
解析:将常量池里的符号引用转换为直接引用。

③初始化

初始化类的静态赋值语句和静态代码块,主动引用会被触发类的初始化,被动引用不会触发类的初始化。

⑤使用

执行类的初始化,主动引用会被触发类的初始化,被动引用不会触发类的初始化。

⑥卸载

卸载过程就是清楚堆里类的信息,以下情况会被卸载:
i>类的所有实例都已经被回收;
ii>类的ClassLoader被回收;
ii>类的CLass对象没有被任何地方引用,无法在任何地方通过 反射访问该类。

5)内存溢出异常

Heap中的对象数量到达最大堆的容量限制后抛出。

6)编译

①反例

以下代码保存到B.java文件中,是合法的,但是无法运行。

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

原因:
运行时,先编译B.java文件,通过。在B.class文件中找java的入口方法main,找不到。因为通过javac B.java命令编译后只会产生一个A.class文件(编译时,产生的.class文件名与类名相同)。

public static void main(String[] args){}方法是java程序的入口方法,其他main方法不是,并且这个入口方法必须被定义在类名与文件名相同的public类中。

一个文件内部可以有多个类的存在,但只有被public修饰的类的名字与文件的名字相同,其他类的名字可以根据需求随意起名字。

4,Dalvik Virtual Machine(DVM)

1)定义:

Android中的所有Java程序都是运行在Dalvik VM上的。Android上的每个程序都有自己的线程,DVM只执行.dex的Dalvik executable 文件。每个Android应用在底层都对应有一个独立的DVM实例并在其解释下执行。
是android4.0以下操作系统的主要的组成部分,Android Runtime中的元件包含:核心函数库(Core Libraries)、DVM。android4.4时被ART取代。

2)JVM与DVM区别:

①Java VM是以基于栈的虚拟机(Stack-based),而Dalvik是基于寄存器的虚拟机(Register-based)。显然,后者最大的好处在于可以根据硬件实现更大的优化,缩短编译时间,这更适合移动设备的特点。
②运行环境——Dalvik经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个 Dalvik应用作为一个独立的Linux进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。

3)DVM与ART区别:

①DVVM是执行的时候编译+运行,安装快,开启应用慢,应用占用空间小。ART是安装的时候就编译好了,执行时直接运行,所以安装慢、开启应用快,占用空间大。
②ART(Ahead-Of-Time compiler):相比 ios,android卡的主要原因就是系统和应用层之间还有一层虚拟机,加入ART可提高系统整体的流畅性。

5,java编码方式

1)概念

计算机存储信息的最小单元是一个字节即8bit,所以能表示的范围是0~255,这个范围无法保存所有的字符,所以需要一个新的数据结构char来表示这些字符,从char到byte需要编码。

2)常见编码方式

①ASCII

总共有 128 个,用一个字节的低 7 位表示,031 是控制字符如换行回车删除等;32126 是打印字符,可以通过键盘输入并且能够显示出来。

②GBK

码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。

UTF-16

UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用两个字节来表示 Unicode 转化格式,这个是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是 16 个 bit,所以叫 UTF-16。UTF-16 表示字符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作,这也是 Java 以 UTF-16 作为内存的字符存储格式的一个很重要的原因。

UTF-8

统一采用两个字节表示一个字符,虽然在表示上非常简单方便,但是也有其缺点,有很大一部分字符用一个字节就可以表示的现在要两个字节表示,存储空间放大了一倍,在现在的网络带宽还非常有限的今天,这样会增大网络传输的流量,而且也没必要。而 UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度。不同类型的字符可以是由 1~6 个字节组成。

3)场景

Java中需要编码的地方一般都在字符到字节的转换上,这个一般包括磁盘IO和网络IO。
Reader 类是 Java 的 I/O 中读字符的父类,而 InputStream 类是读字节的父类,InputStreamReader 类就是关联字节到字符的桥梁,它负责在 I/O 过程中处理读取字节到字符的转换,而具体字节到字符的解码实现它由 StreamDecoder 去实现,在 StreamDecoder 解码过程中必须由用户指定 Charset 编码格式。

6,java运行时识别对象和类的信息

1)RTTI(Run-Time Type Identification,运行时类型识别)

①概念

RTTI能在运行时就能够自动识别每个编译时已知的类型。

很多时候需要进行向上转型,比如Base类派生出Derived类,但是现有的方法只需要将Base对象作为参数,实际传入的则是其派生类的引用。那么RTTI就在此时起到了作用,比如通过RTTI能识别出Derive类是Base的派生类,这样就能够向上转型为Derived。类似的,在用接口作为参数时,向上转型更为常用,RTTI此时能够判断是否可以进行向上转型。

而这些类型信息是通过Class对象(java.lang.Class)的特殊对象完成的,它包含跟类相关的信息。每当编写并编译一个类时就会产生一个.class文件,保存着Class对象,运行这个程序的Java虚拟机(JVM)将使用被称为类加载器(Class Loader)的子系统。而类加载器并非在程序运行之前就加载所有的Class对象,如果尚未加载,默认的类加载器就会根据类名查找.class文件(例如,某个附加类加载器可能会在数据库中查找字节码),在这个类的字节码被加载时接受验证,以确保没有被破坏并且不包含不良Java代码。这也是Java中的类型安全机制之一。一旦某个类的Class对象被载入内存,就可以创建该类的所有对象。

②已知的RTTI形式包括

1、传统的类型转换,由RTTI保证类型转换的正确性,如果执行一个错误的类型转换,就会抛出ClassCastException异常;
2、代表对象的类型的Class对象,通过查询Class对象(即调用Class类的方法)可以获取运行时所需的信息。

2)反射(分析类能力的程序)

①概念

指java程序在运行时可以访问、检测和修改其自身的状态或行为的一种能力。通过反射机制,可以在运行时加载一个只知道名称的类,获得它的完整构造、生成它的实例、对它的属性赋值、调用它的方法。

②实现

获取类

通过已知名称的类的getClass()方法获取Class类对象。

在程序运行期间,java运行时系统始终为所有的对象维护一个被称为运行时的类型标识,这个信息跟踪着每个对象所属的类。JVM利用运行时类型信息选择相应的方法执行。可以通过专门的java类访问这些信息,保存这些信息的类被称为Class,通过Object类中的getClass()方法可以获取一个Class类型的实例。

//第一种方式:  
//注意:参数必须是类名或接口名,否则会抛出异常:checked exception
Class c1 = Class.forName("Employee");  
//第二种方式:  
//java中每个类型都有class 属性.  
Class c2 = Employee.class;  
   
//第三种方式:  
//java语言中任何一个java对象都有getClass 方法  
Employeee = new Employee();  
Class c3 = e.getClass(); //c3是运行时类 (e的运行时类是Employee) 

应用:快速启动程序。
main方法加载时,会加载所有需要的类,比较耗时。为了快速启动,可以先显示一个启动画面,然后通过forName手工地加载其他类。

比较:==

if(e.getClass == Employee.class)
创建对象

获取类以后我们来创建它的对象,利用newInstance。调用默认构造器(无参构造器)初始化创建对象。如果没有默认构造器则抛出异常。

//创建此Class c 对象所表示的类的一个新实例  
Object o = c.newInstance(); //调用了Employee的无参数构造方法. 

更加灵活的反射机制:

// java.lang.reflect.Constructor类里也有一个newInstance方法可以创建对象。
Constructor<Employee> constructor = Employee.class.getConstructor();//需要做一个NoSuchMethodException异常处理
Employee emp3 = constructor.newInstance();

Java中有5种创建对象的方式,下面给出它们的例子还有它们的字节码:

方式构造函数详情使用
使用new关键字调用无参构造函数
使用Class类的newInstance方法调用任意的构造函数(无参的和带参数的)
使用Constructor类的newInstance方法调用无参、有参、私有的构造函数
使用clone方法不调用构造函数无论何时我们调用一个对象的clone方法,jvm就会创建一个新的对象,将前面对象的内容全部拷贝进去实现Cloneable接口并实现其定义的clone方法
使用反序列化不有调用构造函数当我们序列化和反序列化一个对象,jvm会给我们创建一个单独的对象为了反序列化一个对象,我们需要让我们的类实现Serializable接口
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));
Employee emp5 = (Employee) in.readObject();
获取属性

java.lang.reflect包中有三个类:Field(域)、Method(方法)、Constructor(构造器)。
getField、getMethods和getConstructors方法返回类的public域、方法和构造器数组。
getDeclareFields、getDeclareMethods和getDeclaredConstructors方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和protected成员,不包括超类成员。

属性分为所有的属性和指定的属性:

/**的所有属性*/
//获取整个类 
            Class c = Class.forName("java.lang.Integer");  
              //获取所有的属性?  
            Field[] fs = c.getDeclaredFields();  
       
                   //定义可变长的字符串,用来存储属性  
            StringBuffer sb = new StringBuffer();  
            //通过追加的方法,将每个属性拼接到此字符串中  
            //最外边的public定义  
            sb.append(Modifier.toString(c.getModifiers()) + " class " + c.getSimpleName() +"{\n");  
            //里边的每一个属性  
            for(Field field:fs){  
                sb.append("\t");//空格  
                sb.append(Modifier.toString(field.getModifiers())+" ");//获得属性的修饰符,例如public,static等等  
                sb.append(field.getType().getSimpleName() + " ");//属性的类型的名字  
                sb.append(field.getName()+";\n");//属性的名字+回车  
            }  
      
            sb.append("}");  
      
            System.out.println(sb);  
/**获取特定的属性*/
//获取类  
    Class c = Class.forName("User");  
    //获取id属性  
    Field idF = c.getDeclaredField("id");  
    //实例化这个类赋给o  
    Object o = c.newInstance();  
    //打破封装  
    idF.setAccessible(true); //使用反射机制可以打破封装性,导致了java对象的属性不安全。  
    //给o对象的id属性赋值"110"  
    idF.set(o, "110"); //set  
    //get  
    System.out.println(idF.get(o)); 

③方法

getDeclaredMethods() //获取所有的方法
getReturnType()  //获得方法的放回类型
getParameterTypes() //获得方法的传入参数类型
getDeclaredMethod("方法名",参数类型.class,……)  //获得特定的方法
 
getDeclaredConstructors()  //获取所有的构造方法
getDeclaredConstructor(参数类型.class,……)  //获取特定的构造方法
 
getSuperclass()  //获取某类的父类
getInterfaces()  //获取某类实现的接口

④优缺点

使代码更加灵活,
但运用它会使软件的性能降低,复杂度增加,增加安全隐患(只有运行时才会发现错误)。

⑤功能

主要使用者是工具构造者,而不是应用程序员。
i>在运行中分析类的能力;
ii>在运行中查看对象;
例如,编写一个toString方法供所有类使用。
iii>实现通用的数组操作代码;
iv>利用Method对象,这个对象很像C++中的函数指针

⑥应用

控制反转(IoC)与依赖注入(DI)

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值