java进阶之阶段一

Posted caozhenghua

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java进阶之阶段一相关的知识,希望对你有一定的参考价值。

1.几个常用单词

1.1 jdk

? JDK是 Java 语言的软件开发工具包,主要用于移动设备嵌入式设备上的java应用程序。JDK是整个java开发的核心,它包含了JAVA的运行环境(JVM+Java系统类库)和JAVA工具。

jdk1.8新特性

  1. Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可。
  2. 新增lambda表达式
  3. 提供函数式接口
  4. Java 8 允许你使用关键字来传递方法或者构造函数引用
  5. 我们可以直接在lambda表达式中访问外层的局部变量。

JDK包含的基本组件包括:

? javac – 编译器,将源程序转成字节码

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

? javadoc – 文档生成器,从源码注释中提取文档

? jdb – debugger,查错工具

? java – 运行编译后的java程序(.class后缀的)

? appletviewer:小程序浏览器,一种执行HTML文件上的Java小程序的Java浏览器。

? Javah:产生可以调用Java过程的C过程,或建立能被Java程序调用的C过程的头文件。

? Javap:Java反汇编器,显示编译类文件中的可访问功能和数据,同时显示字节代码含义。

? Jconsole: Java进行系统调试和监控的工具

1.2JRE

? 是java运行时环境,包含了java虚拟机,java基础类库。是使用java语言编写的程序运行所需要的软件环境,是提供给想运行java程序的用户使用的。

? 技术图片

1.3 JVM

什么是jvm?

? JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

技术图片

JVM的实现原理

? 1,类装载器(ClassLoader)主要负责加载class文件,是否能执行主要取决于execution engine它是负责执行被加载类中包含的指令。有两种类加载器分别为启动类加载器和用户自定义类加载器,然而启动类加载器是JVM实现的一部分,用户自定义类加载器是Java程序一部分。

技术图片

? 2,本地方法栈(native method stack)主要作用是登记native方法,然后在execution engine执行的时候加载本地方法库。

? 3,栈有时我们又叫栈内存,负责Java程序的运行,它是在线程创建时创建的,所以生命周期也是和线程生命周期一致,同时消亡,线程结束了栈也就释放,特别提醒的是栈不存在垃圾回收的问题,因为线程结束栈就是释放了。平时我们写的类变量、引用类型变量、实例方法等等都是在函数的栈内存分配好。

? 4,程序计数器,是指方法区中的方法字节码由引擎读取下一条指令,它是一个非常小的内存空间。为什么有这种东西呢,大家都知道每个线程都是有一个程序计数器的,是线程私有的,相当一个指针。

? 5,方法区它是指线程共享的,谁都可以共享使用,我们通常用来保存装载类的元结构信息。

? 6,堆(heap)它是Java虚拟机用来存储对象实例的,比我们在开发过程使用的new对象,只要通过new创建的对象的内存的对象都在堆分配,注意一点的是堆中的对象内存需要等待垃圾器(GC)进行回收,也是Java虚拟机共享区。

? 7,本地接口(native interface)作用是融合不同的编程语言为Java所用,注意底层是C、C++写的,学习JVM时了解C语言一些更好,最起码能看懂,这个方法的行为就是native method stack中登记native方法,然后在execution engine执行时加载native libraries的。

JVM运行时数据区

技术图片

第一块:PC寄存器

PC寄存器是用于存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。

第二块:JVM栈

JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。

第三块:堆(Heap)

它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。

(1) 堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的

(2) Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配

(3) TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。

(4) 所有新创建的Object 都将会存储在新生代Yong Generation中。如果Young Generation的数据在一次或多次GC后存活下来,那么将被转移到OldGeneration。新的Object总是创建在Eden Space。

第四块:方法区域(Method Area)

(1)在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。

(2)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

第五块:运行时常量池(Runtime Constant Pool)

存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。

第六块:本地方法堆栈(Native Method Stacks)

JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。

