java 和 JVM

C++和Java的区别

  • 指针:java中不存在指针的概念,编程者无法直接通过指针来直接访问内存,有利于维护java程序的安全

  • 多重继承:C++支持多重继承,java不支持多重继承,但是允许一个类继承多个接口来实现多重继承的问题

  • 数据类型和类:java是完全面向对象的语言,所有的函数和变量必须是类的一部分,而C++中允许将函数和变量设置为全局,兼具面向过程和面向对象的特点

  • 内存管理:Java中由系统进行自动的内存管理和回收,C++中需要程序员手动释放内存资源。当Java中的一个对象不会再用到的时候,无用内存会给它贴上标签以示删除,Java的GC过程是以线程的形式在后台运行,利用空闲时间工作

  • 操作符重载:Java不支持操作符重载,C++支持操作符重载,也是C++一个突出特征

  • 预处理功能:Java不支持预处理,C++有一个预编译阶段,Java没有预处理器,但它提供了import与C++预处理器具有类似功能

  • Java不支持缺省函数参数

  • 字符串:字符串变量

  • 类型转换:C++中有数据类型隐含转换的机制,Java中需要限时强制类型转换

  • 异常:Java中异常机制用于捕获例外事件,增强系统的容错能力

  • Java为解释型语言,程序源代码通过Java编译器编译成字节码,然后由jvm解释执行,C++为编译型语言,源代码通过编译链接后直接生成可执行的二进制代码,可直接执行,因此java的执行速度比C++要慢,但是Java可以跨平台

  • Java具有平台无关性,对每种数据类型都分配了固定长度的空间,但是C++不同,在不同的平台上会分配不同的字节数

Java跨平台的原因

  • 第一点: 我们通常将CPU处理器和操作系统的整体称之为平台,不同的CPU中可能使用不同的指令集(指令集是CPU中用于计算和控制计算机系统的一套指令的集合),而不同操作系统支持不同CPU的指令集
  • C语言的编译过程:windows下通过VS编译成exe文件,Linux下通过gcc编译成elf文件,但是window所编译的exe是不能在linux上运行的,因此结论是:编译器是与平台相关的,编译后的文件也是与平台相关的,我们所说的跨平台是指编译后的文件跨平台,而不是源程序跨平台,这点要注意。
  • 对于Java而言,源程序为.java文件,通过与平台无关的编译器编译成与平台无关的中间码,也就是.class文件,中间码再由解释器(也就是jvm)解释执行,注意解释器是与平台相关的,也就是不同平台需要不同的解释器

JVM:Java虚拟机

https://segmentfault.com/a/1190000004206269#articleHeader0

1. 基本特性

  • JRE由Java api和JVM组成,JVM通过classloader来加载类应用,通过JavaAPI来执行
  • 基于栈结构的虚拟机
  • 符号引用:除了基本类型外所有的Java类型都是通过符号引用来取得关联,而不是通过显式内存地址的引用
  • 垃圾回收机制:显式创建,通过GC自动回收
  • 明确界定基本类型字节长度保证平台的无关性
  • 网络字节序: 基于大端的字节序

2. Java程序的执行过程


类加载将Java字节码载入到运行时数据区,执行引擎负责Java字节码执行;

3. 类加载

Java提供了动态加载的特性,只有在运行时第一次遇到类时才会去加载和链接,而非在编译时加载它。JVM的类加载器负责类的动态加载过程。Java类加载器的特点如下:

  • 层次结构:Java的类加载器按是父子关系的层次结构组织的。Boostrap类加载器处于层次结构的顶层,是所有类加载器的父类。

  • 委派模式:基于类加载器的层次组织结构,类加载器之间是可以进行委派的。当一个类需要被加载,会先去请求父加载器判断该类是否已经被加载。如果父类加器已加载了该类,那它就可以直接使用而无需再次加载。如果尚未加载,才需要当前类加载器来加载此类。

  • 可见性限制:子类加载器可以从父类加载器中获取类,反之则不行。

  • 不能卸载: 类加载器可以载入类却不能卸载它。但是可以通过删除类加载器的方式卸载类。

