JAVA篇:java和JVM
java简介
1、java的三个体系
java是面向对象程序语言java和java平台的总称,java有三个体系:
JAVASE (java2 Platform Standard Editor) java平台标准版
JAVAEE(java2 Platform Enterprise Editor) java平台企业版
JAVAME(java2 Platform Micro Editor) java平台标准版
JavaSE是JavaEE和JavaME的基础。
JavaEE是在JavaSE的基础上构建,用以开发B/S加工软件,即开发企业级应用。JavaEE在JAVASE的基础上扩展,添加了许多便捷的框架,譬如我们常用的Spring,Struts,Hibernate。JavaSE也可以说既是框架也是规范,它包含了许多我们开发时用到的组件如Servlet,EJB,JSP,JSTL,同时JAVAEE提供了许多规范的接口却并不实现,使得即使不同厂商实现细节不同,展现给外部使用的却是统一规范的接口。
JavaME则是一套专门为嵌入式设备设计的api接口规范,专门用于开发移动设备软件和嵌入式设备软件。
注:之前有接触过android studio,关于android 软件的开发和JavaME,我看到的说法是两者唯一的关系就是都是java语言实现的。android 软件不能运行在javaME的环境中,javaME软件也不能运行在android环境中。
2、java的主要特性
java语言是简单的,因为它的语法与C和C++相似,但是又抛弃了一些C++中很少使用的、复杂的特性,比如操作符重载,多继承,自动的强制类型转换,并且舍弃了指针的使用,提供了自动分配和回收内存的垃圾处理器。
java语音是面向对象的,java语音提供了类、接口和继承等面向对象的特性,不过java的类不支持多继承,接口支持多继承,java提供了类于接口之间的实现机制,并且全面支持动态绑定。
Java语言是分布式的,它支持internet应用开发,它的网络应用编程接口(java net)提供了支撑网络应用开发的类库,譬如URL, URLconnection, Socket, ServerSocket等,其中java的RMI(远程方法激活)机制也是开发分布式应用的重要手段。
Java语言式健壮的,java的强类型机制、异常处理、垃圾的回收器是java程序健壮性的重要保证,java丢弃指针机制是明智的,java的安全检查机制使得java更具健壮性。
java语言是安全的,它提供了一个安全机制以防止来自网络的恶意代码的攻击。java对通过网络下载的类具有一个安全防范机制(类ClassLoader),如分配不同的名字空间防止替代本地同名类、字节代码检查,并提供安全管理机制(类SecurityManager)让java应用设置安全卫兵。
Java语言是体系结构中立的,java程序(.java)在java平台中被编译成体系结构中立的字节码格式(.class),然后可以在实现了java平台的任何系统中运行,这种途径适合于异构的网络环境和软件的分发。
java语言是可移植的,这种可移植性来源于体系结构中立性。
Java语言是解释型的,java程序在java平台上被编译成可移植的字节码格式,在运行时,java平台的java解释器对这些字节码进行解释执行,执行过程中需要的类在联结过程中被载入到运行环境中。
java语言是高性能的,相比于解释型的高级脚本语言,java是高性能的,随着JIT(Just-In-Time)编译技术的发展,java的运行速度越来越接近C++。
Java语言是多线程的,java支持多线程同时执行,并提供了多线程的同步机制。线程是java语言一种特殊对象,它必须由Thread类或者其子(孙l)类来创建。
java语言是动态的,java语言的设计目的之一就是适应于动态变化的环境,java程序需要的类能够动态地被载入到运行环境,也可以通过网络环境载入所需要的类,这有利于软件的升级。另外,java中的类有一个运行时刻的表示,能进行运行时刻的类型检查。
其实java的这几个主要特性的描述已经包含了java相关的许多基础的需要了解的知识点了。
3、JDK与JRE的关系:
JDK:支持Java程序开发的最小环境。JDK = Java程序设计语言+Java虚拟机+Java API类库。
JRE:支持Java程序运行的标准环境。JRE=Java虚拟机 +Java API类库中的Java SE API子集 。
JVM
关于JVM到底要从什么地方开始写起来,我一度十分犹豫,或者说我到底想要从这次的知识整理中构建起怎么样的概念呢?
java技术的核心就是java虚拟机(JVM,Java Virtual Machine),所有的java程序都运行在JVM内部。然而JVM是跨语言的平台,它是语言无关的,它并不与java语言绑定,任何编程语言的编译结果满足并包含JVM的内部指令、符号表以及其他的辅助信息,它就是一个有效的字节码文件,能够被虚拟机识别并装载运行。java虚拟机在操作系统之上,并不与硬件直接交互。
JVM的整体体系包含JVM内存区域,JVM内存溢出,类加载,垃圾回收,性能优化。
1 JVM内存区域和JVM内存溢出
如图所示,JVM运行时的内存区域包含程序计数器,本地方法栈,虚拟机栈,方法区,堆,其中程序计数器,本地方法栈,虚拟机栈是线程隔离的,方法区和堆是线程共享的。
1.1 程序计数器(Program Counter Register)
内存空间小,线程私有。是当前线程执行字节码的行号指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器实现。若线程正在执行一个java方法,这个计数器记录的则是正在执行的虚拟机字节码指令的地址,若线程正在执行一个native方法,那么计数器的值则为Undefined。程序计数器是唯一在java虚拟机规范中没有规定OutOfMemoryError情况的区域。
1.2 虚拟机栈(VM Stack)
线程私有,生命周期和线程一致,描述的是java方法执行时的内存模型:每个方法执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表,操作数栈,动态链接和方法出口等信息,每一个方法的调用到结束都对应着一个栈帧从虚拟机栈入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本类型(boolean,byte,short,char,int,float,long,double),对象引用(reference类型)和returnAddress类型
StackOverFlowError:线程请求栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而拓展时无法申请到足够的内存。
1.3 本地方法栈(Native Method Stack)
它与虚拟机栈的区别是,本地方法栈是为native方法服务的,虚拟机栈式为java方法服务的。
1.4 堆(Heap)
在大部分应用中,这是java虚拟器所管理的内存中最大的一块,线程共享,主要用来存储数组和对象的实例。在堆的内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),这些区域可以位于物理上不连续的区域,但要求逻辑上连续。
OutOfMemoryError:如果堆没有内存完成实例分配,且堆无法再拓展时抛出该异常。
1.5 方法区(Method Area)
属于共享内存区域,存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
运行时常量池:用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 Intern())都可将常量放入,内存有限,无法申请时抛出OutOfMemoryError。
1.6 直接内存
非虚拟机运行时的部分数据区域
2 类加载
虚拟机类加载过程是指虚拟机把描述类的数据从.class文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的java类型。在java中,类的加载、连接和初始化过程都是在程序运行期间完成的。
2.1 类加载时期机
在类的生命周期中,类的加载、验证、准备、初始化和卸载这几个阶段是固定的,解析阶段可以在初始化之后再开始(运行时绑定、动态绑定或晚期绑定)。
那么什么情况会触发类的初始化(类加载)呢?
a) 在遇到new,getstatic,putstatic和invokestatic这4条字节码时类没有初始化则触发初始化。比如说,当我们使用new创建某个类的实例,调用某个类的静态变量(在编译期已经放入了运行时常量池的除外)和静态方法时需要先把这个类初始化。
b)使用 java.lang.reflect 包的方法对类进行反射调用的时候。
c)调用子类的初始化时,要求父类也已经初始化
d)虚拟机启动时会先初始化用户指定的主类
e)当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。
类初始化的特殊情况:
a) 当子类继承父类,而静态变量定义在父类中,通过子类名调用该静态变量,触发的是定义了该静态变量的父类。
b)若类的静态变量被final修饰是一个常量,调用时不会触发类的初始化
c)当创建某个类的数组,先创建的是数组类,并不会触发该类的初始化
2.2 类加载过程
2.2.1 加载
a)找到需要加载的类:通过类的全限定名来获取定义此类的二进制流(zip包,网络,运算生成,jsp生成,数据库读取)。
b)将类放入方法区:将二进制流所代表的静态存储结构转化为方法区运行时的数据结构
c)创建访问该类的入口:在内存中生成一个代表这个类的java.lang.Class对象,作为这个类各种数据结构的访问入口。
数组类的特殊性:数组类不通过类加载器创建,而是由虚拟机直接创建,但是数组类的元素类型最终是由类加载器加载的。
a)如果数组类的元素是引用类型,则由递归采用类加载加载
b)若数组类的元素是值类型,则虚拟机会将数组标记为引导类加载器关联
c)数组类的可见性与其元素可见性一致,若其元素是值类型,则数组类可见性默认是public
加载阶段和连接阶段是交叉进行的,只保证开始时间的前后。
2.2.2 验证
验证是连接的第一步,主要是确认字节码中包含的信息符合当前虚拟机的要求,主要包括文件格式验证,元数据验证,字节码验证,符号引用验证。
文件格式验证:文件格式验证直接验证字节流,只有通过了文件格式验证的字节流才会存储进方法区,后续三个验证都是针对方法区存储结构,不再直接操作字节流。
元数据验证:进行的是语义校验,保证不存在不符合java语言规范的元数据信息。譬如说这个类是否有父类,继承父类时是否有不符合规范的情况(继承final类、非抽象类未实现全部方法等等)
字节码验证:这是整个校验过程中最复杂的部分,主要目的时通过数据流和控制流的分析,确定程序语义是安全的,符合逻辑的。这个阶段通过对方法体的校验分析,保证类的方法在运行时不会做出危害虚拟机安全的事件。
符号引用验证:符号引用验证发生在将符号引用转变成直接引用的时候,即在连接的第三阶段--解析的时候发生。主要是确保对类自身以外的信息进行匹配性校验。譬如说引用的类是否能通过全限定名找到,是否能在指定类里面找到指定的变量和方法,以及这些方法是否可访问。如果这个阶段发生错误,会抛出java.lang.IncompatibleClass.changeError异常的子类,如java.lang.illegalAccessError,java.lang.NoSuchFiledError,java.lang.NoSuchMethodError等。
2.2.3 准备
这个阶段正式为类分配内存并设置变量初始值,这里所指的变量含static修饰的变量,其他类成员变量实例初始化阶段才会赋值。
但是这里的初始值是指数据类型的零值,而不是我们常说的“对变量进行初始化”,如public static int a = 12;之中把a赋值为12的putstatic指令是程序编译后存放于clinit()方法中的,所以初始化阶段才会将a赋值为12。
特殊情况:ConstantValue属性的值。
static final修饰的字段在编译时生成ConstantValue属性,在类加载的准备阶段直接把ConstantValue的值赋给该字段,可以理解为编译期即把结果放进常量池。
public class Test{ public static int a1 = 12;//在类加载的连接-准备阶段,将int的零值0赋给a1,在初始化阶段才将12赋给a1 public final int a2=13;//final修饰的字段在运行时初始化,可以直接赋值也可以在实例构造器中赋值,赋值后不可更改。 public static final int a3=14;//在编译时生成ConstantVlaue属性的值12,在类加载的连接-准备阶段直接把ConstantValue的值赋给a3 }
2.2.4 解析
这个阶段是虚拟机将符号引用替换为直接引用的过程。
符号引用以一组符号来描述引用的目标,符号可以是任何形式的字面量。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机的内存布局相关。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,分别对应常量池的7种常量类型。
2.2.5 初始化
前面过程都是以虚拟机主导,而初始化阶段开始执行类中的java代码。运行clinit(),初始化类变量,静态代码块。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块static{}中的语句合并产生的,编译器收集的顺序是由语句出现在源文件中的顺序决定了,静态语句块只能访问到定义在静态语句块之前的变量,定义在静态语句块之后的变量,在该静态语句块可以赋值,但是不能访问
2.3 类加载器
通过类的全限定名来获取描述类的二进制字节流。
2.3.1 类加载器类型
对于java虚拟机来说,只存在两种类加载器,一种是启动类加载器(C++实现,是java虚拟机的一部分),另一种是其他类加载器(java实现,独立于虚拟机外部,且都继承于java.lang.ClassLoader)
a)启动类加载器
加载lib下或被-Xbootclasspath路径下的类
b)拓展类加载器
加载lib/ext或者被java.ext.dirs系统变量所指定的路径下的类
c)应用程序类加载器
ClassLoader负责,加载用户路径上所指定的类库
2.3.2 双亲委派模型
除顶层启动类加载器之外,其他都有自己的父加载器。
双亲委派模型工作过程:如果一个类加载器收到一个类加载请求,它首先不会自己加载,而是把这个请求委派给父类加载器,只有父类加载器无法完成时子类才会尝试加载。
2.3.3 破坏性双亲委派模型
为什么使用双亲模型?一个是安全,另一个是性能,避免了重复加载和核心类被篡改。但是在以下三种情况中双亲委派模型被破坏:
第一次破坏:由于双亲委派模型是在jdk2.0出现的,而类加载器和抽象类java.lang.ClassLoader在jdk1.0就出现了,在双亲委派模型出现前就已经存在了的用户自定义类加载器是不符合双亲委派模型的,这部分自定义类加载器继承了ClassLoader抽象类,重写了loadClass()方法
第二次破坏:是由双亲委派模型自身的缺陷引起的,为此引入了线程上下文加载器(Thread Context ClassLoader),Java中所有涉及SPI的加载动作基本上都采用了线程上下文加载器,例如JNDI、JDBC、JCE、JAXB和JBI等,破坏了双亲委派模型的层次模型。
第三次破坏:是由用户对程序动态性的追求所导致的,譬如现在所追求的代码热替换、模块热部署等,简单的说就是不需要机器进行重启,部署上就能用。OSGI实现模块热部署的关键就是它自定义的类加载器的实现,在OSGI中,类加载器不再是双亲委派模型的树状结构,而是发展成为更加复杂的网状结构。
有关第二次第三次破坏相关的,可能后面有需要会去进行了解。
3、垃圾回收
3.1 概述
java虚拟机的程序计数器、虚拟机栈、本地方法栈是线程私有的,会随着线程生灭。而堆和方法区则不同,会随着程序运行加载类、调用方法、创建字段、创建实例对象,分配内存空间……这些操作所需要的内存空间大小和什么时候可以释放空间回收内存都是在程序运行期间才知道的,C++中是人为地管理这部分空间,而java中实现了垃圾回收器来进行这部分内存空间的管理。
3.2 对象已死吗?
要合理地进行内存回收,就需要判断哪些对象是“死”的,哪些对象是“活”的。
3.2.1 引用计数算法(reference-counting)
在对象头部维持一个引用计数器,每次增加对该对象的引用则+1,引用失联则-1,当引用计数器为0则说明对象已死。
优点:简单高效。
缺点:无法区分强、软、弱、虚引用类别,而且当出现循环引用的问题,就会成锁,造成内存溢出。
public class A{ /*非循环引用情况下*/ B b1 = new B();//b1指向对象B1的引用计数器counter=1 b1 = null; //b1.counter=0,回收b1指向对象的内存 /*循环引用*/ B b2 = new B();//b2指向对象B2的counter=1; B b3 = new B();//b3指向对象B3的counter=1; b2.instance = b3;//B2的counter=2;维持着两个引用,一个来自于b2,一个来自于B3.instance b3.instance = b2;//B3的counter=2; b2 = null; //B2的counter = 1;维持着来自于B3.instance的引用 b3 = null;//B3的counter = 1; //虽然在程序中我们已经无法访问到B2和B3的空间了,但是双方互相引用,彼此维持着counter=0的情况,若是使用引用计数算法,这两个内存永远无法回收也无法使用了。 } class B{ public Object instance = null; }
3.2.2 可达性分析算法(Gc Roots Tracing)
通过一系列GC Roots对象为起点,向下搜索,走过的路径成为引用链。而当一个对象到任一GC Roots对象都没有引用链相连的时候,则证明对象是不可用的。
在可达性分析中被标记为不可达的对象并不意味着“死亡”,它会被暂时标记并且进行一次筛选,筛选条件是是否有必要执行finalize()方法,如果被判定有必要执行finaliza()方法,就会进入F-Queue队列中,并有一个虚拟机自动建立的、低优先级的线程去执行它。稍后GC将对F-Queue中的对象进行第二次小规模标记。如果这时还是没有新的关联出现,那基本上就真的被回收了。
在可达性分析算法可以作为GC Roots对象的包含下面几种情况:
a)虚拟栈中的局部变量表引用的对象
b)本机方法栈中的JNI所引用的对象
c)方法区的静态变量和常量所引用的对象
如图所示,reference1,reference2,reference3作为GC Roots有以下三条引用链
reference1-->对象实例1
reference2-->对象实例2
reference3-->对象实例4-->对象实例6
而对象实例3和对象实例5则是这次可达性分析中被标记为不可达对象的实例
3.2.3 引用类型
在引用计数法中提到了四种引用类型,强引用、软引用、弱引用和虚引用,这里列一下概念。
强引用(Strong Reference)
强引用则是我们程序中经常见到的,譬如“Object obj = new Object()”这类引用,只要强引用还存在,垃圾回收器永远不会将被引用的对象回收。
软引用(Soft Reference)
软引用是描述一些还有用但不是必须的对象,被软引用的对象在系统将要发生内存溢出前会被列入回收范围。
弱引用(Weak Reference)
弱引用也是描述非必须对象,而且比软引用更弱。无论是否发生内存溢出,弱引用对象都会在下一次垃圾回收发生时被回收。
虚引用(Phantom Reference)
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在回收这个对象时收到一个系统通知。
3.2.4 方法区回收
可以看到前面更多地着眼于堆的垃圾回收,这部分垃圾回收效率较高,而方法区的垃圾收集效率则远低于此。
方法区垃圾回收主要是两部分内容:废弃的常量和无用的类。
判断废弃常量:一般是判断没有该常量的引用。
判断无用的类:
a)该类的所有实例都已经被回收,java堆中不存在该类的实例
b)加载该类的ClassLoader已经被回收
c)该类对应的java.lang.Class对象没有被引用,并且无法通过反射机制被访问。
3.3 内存回收方法
就是最简单的将需要回收的对象标记出来,然后把这些对象在内存中的信息清除。
缺点:效率不高,空间会产生大量碎片。
3.3.2 标记-整理算法:标记-清除-压缩
这个算法在标记-清除的算法之上再进行一下压缩空间,重新移动动向的过程。由于压缩空间需要一定的时间,会影响垃圾收集的效率。
3.3.3 复制算法:标记-清除-复制
把内存分为两个空间,一个空间用来负责装载正常的对象信息,另一个内存空间用于垃圾回收。每次把其中一个空间存活的对象都复制到另一个空间,然后再把这个空间一次性删除。这个算法比标记-清除-压缩效率高,但是需要两块空间,对内存要求比较大,内存的利用率较低,适用于短生存期的对象。
3.3.4 分代回收
基于对象的生存期长短,将对象分为新生代、老年代,针对性地使用内存回收算法。
3.3.4.1 新生代
新生代通常存活时间较短,因此基于标记-清除-复制算法来进行回收。
3.3.4.2 老年代
在垃圾回收多次,如果对象仍然存活,并且新生代的空间不够,则对象会存放在老年代。
老年代采用的是标记-清除-压缩算法。使用标记-清除来回收空间时会产生很多零碎空间(即浮动垃圾),当老年代的零碎空间不足以分配的时候,就会采用压缩算法,在压缩的时候,应用需要暂停。
3.3.4.2 持久代
4、总结
大概先写到这里,其实还有很多东西并没有深入,譬如说性能优化,线程与内存模型,破坏双亲委托模型,Hotspot等等,很多都是出自《[深入理解java虚拟机》,后期需要去再看看理理。大部分是从别人的描述中挑挑拣拣抄过来,但是每个人的描述和理解都各有偏重点,自己写的时候才是以自己的思路建立概念……但是学习的时候有选择性地跳过了某些知识点,想着深入了解或许可能在下一篇或者下下下篇吧,希望不要鸽。
总之写完了,晃晃脑子是否空空如也一无所获?
参考