1.4 JUC

? java.util.concurrent(简称JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的Collection 实现等。

技术图片

1.5 Java SE

? JSE 指标准版一般用于用户学习JAVA语言的基础也是使用其他两个版本的基础主要用于编写C/S项目和提供标准的JAVA类库,是所有基于Java语言开发的基础,该版本主要用于开发桌面应用程序。

1.6 Java EE

? JEE 指企业版依托互连网技术提供企业级平台应用说白了就是用来构建大型网站和B/S系统 ,作为一个企业版本,主要是给出一个开发企业级应用架构的解决方案,同时给出了在这个架构中相关组件以供开发人员使用,例如我们连接数据库所用的JDBC。

1.7 JME

? JME 指移动版为小型移动器械搭建使用平台主要是用来为手机编程,制作手机相关软件的 三个版本一个是做C/S项目如QQ 一个是做网站如163 一个是做手机系统如大部分手机的小游戏 ,是针对移动设备,嵌入式系统的开发。

2.java运行流程

java程序执行过程分为两步,下图为流程示意图

? 第一步:将java源码(.java文件)通过编译器(javac.exe)编译成JVM文件(.class文件)

? 第二步:将JVM文件通过java.exe执行,输出结果

技术图片

通过如上分析,我们发现JVM至关重要,其向上屏蔽了操作系统的差异,也正因为JVM的该作用,才使java这门编程语言能够实现跨平台,

其原理大致可描述为如下:

技术图片

3.GC

3.1 什么是GC?

理解GC机制就从:“GC的区域在哪里”,“GC的对象是什么”,“GC的时机是什么”,“GC做了哪些事”几方面来分析。

? GC (Garbage Collection, 垃圾回收) 是指一种自动的内存管理机制. 在这种机制中由垃圾回收器来负责回收用户进程中不再使用的对象所占据的内存.

为什么要有GC?

  • 安全性考虑;-- for security.
  • 减少内存泄露;-- erase memory leak in some degree.
  • 减少程序员工作量。-- Programmers don‘t worry about memory releasing.

哪些内存需要回收?

? 内存运行时JVM会有一个运行时数据区来管理内存。它主要包括5大部分:程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap).而其中程序计数器、虚拟机栈、本地方法栈是每个线程私有的内存空间,随线程而生,随线程而亡。例如栈中每一个栈帧中分配多少内存基本上在类结构去诶是哪个下来时就已知了,因此这3个区域的内存分配和回收都是确定的,无需考虑内存回收的问题。但方法区和堆就不同了,一个接口的多个实现类需要的内存可能不一样,我们只有在程序运行期间才会知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC主要关注的是这部分内存。

GC主要进行回收的内存是JVM中的方法区
涉及到多线程(指堆)、多个对该对象不同类型的引用(指方法区),才会涉及GC的回收。

什么时候回收?

对象“已死”的判定算法

由于程序计数器、Java虚拟机栈、本地方法栈都是线程独享,其占用的内存也是随线程生而生、随线程结束而回收。而Java堆和方法区则不同,线程共享,是GC的所关注的部分。

在堆中几乎存在着所有对象,GC之前需要考虑哪些对象还活着不能回收,哪些对象已经死去可以回收。

有两种算法可以判定对象是否存活:

1.)引用计数算法:给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。但是它很难解决两个对象之间相互循环引用的情况。

2.)可达性分析算法:通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明此对象已死、可回收。Java中可以作为GC Roots的对象包括:虚拟机栈中引用的对象、本地方法栈中Native方法引用的对象、方法区静态属性引用的对象、方法区常量引用的对象。

在主流的商用程序语言(如我们的Java)的主流实现中,都是通过可达性分析算法来判定对象是否存活的。

无论是引用计数器还是可达性分析,判定对象是否存活都与引用有关!那么,如何定义对象的引用呢?