每个类加载器都有自己的空间,用于存储其加载的类信息。当类加载器需要加载一个类时,它通过FQCN)(Fully Quanlified Class Name: 全限定类名)的方式先在自己的存储空间中检测此类是否已存在。在JVM中,即便具有相同FQCN的类,如果出现在了两个不同的类加载器空间中,它们也会被认为是不同的。存在于不同的空间意味着类是由不同的加载器加载的。
279698375-5700c7e49cda5_articlex.png

当JVM请示类加载器加载一个类时,加载器总是按照从类加载器缓存、父类加载器以及自己加载器的顺序查找和加载类。也就是说加载器会先从缓存中判断此类是否已存在,如果不存在就请示父类加载器判断是否存在,如果直到Bootstrap类加载器都不存在该类,那么当前类加载器就会从文件系统中找到类文件进行加载。

  • Bootstrap加载器:Bootstrap加载器在运行JVM时创建,用于加载Java APIs,包括Object类。不像其他的类加载器由Java代码实现,Bootstrap加载器是由native代码实现的。
  • 扩展加载器(Extension class loader):扩展加载器用于加载除基本Java APIs以外扩展类。也用于加载各种安全扩展功能。
  • 系统加载器(System class loader):如果说Bootstrap和Extension加载器用于加载JVM运行时组件,那么系统加载器加载的则是应用程序相关的类。它会加载用户指定的CLASSPATH里的类。
  • 用户自定义加载器:这个是由用户的程序代码创建的类加载器。

像Web应用服务器(WAS: Web Application Server)等框架通过使用用户自定义加载器使Web应用和企业级应用可以隔离开在各自的类加载空间独自运行。也就是说可以通过类加载器的委派模式来保证应用的独立性。不同的WAS在自定义类加载器时会有略微不同,但都不外乎使用加载器的层次结构原理。

如果一个类加载器发现了一个未加载的类,则该类的加载和链接过程如下图

类加载过程.png

每一步的具体描述如下:

  • 加载(Loading): 从文件中获取类并载入到JVM内存空间。
  • 验证(Verifying): 验证载入的类是否符合Java语言规范和JVM规范。在类加载流程的测试过程中,这一步是最为复杂且耗时最长的部分。大部分JVM TCK的测试用例都用于检测对于给定的错误的类文件是否能得到相应的验证错误信息。
  • 准备(Preparing): 根据内存需求准备相应的数据结构,并分别描述出类中定义的字段、方法以及实现的接口信息。
  • 解析(Resolving): 把类常量池中所有的符号引用转为直接引用。
  • 初始化(Initializing): 为类的变量初始化合适的值。执行静态初始化域,并为静态字段初始化相应的值。

4. 运行时数据区

运行时数据区.png

运行时数据区是JVM运行时操作系统分配的内存区域,运行时数据区可分为6部分,即:为每个线程分别创建的PC寄存器JVM栈本地方法栈,和被所有线程共用的数据堆方法区,和运行时常量池

  • PC寄存器:每一个线程都会有一个Program counter寄存器,随着线程启动而创建,其中存放要执行的JVM指令地址;

  • JVM栈:每一个线程都会有一个JVM栈,随着线程启动而创建,其中存储的数据元素为栈帧,在JVM中一旦有方法执行,JVM都会为之创建一个栈帧,并添加到当前现成的JVM栈中,当方法运行结束后,栈帧也会随之移除。栈帧中保存着对本地变量数组,操作数栈和属于当前运行方法的运行时常量池的引用

  • 本地方法栈:为非Java编写的本地程序定义的栈空间,也就是说它基本上是用于通过JNI(Java Native Interface)方式调用和执行的C/C++代码。根据具体情况,C栈或C++栈将会被创建。

  • 方法区:方法区是被所有线程共用的内存空间,在JVM启动时创建。它存储了运行时常量池、字段和方法信息、静态变量以及被JVM载入的所有类和接口的方法的字节码。不同的JVM提供者在实现方法区时会通常有不同的形式。在Oracle的Hotspot JVM里方法区被称为Permanent Area(永久区)或Permanent Generation(PermGen, 永久代)。JVM规范并对方法区的垃圾回收未做强制限定,因此对于JVM实现者来说,方法区的垃圾回收是可选操作。

  • 运行时常量池:一个存储了类文件格式中的常量池表的内存空间。这部分空间虽然存在于方法区内,但却在JVM操作中扮演着举足轻重的角色,因此JVM规范单独把这一部分拿出来描述。除了每个类或接口中定义的常量,它还包含了所有对方法和字段的引用。因此当需要一个方法或字段时,JVM通过运行时常量池中的信息从内存空间中来查找其相应的实际地址。

  • 数据堆:堆中存储着所有的类实例或对象,并且也是垃圾回收的目标场所。当涉及到JVM性能优化时,通常也会提及到数据堆空间的大小设置。JVM提供者可以决定划分堆空间或者不执行垃圾回收。

