java中一个实例对象被创建的过程详解

Objects, Classes and ClassLoaders

对象(Objects),类(Classes)以及类加载器(ClassLoaders)

在Java中一切皆是对象(Object),并且所有对象都是由它们的类(Class)指定的。所以每一个对象都有一个到java.lang.Class(用于描述对象的结构)的实例的引用。

Person boss = new Person();

Java虚拟机(JVM)需要理解待创建对象的结构,为此,JVM会查找叫Person的类。并且,如果在程序的这一次运行中,Person类是第一次被访问的话,它必须要被JVM加载(loaded),通常这是从对应的Person.class文件加载的。从磁盘上查找Person.class文件、将其加载到内存中,并解析它的结构的过程,就叫作类的加载(class loading)。保证合适的类被加载是类加载器(ClassLoader)的职责。ClassLoader是java.lang.ClassLoader类的实例,并且Java程序中的每一个类都必须被一些ClassLoader加载,每一个classloader都持有它所加载的所有类的引用。

Java中一个实例对象被创建的过程

一、类的加载过程

首先,Jvm在执行时,遇到一个新的类时,会到内存中的方法区去找class的信息,如果找到就直接拿来用,如果没有找到,就会去将类文件加载到方法区。在类加载时,静态成员变量加载到方法区的静态区域,非静态成员变量加载到方法区的非静态区域。

静态代码块是在类加载时自动执行的代码,非静态代码块是在创建对象时自动执行的代码,不创建对象不执行该类的非静态代码块。

加载过程:

1、JVM会先去方法区中找有没有相应类的.class存在。如果有,就直接使用;如果没有,则把相关类的.clss加载到方法区。

2、在.class加载到方法区时,先加载父类再加载子类;先加载静态内容,再加载非静态内容

3、加载静态内容:

  • 把.class中的所有静态内容加载到方法区下的静态区域内
  • 静态内容加载完成之后,对所有的静态变量进行默认初始化
  • 所有的静态变量默认初始化完成之后,再进行显式初始化
  • 当静态区域下的所有静态变量显式初始化完后,执行静态代码块

4、加载非静态内容:把.class中的所有非静态变量及非静态代码块加载到方法区下的非静态区域内。

5、执行完之后,整个类的加载就完成了。

对于静态方法和非静态方法都是被动调用,即系统不会自动调用执行,所以用户没有调用时都不执行,主要区别在于静态方法可以直接用类名直接调用(实例化对象也可以),而非静态方法只能先实例化对象后才能调用。

 

二、对象的创建过程

1、new一个对象时,在堆内存中开辟一块空间。

2、给开辟的空间分配一个地址。

3、把对象的所有非静态成员加载到所开辟的空间下。

4、所有的非静态成员加载完成之后,对所有非静态成员变量进行默认初始化。

5、所有非静态成员变量默认初始化完成之后,调用构造函数。

6、在构造函数入栈执行时,分为两部分:先执行构造函数中的隐式三步,

        ====①执行super()语句   ②对开辟空间下的所有非静态成员变量进行显示初始化  ③执行构造代码块====

再执行构造函数中书写的代码。

7、在整个构造函数执行完并弹栈后,把空间分配的地址赋给引用对象。

注:  super语句,可能出现以下三种情况:

1)构造方法体的第一行是this()语句,则不会执行隐式三步,而是调用this()语句所对应的的构造方法,最终肯定会有第一行不是this语句的构造方法。

2)构造方法体的第一行是super()语句,则调用相应的父类的构造方法, 

3)构造方法体的第一行既不是this()语句也不是super()语句,则隐式调用super(),即其父类的默认构造方法,这也是为什么一个父类通常要提供默认构造方法的原因。

转载自:Java类的加载和对象创建流程的详细分析

 

Java的内存泄漏基本上按照内存区域的划分可以分为:

  1. 堆(heap)内存泄漏:大家都比较熟悉

  2. 栈(stack)内存泄漏:当前线程运行期间维护的中间变量等信息过多,例如常见的死循环引起stack over flow

  3. 方法区(permanent heap)内存泄漏:分析其原因的文章较少,本文的着重点。