当内存空间还够时,能够保存在内存中;如果进行了垃圾回收之后内存空间仍旧非常紧张,则可以抛弃这些对象。所以根据不同的需求,给出如下四种引用,根据引用类型的不同,GC回收时也会有不同的操作:

  • 强引用(Strong Reference):Object obj = new Object();只要强引用还存在,GC永远不会回收掉被引用的对象。
  • 软引用(Soft Reference):描述一些还有用但非必需的对象。在系统将会发生内存溢出之前,会把这些对象列入回收范围进行二次回收(即系统将会发生内存溢出了,才会对他们进行回收。)
  • 弱引用(Weak Reference):程度比软引用还要弱一些。这些对象只能生存到下次GC之前。当GC工作时,无论内存是否足够都会将其回收(即只要进行GC,就会对他们进行回收。)
  • 虚引用(Phantom Reference):一个对象是否存在虚引用,完全不会对其生存时间构成影响。

关于方法区中需要回收的是一些废弃的常量无用的类

  1. 废弃的常量的回收。这里看引用计数就可以了。没有对象引用该常量就可以放心的回收了。
  2. 无用的类的回收。什么是无用的类呢?
  • 该类所有的实例都已经被回收。也就是Java堆中不存在该类的任何实例;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

3.2 GC常用算法

标记-清除算法

最基础的算法,分标记和清除两个阶段:首先标记处所需要回收的对象,在标记完成后统一回收所有被标记的对象。

它有两点不足:一个效率问题,标记和清除过程都效率不高;一个是空间问题,标记清除之后会产生大量不连续的内存碎片(类似于我们电脑的磁盘碎片),空间碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

技术图片

复制算法

为了解决效率问题,出现了“复制”算法,他将可用内存按容量划分为大小相等的两块,每次只需要使用其中一块。当一块内存用完了,将还存活的对象复制到另一块上面,然后再把刚刚用完的内存空间一次清理掉。这样就解决了内存碎片问题,但是代价就是可以用内容就缩小为原来的一半。

技术图片

标记-整理算法

复制算法在对象存活率较高时就会进行频繁的复制操作,效率将降低。因此又有了标记-整理算法,标记过程同标记-清除算法,但是在后续步骤不是直接对对象进行清理,而是让所有存活的对象都向一侧移动,然后直接清理掉端边界以外的内存。

技术图片

分代收集算法

当前商业虚拟机的GC都是采用分代收集算法,这种算法并没有什么新的思想,而是根据对象存活周期的不同将堆分为:新生代和老年代,方法区称为永久代(在新的版本中已经将永久代废弃,引入了元空间的概念,永久代使用的是JVM内存而元空间直接使用物理内存)。

这样就可以根据各个年代的特点采用不同的收集算法。

技术图片

新生代中的对象“朝生夕死”,每次GC时都会有大量对象死去,少量存活,使用复制算法。新生代又分为Eden区和Survivor区(Survivor from、Survivor to),大小比例默认为8:1:1。

老年代中的对象因为对象存活率高、没有额外空间进行分配担保,就使用标记-清除或标记-整理算法。

新产生的对象优先进去Eden区,当Eden区满了之后再使用Survivor from,当Survivor from 也满了之后就进行Minor GC(新生代GC),将Eden和Survivor from中存活的对象copy进入Survivor to,然后清空Eden和Survivor from,这个时候原来的Survivor from成了新的Survivor to,原来的Survivor to成了新的Survivor from。复制的时候,如果Survivor to 无法容纳全部存活的对象,则根据老年代的分配担保(类似于银行的贷款担保)将对象copy进去老年代,如果老年代也无法容纳,则进行Full GC(老年代GC)。

大对象直接进入老年代:JVM中有个参数配置-XX:PretenureSizeThreshold,令大于这个设置值的对象直接进入老年代,目的是为了避免在Eden和Survivor区之间发生大量的内存复制。