5. 执行引擎

JVM通过类加载器把字节码载入运行时数据区是由执行引擎执行的。执行引擎以指令为单位读入Java字节码,就像CPU一个接一个的执行机器命令一样。每个字节码命令包含一字节的操作码和可选的操作数。执行引擎读取一个指令并执行相应的操作数,然后去读取并执行下一条指令。

尽管如此,Java字节码还是以一种可以理解的语言编写的,而不像那些机器直接执行的无法读懂的语言。所以JVM的执行引擎必须要把字节码转换为能被机器执行的语言指令。执行引擎有两种常用的方法来完成这一工作:

  • 解释器(Interpreter):读取、解释并逐一执行每一条字节码指令。因为解释器逐一解释和执行指令,因此它能够快速的解释每一个字节码,但对解释结果的执行速度较慢。所有的解释性语言都有类似的缺点。叫做字节码的语言人本质上就像一个解释器一样运行。
  • 即时编译器(JIT: Just-In-Time):即时编译器的引入用来弥补解释器的不足。执行引擎先以解释器的方式运行,然后在合适的时机,即时编译器把整修字节码编译成本地代码。然后执行引擎就不再解释方法的执行而是通过使用本地代码直接执行。执行本地代码较逐一解释执行每条指令在速度上有较大的提升,并且通过对本地代码的缓存,编译后的代码能具有更快的执行速度。

然而,即时编译器在编译代码时比逐一解释和执行每条指令更耗时,所以如果代码只会被执行一次,解释执行可能会具有更好的性能。所以JVM通过检查方法的执行频率,然后只对达到一定频率的方法才会做即时编译。

执行引擎.png

Java垃圾回收机制

垃圾回收算法和JVM垃圾回收综述

深入解析Java垃圾回收机制

1. 哪些内存需要回收?

  • 引用计数法:判断是否还需要使用,最简单方法是通过目前是否有引用指向这个对象,如果没有说明这个对象就不会再使用了,这种通过引用是否存在的方法叫做引用计数法,但是存在一个问题无法解决就是对象循环引用问题

  • 可达性分析:这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

    在Java语言中,可作为GC Roots的对象包括下面几种:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

2 如何回收?

  • step 1: marking 标记
    第一步是标记,也就是找到那些需要回收的对象和确定哪些对象不需要回收,所有堆中的对象都会扫描一遍,这通常是一个很耗时的过程

Mark.PNG

  • step2 : normal deletion
    垃圾收集器清除掉标记出来的对象区域,简单的清除带来的问题是产生大量的不连续的内存碎片,空间碎片太多可能会导致在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而提前触发一次垃圾回收

  • step 2 :improve -- deletion with compacting 压缩整理
    由于简单的清除可能会存在碎片的问题,所以又出现了压缩清除的方法,也就是先清除需要回收的对象,然后再对内存进行压缩操作,将内存分成可用和不可用两大部分。
    compacting.PNG

3 分代回收

  • 为什么需要分代回收:一个程序中大部分对象都是短命的,因此为了增大GC的效率,将JVM堆分代,分为新生代,老年代和永久代。