运行时数据区分类

Java虚拟机的运行时数据区一般分类如下(不一定是物理划分):

  1. 堆:主要存放对象实例,线程共享
  2. 栈:主要存储特定线程的方法调用状态,线程独占
  3. 本地方法栈:存储本地方法的调用状态,线程独占
  4. PC寄存器:学过操作系统课程的都知道,线程独占
  5. 方法区:主要存储了类型信息,线程共享

方法区可以简单的等价为所谓的PermGen区域(永久存储区),在很多虚拟机相关的文档中,也将其称之为"永久堆"(permanent heap),作为堆空间的一部分存在。介于此,我们可以简单说明一下我们常用的几个堆内存配置的参数关系:
*-XX: PermSize:*永久堆(Pergen区域)大小默认值
*-XX:MaxPermSize:*永久堆(Pergen区域)最大值
*-Xms:*堆内存大小默认值
*-Xmx:*堆内存最大值

运行时数据区访问方式总结

从开发者角度,虚拟机运行时数据区的访问方式简要归纳如下:

  1. 活动的线程可以通过对应的栈来访问运行时数据区信息
  2. 栈是堆访问的入口
  3. 堆上Java.lang.Class实例是访问PermGen区域中类型信息的入口 

PermGen OOM原因总结

通过上面的测试程序分析,我们发现PermGen OOM发生的原因和类型装载、类型卸载有直接的关系,可以对PermGen OOM发生的原因做如下大致的总结:

  1. 为PermGen区域分配的堆空间过小,可以通过合理的设置-XX: PermSize参数和-XX:MaxPermSize参数来解决。
  2. 类型卸载不及时,过时无效的类型信息占用了空间,我们不妨称其为"永久堆"的内存泄漏,需要通过深入分析类型卸载的原理来寻找对应的防范措施。

常见的类加载器和类型卸载的可能性总结

     通过前面的讨论,我们知道如果加载某种类型的类加载器实例没有处于unreachable状态,则该类型就不会被卸载,该类型不被卸载,则对应的类型信息在PermGen区域中占有的堆内存就不会被释放。下面,针对典型的Java应用分类,分析一下常用类加载器加载的类型被下载的可能性。


【普通Java应用】
系统类加载器:由于其负责加载虚拟机的核心类型,所以由其加载的类型在整个程序运行期间不可能被卸载,对应类型信息占用的PermGen区域堆空间不可能得到释放。
扩展类加载器:负责加载JDK扩展路径下的类型,扩展类加载器同时又作为系统类加载器的父类加载器,所以,由其加载的类型在整个程序运行期间基本上不可能被卸载,对应类型信息占用的PermGen区域堆空间基本不可能得到释放。
系统类加载器:负责加载程序类路径上面的类型,由其加载的类型在整个程序运行期间基本上不可能被卸载,对应类型信息占用的PermGen区域堆空间基本不可能得到释放。
用户自定义类加载器:对于其加载的类型,满足类型卸载要求的可能性比较容易控制,只要是其实例本身处于unreachable状态,其加载的类型会被卸载,PermGen区域中对应的空间占有也会被释放。

 

【插件开发】
系统类加载器:由于其负责加载虚拟机的核心类型,所以由其加载的类型在插件应用运行期间不可能被卸载,对应类型信息占用的PermGen区域堆空间不可能得到释放。
插件类加载器:系统插件类加载器负责加载OSGI实现的相关类型,所以由其加载的类型在插件应用运行期间不可能被卸载;用户开发的插件所使用的默认插件类加载器,和特定的插件本身进行域绑定,插件之间存在一定的类型引用关系,并且特定插件在整个插件应用的运行时被停止的可能性也很小,所以类型卸载发生几率极小。
用户自定义类加载器:对于其加载的类型,满足类型卸载要求的可能性比较容易控制,只要是其实例本身处于unreachable状态,其加载的类型会被卸载,PermGen区域中对应的空间占有也会被释放。