长期存活的对象进入老年代:JVM给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将被移入Survivor并且年龄设定为1。每熬过一次Minor GC,年龄就加1,当他的年龄到一定程度(默认为15岁,可以通过XX:MaxTenuringThreshold来设定),就会移入老年代。但是JVM并不是永远要求年龄必须达到最大年龄才会晋升老年代,如果Survivor 空间中相同年龄(如年龄为x)所有对象大小的总和大于Survivor的一半,年龄大于等于x的所有对象直接进入老年代,无需等到最大年龄要求。

3.3 垃圾收集器

Serial收集器

单线程收集器,表示在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。"Stop The World".

ParNew收集器

实际就是Serial收集器的多线程版本。

  • 并发(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
  • 并行(Concurrent):指用户线程与垃圾收集线程同时执行,用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

Parallel Scavenge收集器

该收集器比较关注吞吐量(Throughout)(CPU用于用户代码的时间与CPU总消耗时间的比值),保证吞吐量在一个可控的范围内。

CMS(Concurrent Mark Sweep)收集器

CMS收集器是一种以获得最短停顿时间为目标的收集器。

G1(Garbage First)收集器

从JDK1.7 Update 14之后的HotSpot虚拟机正式提供了商用的G1收集器,与其他收集器相比,它具有如下优点:并行与并发;分代收集;空间整合;可预测的停顿等。

4.类加载器

  1. JVM如何加载Class文件?
  2. Class文件中的信息进入到虚拟机后会发生什么变化?
    简而言之,JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(loading), 验证(verification), 准备(preparation), 解析(resolution), 初始化(initialization), 使用(using), 卸载(unloading).

加载(Loading)

JVM在此阶段需要完成:

  • 通过一个类的全限定名来获取定义此类的二进制字节流(可以从.class文件,从网络中-Applet,动态代理,从JSP文件生成);
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

验证(Verification)

该阶段的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。直接决定了JVM能否承受恶意代码的攻击。

准备(Preparation)

准备阶段正式为类变量分配内存并设置类变量(不包含实例变量)初始值,这些变量所使用的内存都将在方法区中进行分配。

public static int value = 123;

注意:类变量value的值在准备阶段后的初始值为0而不是123,把value赋值为123的动作在初始化阶段才会被执行。

解析(Resolution)

解析阶段是JVM将常量池内的符号引用替换为直接引用的过程。
需要进行类或接口的解析,字段解析,方法解析。

类或接口的解析

假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,则JVM会完成3个步骤:

  1. 如果C不是一个数组类型,JVM会把代表N的全限定名传递给D的类加载器去加载这个类;
  2. 如果C是一个数组类型,则符号引用N会是形如"[Ljava/lang/Integer"的形式,则需要加载的元素类型就是"java.lang.Integer",然后JVm会生成一个代表数组维度和元素的数组对象;
  3. 如果上述步骤都成功,则C在JVM中已经成为一个有效的类或接口了。最后再检查D是否具备对C的访问权限,如果不具备,将会抛出"java.lang.IllegalAccessError"异常。

字段解析

要解析一个未被解析过得字段符号引用,则首先会解析字段所属的类或接口的符号引用,设为C,然后JVM会从这个类开始逐级往父类或接口找与这个字段相匹配的字段:

  1. 如果该类C本省就包含了名称与描述符都匹配的字段,则返回这个字段的直接引用,over;
  2. 否则,如果C中实现了接口,则按照继承关系从下往上搜索各个接口和它的父接口,寻找与之相匹配的字段,找到则over;
  3. 否则,如果C不是java.lang.Object的话,会按照继承关系从下往上搜索其父类,搜索到则over;
  4. 否则,查找失败,抛出"java.lang.NoSuchFieldError"异常。

类方法解析

其中,方法解析分为类方法解析与接口方法解析,两者略有不同。
对于类方法解析,同字段解析一样,首先要解析出该类方法所属的类或接口的符号引用,假设解析为C:

  1. 如果在类方法表中发现索引C是个接口,则直接抛出java.lang.IncompatibleClassChangeError异常;
  2. 在类C中查找是否含有简单名称和描述符都与目标相匹配的方法,有则返回这个方法的直接引用,over;
  3. 否则,在类C的父类中递归查找是否有简单名称和描述符都匹配的方法,如果有则返回这个方法的直接引用,over;
  4. 否则,在类C实现的接口列表和它们的父接口中递归查找,如果存在,就说明类C是一个抽象类,查找结束,抛出"java.lang.AbsrtactMethodError"异常;
  5. 否则,查找失败,抛出"java.lang.IllegalAccessError"异常。

接口方法解析

接口方法也要首先解析出该方法所属的类或接口,假设为C:

  1. 如果在接口方法表发现索引C是一个类,则抛出"java.lang.IncompatibleClassChangeError"异常;
  2. 否则,在接口C中查找是否有简单名称和描述符相匹配的犯法,有则返回这个方法的直接引用,over;
  3. 否则,在接口C的父接口中递归查找,知道java.lang.Object类,看是否有匹配的方法,有则返回直接引用,over;
  4. 否则,查找失败,抛出"java.lang.NoSuchMethodError".

初始化(Initialization)

初始化时类加载过程的最后一步,在这个阶段,才真正开始执行类中定义的Java程序代码。
在Preparation阶段,已经给类变量有过一次初始的赋0或null的设置,是执行类构造器clinit()方法的过程。
这里稍微列一下clinit()方法的特点:

  1. clinit()方法是由编译器自动收集类中所有类变量的赋值动作静态语句块(static{}块)中的语句合并产生的。收集器的收集顺序由语句在源文件中出现的顺序决定,静态语句块后面的语句可以进行赋值,但是访问不到(不能print,不能使用)。
  2. 它并不是必需的。如果一个类中红既没有赋值操作,有没有静态语句块,可以没有该方法;
  3. 虚拟机会保证父类的clinit()方法在子类的执行前执行。所以第一个被执行的一定是java.lang.Object;父类中定义的static语句块要由于子类的变量赋值操作;
  4. 虚拟机会保证一个类的clinit()方法子啊多线程环境下被正确的加锁、同步,如果多个线程同时初始化一个类,那么只有一个线程会执行这个类的clinit()方法。

双亲委派模型

从Java虚拟机的角度看,只有两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader):用C++实现,是虚拟机自身的一部分;
  2. 所有其他的类加载器:用Java语言实现,独立于虚拟机外部,都继承自抽象类java.lang.ClassLoader;

