【java学习】JVM学习

1,JVM

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

2,JDK(Java Development Kit)

1)概念:

JDK是用于支持Java程序开放的最小环境。

2)组成:

Java程序设计语言、Java虚拟机、Java API类库等三部分组成。

3)包含组件:

①Javac.exe

是收录于JDK中的Java语言编译器。可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于JVM的字节码。

②java

运行工具,运行.class的字节码

③jar

打包工具,将相关的类文件打包成一个文件

④javadoc

文档生成器,从源码注释中提取文档,注释需匹配规范

⑤jdb debugger

调试工具

⑥jps

显示当前java程序运行的进程状态

⑦javah

从Java类生成C头文件和C源文件。这些文件提供了连接胶合,使Java和C代码可进行交互。

4)Java类库(Java API)

Java官方为开发者提高的很多功能强大的类,分别放在各个包中。
Java类库具有跨平台的特点,保证了软件的可移植性。

①java.*开头:

java的核心包。

a>java.lang

java编程的基础类。如:Object,Math,String,StringBuffer,System,Thread等。

b>java.util

包含集合框架、遗留的集合类、事件模型、日期和时间实施、国际化和各自实用工具类(字符串标记生成器、随机数生成器)。

c>java.io

通过文件系统、数据流和序列化提供系统的输入与输出。

d>java.net

实现网络应用与开发的类。

e>java.sql

使用Java语言访问并处理存储再数据源(通常是一个关系型数据库)中的数据API

f>java.awt

GUI设计与开发的类:创建界面和绘制图形图像的所有类。

g>java.swing

GUI设计与开发的类:是一组轻量级的组件。

h>java.text

与自然语言无关的方式来处理文本、日期、数字和消息的类和接口。

②javax.*开头:

扩展包。x是extension的意思。是对java.*的优化和扩展。

③org.*开头:

是各个机构或组织发布的包。这些组织有影响力,且代码质量高。

④com.*开头:

由盈利性公司发布,有版权问题。
注意:为防止命名重复,惯例:以自己的域名倒写形式作为开头来为自己开发的包命名,如百度:com.baidu.*开头。

5)安装配置

jdk安装需要两次:第一次是JDK,第二次是jre,分别放入不同的文件夹。

环境配置:
【系统与安全】,【系统】,【高级系统设置】,【系统属性-高级】,【环境变量】

①JAVA_HOME

值为jdk安装目录
JAVA_HOME

②PATH

变量值为
%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin

③CLASSPATH

变量值为
.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;

检测安装是否成功:
【WIN+R】,【cmd】,输入【java -version】,【javac】【javah】,【回车】,若出现很多英文,代表配置

问题:【java -version】有结果,【javac】【javah】不可以。
解决:在用户变量和系统变量都配置PATH(注意区分大小写)。

3,JRE(Java Runtime Environment)

1)概念:

JRE是支持Java程序运行的标准环境。

2)组成:

把Java API类库中的Java SE API子集和Java虚拟机这两部分统称为JRE。

4,Java内存

1)内存区域与内存溢出异常

Java虚拟机(Virtual Machine)所管理的内存包括的运行时数据区域(如下图):
JVM内存

i)程序计数器(Program Counter Register)

程序计数器是一个比较小的内存区域,是线程隔离的。

a>功能:

指示当前线程所执行的字节码的行号。

b>JVM的多线程的实现:

线程轮流切换并分配处理器执行时间。在任何一个确定的时刻,一个CPU(内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,故这类内存区域为“线程私有”内存。
如果线程正在执行一个:
①Java方法,PCR记录的是正在执行的VM字节码指令地址。
②Native方法,PCR值为Undefined。

c>特点:

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

ii)虚拟机栈(VM Stack)

线程私有,生命周期与线程相同。
Java的内存并非简单的区分为Heap和Stack,实际远比这复杂。Stack指的是VM Stack,或者说是虚拟机中局部变量表部分。

a>功能:存放方法运行时所需的数据,成为栈帧

VM Stack描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,对应一个Stack Frame在VM Stack中入栈出栈的过程。
局部变量表所需的内存空间在编译期间完成分配,进入方法时,方法所需在Stack Frame中分配的局部变量的空间是确定的,且在运行期间大小不变。

b>异常情况:

①StackOverflowError异常:线程请求的栈深度大于VM所允许的深度。
②OutOfMemoryError异常:JVM Stack动态扩展时,无法申请到足够的内存。

iii)本地方法栈(Native Method Stack)

功能:为VM使用Native方法服务。
其他与VM Stack一致。

iv)方法区(Method Area)

与Java Heap一样,是各个线程共享的内存区域。是线程隔离的。

a>功能:

存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

b>运行常量池(Runtime Constant Pool):

是方法区的一部分。
Class文件:包含类的版本、字段、方法、接口等描述信息,还包括常量池(Constant Pool Table)信息,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法的运行时常量池中存放。

c>OutOfMemoryError异常:

常量池无法再申请到内存时抛出。

v)堆(Heap,GC堆)

所有线程共享,存储已被VM加载的类信息、常量、静态变量,即时编译器编译后的代码等数据。这个区域的内存回收目标主要是针对常量池的对象的回收和堆类型的卸载。

a>功能:

存放对象实例、数组。

b>特点:

由于JIT编译器的发展与逃逸分析技术的成熟,栈上分配、标量替换优化技术的发生,对象的分配也不一定就在堆上。
Java堆是GC管理的主要区域,故很多时候被称为“GC堆”。
Java堆可以处于物理上不连续的内存空间中。
Java Heap是JVM所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在VM启动时创建。

c>分类

JVM堆内存分为2块:Permanent Space 和 Heap Space。

Permanent 即 持久代(Permanent Generation),主要存放的是Java类定义信息,与垃圾收集器要收集的Java对象关系不大。
Heap = { Old + NEW = {Eden, from, to} },Old 即 年老代(Old Generation),New 即 年轻代(Young Generation)。年老代和年轻代的划分对垃圾收集影响比较大。

在年轻代中经历了N次(可配置)垃圾回收后仍然存活的对象,就会被复制到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象
年老代溢出原因:循环上万次的字符串处理、创建上千万个对象、在一段代码内申请上百M甚至上G的内存。
持久代溢出原因:动态加载了大量Java类而导致溢出。

2)内存管理

i>内存分配

为变量分配内存空间。

a)类变量和实例变量

①类变量

当类创建好后,类变量也随之创建好,无论创建多少个实例来引用访问类变量,底层都是对本类的引用。

②实例变量

为java对象所有,每次创建Java对象都会为它分配内存,并初始化。

b)基本类型与引用类型

①基本类型

在栈中分配内存,值保存于栈中。

②引用类型

在栈中保存变量指针,指向堆中的对象。

ii>内存回收

【java学习】垃圾回收机制(GC)

5,类文件(.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,多个,从左到右排列在接口索引集合中。

6,类的加载机制

1)概念

①类加载机制

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

②类加载器

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

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

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

类加载器可以分为三类:
启动类加载器(Bootstrap ClassLoader):负责加载\lib目录下或者被-Xbootclasspath参数所指定的路径的,并且是被虚拟机所识别的库到内存中。
扩展类加载器(Extension ClassLoader):负责加载\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修饰的类的名字与文件的名字相同,其他类的名字可以根据需求随意起名字。

7,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可提高系统整体的流畅性。

8,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 编码格式。

猜你喜欢

转载自blog.csdn.net/sunshinetan/article/details/54406347