PermGen内存溢出的应对措施

     通过上面的PermGen OOM的原因的分析,不难看出对应的应对措施:

  1. 合理的设置-XX: PermSize和-XX:MaxPermSize参数(主要的有效措施)
  2. 有效的利用的虚拟机类型卸载的机制(针对程序进行调优)

合理设置参数(针对普通用户和开发者)

     通过设置合理的XX: PermSize和-XX:MaxPermSize参数值是减少和有效避免PermGen OOM发生的最有效最主要的措施,尤其是针对普通用户而言,这基本上是唯一的办法。关于合理设置这两个参数,建议如下:

  1. XX: PermSize参数的设置尽量建立在基准测试的基础之上,可以利用监控工具对稳定运行期间PermGen区域的大小进行统计,取合理的平均值。网上的很多资料中,建议XX: PermSize和XX:MaxPermSize设置为相同的数值,个人觉得这是不正确的,因为两个参数的出发点是不一样的。XX: PermSize设置的过大肯定会在应用运行的大部分时间中浪费堆内存,有可能会明显增加存放普通对象实例的堆空间的垃圾收集的次数。
  2. XX:MaxPermSize参数的设置应该着眼于PermGen区域使用的峰值,因为这是避免PermGen OOM的最后一道屏障,其设置最好也是建立在性能监控工具的统计结果之上。
  3. 和虚拟机有关的性能参数较多的分为两类,一类是初始值或默认值,一类是峰值。如果该性能参数是会涉及到的虚拟机垃圾收集机制的,关于初始值或者默认值的设置尽量要建立在测试基础之上,尽量做到在单次垃圾收集时间和垃圾收集频率之间保持一个平衡,否则很有可能适得其反。

有效利用虚拟机类型卸载机制(针对开发者)

此部分的建议可以作为开发者进行性能调优或者日常开发时候的参考,尽量能够配合相应的性能监控工具进行:

  1. 检查是否由于程序设计本身上的缺陷,导致加载了大量实际上并不需要的类型。较新版本的Java虚拟机实现,一般都遵循动态解析的建议,所以不是人为设计的缺陷,一般不会诱发加载了大量实际上并不需要的类型。结合插件开发的应用场景,个人觉得插件功能模块的划分(其中包括了插件依赖关系的设计和有关扩展点的扩展收集等)和第三方jar的使用可能是诱发此问题的两个重要根源。
  2. 对象缓存的使用是否得当,通过前面的分析,我们知道这可能是导致类型不能被卸载的重要原因。缓存的使用,既要认识到其可以提高时间性能的有点,也要分析其可能会给普通对象堆空间和PermGen区域造成的负担。
  3. 自定义类加载器的合理使用,相关的几个注意要点包括:
    1. 是否不恰当的利用的类型更新的特性,也就是说是否对类加载器实例的unreachable状态做了有效的判断。考虑如下场景,假设用户开发了一个自定义类加载器来加载工程输出目录下的临时类型,对临时类型做了不必要的缓存,这肯定会导致所有被加载过的临时类型都不会得到卸载,会直接加重PermGen区域的负担。
    2. 自定义类加载器和其他已有类加载器的协作关系是否合理,是否合理的利用了Java类加载的双亲委派机制。我们知道,不同的类加载器实例(哪怕是同一种类加载器类型的不同实例)加载的同一种自定义类型在虚拟机内部都会被放置到不同的命名空间中作为不同类型来处理,所以合理的设置父类加载器变得很重要,不合理的设置会导致大量不必要的"新"类型被创造出来,况且这些不必要的"新"类型是否能够被及时卸载还是个未知数。
  4. 慎重检查自定义类加载器实例是否被不恰当的缓存了,原因不言而喻。

转载自:

 

 

你可能感兴趣的:(开发工具)