从Java开发人员看,类加载器可分为3种:

  1. 启动类加载器(Bootstrap ClassLoader):负责加载<JAVA——HOME>lib目录中的并且可以被虚拟机识别的;
  2. 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>libext目录中的所有类库,开发者可以直接使用扩展类加载器;
  3. 应用程序类加载器(Application ClassLoader):它是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称它为系统类加载器。他负责加载用户类路径(ClassPath)上所指定的类库。

我们的应用程序都是由着3种类加载器互相配合进行加载的,他们的关系如图所示。而这种层次关系称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)而是以组合(Composition)的关系来复用父类加载器。

双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求。它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的加载器都是如此。所以所有的加载请求都会传送到顶层的启动类加载器中加载,只有当父类加载器表示自己无法加载该类时,才会由子加载器尝试加载。

这样做的优势也很直观:因为我们前面有提到如果两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,则这两个类就不相等。为了避免同一个类被识别成不同类,所以应该使用双亲委派模型。







以上是关于java进阶之阶段一的主要内容,如果未能解决你的问题,请参考以下文章

JVM进阶之字节码文件概述

算法竞赛入门码蹄集进阶塔335题(MT3330-3335)

JVM进阶之字节码文件解析指令

JVM进阶之字节码指令解析(中篇)

Java开发进阶知识之学习篇——hashCode和equals

下一代Dex编译器现已进入预览阶段