分代.PNG

  • 新生代:所有new出来的新对象都在新生代,新生代这部分内存满了之后,就会发起一次GC事件,这种发生在新生代的垃圾回收称为Minor collections,这种收集相对比较快。

  • 老年代:老年代来存储存活事件比较长的对象,一般来说,我们会给新生代的对象限定一个存活的时间,当达到这个时间还没有被收集的时候就会被移动到老年代中。老年代区域的垃圾收集叫做major garbage collection。通常Major garbage collection都相对比较慢,因为老年代的收集包括了对所有对象的收集,也就是同时需要收集新生代和老年代的对象。

  • 永久代:The Permanent generation contains metadata required by the JVM to describe the classes and methods used in the application. The permanent generation is populated by the JVM at runtime based on classes in use by the application. In addition, Java SE library classes and methods may be stored here.
    分代2.PNG

5 分代回收的过程

  • 第一步 所有new出来的对象都会最先分配到新生代区域中,两个survivor区域初始化是为空的

第1步.PNG

  • 第二步,当eden区域满了之后,就引发一次 minor garbage collection

第2步.PNG

  • 第三步,当在minor garbage collection,存活下来的对象就会被移动到S0survivor区域

第3步.PNG

  • 第四步,然后当eden区域又填满的时候,又会发生下一次的垃圾回收,存活的对象会被移动到survivor区域而未存活对象会被直接删除。但是,不同的是,在这次的垃圾回收中,存活对象和之前的survivor中的对象都会被移动到s1中。一旦所有对象都被移动到s1中,那么s2中的对象就会被清除,仔细观察图中的对象,数字表示经历的垃圾收集的次数。目前我们已经有不同的年龄对象了。

第4步.PNG

  • 第五步,下一次垃圾回收的时候,又会重复上次的步骤,清除需要回收的对象,并且又切换一次survivor区域,所有存活的对象都被移动至s0。eden和s1区域被清除。

第5步.PNG

  • 第六步,重复以上步骤,并记录对象的年龄,当有对象的年龄到达一定的阈值的时候,就将新生代中的对象移动到老年代中。在本例中,这个阈值为8.

第6步.PNG

  • 第七步,接下来垃圾收集器就会重复以上步骤,不断的进行对象的清除和年代的移动

  • 最后,我们观察上述过程可以发现,大部分的垃圾收集过程都是在新生代进行的,直到老年代中的内存不够用了才会发起一次 major GC,会进行标记和整理压缩。

Java中对象的生命周期

  1. 创建阶段(created)
    为对象分配存储空间,开始构造对象,从父类到子类对static成员进行初始化,父类成员变量按照顺序初始化,递归调用父类的构造方法,子类成员变量按照顺序初始化,子类构造方法调用,一旦对象被创建,并有某个引用指向它,这个对象的状态就切换到了应用阶段(In Use)

  2. 应用阶段(in use)
    对象至少被一个强引用持有并且对象在作用域内

  3. 不可见阶段(Invisible)
    程序本身不再持有该对象的任何强引用,但是这些引用可能还存在着;一般具体是指程序的执行已经超过该对象的作用域了

  4. 不可达阶段(Unreachable)
    对象处于不可达阶段是指该对象不再被任何强引用所持有。
    与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为”GC root”。存在着这些GC root会导致对象的内存泄露情况,无法被回收。

  5. 收集阶段(Collected)
    当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了finalize()方法,则会去执行该方法的终端操作。
    这里要特别说明一下:不要重载finazlie()方法!原因有两点:

    • 会影响JVM的对象分配与回收速度
      在分配该对象时,JVM需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法;在该方法的执行时需要消耗CPU时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对象执行两次GC。
    • 可能造成该对象的再次“复活”
      在finalize()方法中,如果有其它的强引用再次持有该对象,则会导致对象的状态由“收集阶段”又重新变为“应用阶段”。这个已经破坏了Java对象的生命周期进程,且“复活”的对象不利用后续的代码管理。
  6. 终结阶段
    当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。

  7. 对象空间的重新分配
    垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。

posted @ 2017-09-03 22:02  安安zoe  阅读(732)  评论(0编辑  收藏  举报