打开3d显示无效的文件 MAX文件提示“--类型错误:调用需要函数或类,得到的是:underfined”怎么解决?

1、面向对象的特征有哪些方面?

答:面向对象的特征主要有以下几个方面:

抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽

象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的

继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类

被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让

变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要

手段(如果不能理解请阅读阎宏博士的《Java 与模式》或《设计模式精解》中

关于桥梁模式的部分).

封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问

只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自

治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写

一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,

只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别,

明显全自动洗衣机封装更好因此操作起来更简单;我们现在使用的智能手机也是

封装得足够好的,因为几个按键就搞定了所有的事情)。

多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。

简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分

为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的

服务,那么运行时的多态性可以解释为:当 A 系统访问 B 系统提供的服务时,B

系统有多种提供服务的方式,但一切对 A 系统来说都是透明的(就像电动剃须

刀是 A 系统,它的供电系统是 B 系统,B 系统可以使用电池供电或者用交流电,

甚至还有可能是太阳能,A 系统只会通过 B 类对象调用供电的方法,但并不知道

供电系统的底层实现是什么,究竟通过何种方式获得了动力)。

(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)

实现的是运行时的多态性(也称为后绑定)。

运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:

1). 方法重写(子类继承父类并重写父类中已有的或抽象的方法);

2). 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为).

类的成员不写访问修饰时默认为 default。默认对于同一个包中的其他类相当于公

开(public),对于不是同一个包中的其他类相当于私有(private)。受保护

(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私

有。Java 中,外部类的修饰符只能是 public 或默认,类的成员(包括内部类)的

修饰符可以是以上四种。

3、String 是最基本的数据类型吗?

type),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。

具体内容篇幅较长共485页,20个技术点,1000道面试题.

下面截取部分问题展示,需要完整文档的看最下面.

答:不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于

下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换

了一个 Java 关键字列表,其中有 goto 和 const,但是这两个是目前无法使用的

关键字,因此有些地方将其称之为保留字,其实保留字这个词应该有更广泛的意

义,因为熟悉 C 语言的程序员都知道,在系统类库中使用过的有特殊意义的单词

或单词的组合都被视为保留字)

Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本

数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本

从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。

Java 为每个原始类型提供了包装类型:

最近还遇到一个面试题,也是和自动装箱和拆箱有点关系的,代码如下所示:

如果不明就里很容易认为两个输出要么都是 true 要么都是 false。首先需要注意的

是 f1、f2、f3、f4 四个变量都是 Integer 对象引用,所以下面的==运算比较的不

是值而是引用。装箱的本质是什么呢?当我们给一个 Integer 对象赋一个 int 值的

简单的说,如果整型字面量的值在-128 到 127 之间,那么不会 new 新的 Integer

对象,而是直接引用常量池中的 Integer 对象,所以上面的面试题中 f1f4 的结果

提醒:越是貌似简单的面试题其中的玄机就越多,需要面试者有相当深厚的功力。

答: &运算符有两种用法:(1)按位与;(2)逻辑与。&&运算符是短路与运算。

逻辑与 跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是

表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。很多时候我

们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是 null 而且不

的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行

运算符(|)和短路或运算符(||)的差别也是如此。

补充:如果你熟悉 JavaScript,那你可能更能感受到短路运算的强大,想成为

JavaScript 的高手就先从玩转短路运算开始吧。

通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的

现场保存都使用 JVM 中的栈空间;而通过 new 关键字和构造器创建的对象则放在

堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收

集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为 Eden、

是各个线程共享的内存区域,用于存储已经被 JVM 加载的类信息、常量、静态变

量、JIT 编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的 100、”

hello”和常量都是放在常量池中,常量池是方法区的一部分,。栈空间操作起来

最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过 JVM

的启动参数来进行调整,栈空间用光了会引发 StackOverflowError,而堆和常量

上面的语句中变量 str 放在栈上,用 new 创建出来的字符串对象放在堆上,而”

hello”这个字面量是放在方法区的。

补充 1:较新版本的 Java(从 Java 6 的某个更新开始)中,由于 JIT 编译器的发

展和”逃逸分析”技术的逐渐成熟,栈上分配、标量替换等优化技术使得对象一

定分配在堆上这件事情已经变得不那么绝对了。

补充 2:运行时常量池相当于 Class 文件常量池具有动态性,Java 语言并不要求

常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,String

类的 intern()方法就是这样的。

看看下面代码的执行结果是什么并且比较一下 Java 7 以前和以后的运行结果是否

入的原理是在参数上加 0.5 然后进行下取整。

expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是

12、用最有效率的方法计算 2 乘以 8?

补充:我们为编写的类重写 hashCode 方法时,可能会看到如下所示的代码,其

实我们不太理解为什么要使用这样的乘法运算来产生哈希码(散列码),而且为

什么这个数是个素数,为什么通常选择 31 这个数?前两个问题的答案你可以自己

百度一下,选择 31 是因为可以用移位和减法运算来代替乘法,从而得到更好的性

位相当于乘以 2 的 5 次方再减去自身就相当于乘以 31,现在的 VM 都能自动完成

中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。

14、在 Java 中,如何跳出当前的多重嵌套循环?

在最外层循环前加一个标记如 A,然后用 break A;可以跳出多重循环。(Java 中

句,但是就像要避免使用 goto 一样,应该避免使用带标签的 break 和 continue,

因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用,所以这种语法

构造器不能被继承,因此不能被重写,但可以被重载。

如果两个对象的 hashCode 相同,它们并不一定相同。当然,你未必要按照要求

去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现

在 Set 集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,

如果哈希码频繁的冲突将会造成存取性能急剧下降)。

补充:继承 String 本身就是一个错误的行为,对 String 类型最好的重用方式是关

联关系(Has-A)和依赖关系(Use-A)而不是继承关系(Is-A)。

18、当一个对象被当作参数传递到一个方法后,此方法可改变

这个对象的属性,并可返回变化后的结果,那么这里到底是值传

是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个

参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调

用过程中被改变,但对对象引用的改变是不会影响到调用者的。C++和 C#中可以

通过传引用或传输出参数来改变传入的参数的值。在 C#中可以编写如下所示的代

码,但是在 Java 中却做不到。

说明:Java 中没有传引用实在是非常的不方便,这一点在 Java 8 中仍然没有得到

改进,正是如此在 Java 编写的代码中才会出现大量的 Wrapper 类(将需要通过

方法调用修改的引用置于一个 Wrapper 类中,再将 Wrapper 对象传入方法),

这样的做法只会让代码变得臃肿,尤其是让从 C 和 C++转型为 Java 程序员的开

们可以储存和操作字符串。其中 String 是只读字符串,也就意味着 String 引用的

法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被

面试题 1 - 什么情况下用+运算符进行字符串连接比调用

面试题 2 - 请说出下面程序的输出。

补充:解答上面的面试题需要清除两点:1. String 对象的 intern 方法会得到字符

串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与 String 对象

的 equals 结果是 true),如果常量池中没有对应的字符串,则该字符串将被添加

到常量池中,然后返回常量池中字符串的引用;2. 字符串的+操作其本质是创建

命令获得 class 文件对应的 JVM 字节码指令就可以看出来。

方法能否根据返回类型进行区分?

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,

而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同

的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写

发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返

回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里

氏代换原则)。重载对返回类型没有特殊的要求。

面试题:华为的面试题中曾经问过这样一个问题 - “为什么不能根据返回类型来

区分重载”,快说出你的答案吧!

21、描述一下 JVM 加载 class 文件的原理机制?

JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的

类加载器是一个重要的 Java 运行时系统组件,它负责在运行时查找和装入类文件

由于 Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一

个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、

连接(验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读

入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应

的 Class 对象。加载完成后,Class 对象还不完整,所以此时的类还不可用。当类

被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设

置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后 JVM 对

类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么

就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。

类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加

载器(Extension)、系统加载器(System)和用户自定义类加载器

取了父亲委托机制(PDM)。PDM 更好的保证了 Java 平台的安全性,在该机制

中,JVM 自带的 Bootstrap 是根加载器,其他的加载器都有且仅有一个父类加载

器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载

器自行加载。JVM 不会向 Java 程序提供对 Bootstrap 的引用。下面是关于几个类

Bootstrap:一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar);

System:又叫应用类加载器,其父类是 Extension。它是应用最广泛的

录中记载类,是用户自定义加载器的默认父加载器。

22、char 型变量中能不能存贮一个中文汉字,为什么?

char 类型可以存储一个中文汉字,因为 Java 中使用的编码是 Unicode(不选择

任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一

个 char 类型占 2 个字节(16 比特),所以放一个中文是没问题的。

补充:使用 Unicode 意味着字符在 JVM 内部和外部有不同的表现形式,在 JVM

内部都是 Unicode,当这个字符被从 JVM 内部转移到外部时(例如存入文件系统

中),需要进行编码转换。所以 Java 中有字节流和字符流,以及在字符流和字节

这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;对于 C 程

序员来说,要完成这样的编码转换恐怕要依赖于 union(联合体/共用体)共享内

抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如

果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实

现,否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中

可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其

中的方法全部都是抽象方法。抽象类中的成员可以是 private、默认、protected、

public 的,而接口中的成员全都是 public 的。抽象类中可以定义成员变量,而接

口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而

抽象类未必要有抽象方法。

实例被实例化。而通常的内部类需要在外部类实例化后才能实例化,其语法看起

来挺诡异的,如下所示。

面试题 - 下面的代码哪些地方会产生编译错误?

注意:Java 中非静态内部类对象的创建要依赖其外部类对象,上面的面试题中 foo

和 main 方法都是静态方法,静态方法中没有 this,也就是说没有所谓的外部类对

象,因此无法创建内部类对象,如果要在静态方法中创建内部类对象,可以这样

25、Java 中会存在内存泄漏吗,请简单描述。

理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被

广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无

用但可达的对象,这些对象不能被 GC 回收,因此也会导致内存泄露的发生。例如

Hibernate 的 Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收

这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)

或清空(flush)一级缓存就可能导致内存泄露。下面例子中的代码也会导致内存

上面的代码实现了一个栈(先进后出(FILO))结构,乍看之下似乎没有什么明

显的问题,它甚至可以通过你编写的各种单元测试。然而其中的 pop 方法却存在

内存泄露的问题,当我们用 pop 方法弹出栈中的对象时,该对象不会被当作垃圾

回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过

期引用(obsolete reference)。在支持垃圾回收的语言中,内存泄露是很隐蔽的,

这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起

来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象,

即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,

从而对性能造成重大影响,极端情况下会引发 Disk Paging(物理内存与硬盘的虚

26、抽象的(abstract)方法是否可同时是静态的(static),

都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛

盾的。本地方法是由本地代码(如 C 代码)实现的方法,而抽象方法是没有实现

的,也是矛盾的。synchronized 和方法的实现细节有关,抽象方法不涉及实现细

节,因此也是相互矛盾的。

27、阐述静态变量和实例变量的区别。

静态变量是被 static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的

任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷

贝;实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。

静态变量可以实现让多个对象共享内存。

补充:在 Java 开发中,上下文类和工具类中通常会有大量的静态成员。

28、是否可以从一个静态(static)方法内部发出对非静态

不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在

调用静态方法时可能对象并没有被初始化。

29、如何实现对象克隆?

2). 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真

正的深度克隆,代码如下。

注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛

型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,

不是在运行时抛出异常,这种是方案明显优于使用 Object 类的 clone 方法克隆对

象。让问题在编译的时候暴露出来总是好过把问题留到运行时。

30、GC 是什么?为什么要有 GC?

GC 是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误

的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动

监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放

已分配内存的显示操作方法。Java 程序员不用担心内存管理,因为垃圾收集器会

自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或

垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通

常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死

亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回

收器对某个对象或所有对象进行垃圾回收。在 Java 诞生初期,垃圾回收是 Java

最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过

境迁,如今 Java 的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常

觉得 iOS 的系统比 Android 系统有更好的用户体验,其中一个深层次的原因就在

于 Android 系统中垃圾回收的不可预知性。

补充:垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量

垃圾回收等方式。标准的 Java 进程既有栈又有堆。栈保存了原始型局部变量,堆

保存了要创建的对象。Java 平台对堆内存回收和再利用的基本算法被称为标记和

清除,但是 Java 对其进行了改进,采用“分代式垃圾收集”。这种方法会跟 Java

对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象

伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说,

这里是它们唯一存在过的区域。

幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。

终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集

(Minor-GC)过程是不会触及这个地方的。当年轻代收集不能把对象放进终身

颐养园时,就会触发一次完全收集(Major-GC),这里可能还会牵扯到压缩,

以便为大对象腾出足够的空间。

与垃圾回收相关的 JVM 参数:

-Xmn — 堆中年轻代的大小

-XX:NewRatio — 可以设置老生代和新生代的比例

年代阀值的初始值和最大值

两个对象,一个是静态区的”xyz”,一个是用 new 创建在堆上的对象。

32、接口是否可继承(extends)接口?抽象类是否可实现

接口可以继承接口,而且支持多重继承。抽象类可以实现(implements)接口,抽

象类可继承具体类也可以继承抽象类。

33、一个”.java”源文件中是否可以包含多个类(不是内部类)?

可以,但一个源文件中最多只能有一个公开类(public class)而且文件名必须和

公开类的类名完全保持一致。

类?是否可以实现接口?

可以继承其他类或实现其他接口,在 Swing 编程和 Android 开发中常用此方式来

35、内部类可以引用它的包含类(外部类)的成员吗?有没有

一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。

(1)修饰类:表示该类不能被继承;

(2)修饰方法:表示方法不能被重写;

(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。

37、指出下面程序的运行结果 .

执行结果:1a2b2b。创建对象时构造器的调用顺序是:先初始化静态成员,然后

调用父类构造器,再初始化非静态成员,最后调用自身构造器。

提示:如果不能给出此题的正确答案,说明之前第 21 题 Java 类加载机制还没有

完全理解,赶紧再看看吧。

38、数据类型之间的转换:

如何将字符串转换为基本数据类型?

如何将基本数据类型转换为字符串?

调用基本数据类型对应的包装类中的方法 parseXXX(String)或

一种方法是将基本数据类型与空字符串(”“)连接(+)即可获得其所

对应的字符串;另一种方法是调用 String 类中的 valueOf()方法返回相应字符

39、如何实现字符串的反转及替换?

的方法。有一道很常见的面试题是用递归实现字符串反转,代码如下所示:

如何取得年月日、小时分钟秒?

如何取得某月的最后一天?

问题 2:以下方法均可获得该毫秒数。

问题 3:代码如下所示。

补充:Java 的时间日期 API 一直以来都是被诟病的东西,为了解决这一问题,Java

Clock、Instant 等类,这些的类的设计都使用了不变模式,因此是线程安全的设

42、打印昨天的当前时刻。

在 Java 8 中,可以用下面的代码实现相同的功能。

Microsystems 公司推出的面向对象的程序设计语言,特别适合于互联网应用程序

能而开发的一种可以嵌入 Web 页面中运行的基于对象和事件驱动的解释性语言。

下面对两种语言间的异同作如下比较:

基于对象和面向对象:Java 是一种真正的面向对象的语言,即使是开发

简单的程序,必须设计对象;JavaScript 是种脚本语言,它可以用来制作与网络

无关的,与用户交互作用的复杂软件。它是一种基于对象(Object-Based)和

事件驱动(Event-Driven)的编程语言,因而它本身提供了非常丰富的内部对

解释和编译:Java 的源代码在执行之前,必须经过编译。JavaScript 是

一种解释性编程语言,其源代码不需经过编译,由浏览器解释执行。(目前的浏

览器几乎都使用了 JIT(即时编译)技术来提升 JavaScript 的运行效率)

强类型变量和类型弱变量:Java 采用强类型变量检查,即所有变量在编

译之前必须作声明;JavaScript 中变量是弱类型的,甚至在使用变量前可以不作

声明,JavaScript 的解释器在运行时检查推断其数据类型。

补充:上面列出的四点是网上流传的所谓的标准答案。其实 Java 和 JavaScript

最重要的区别是一个是静态语言,一个是动态语言。目前的编程语言的发展趋势

是函数式语言和动态语言。在 Java 中类(class)是一等公民,而 JavaScript 中

函数和闭包(closure),当然 Java 8 也开始支持函数式编程,提供了对 Lambda

表达式以及函数式接口的支持。对于这类问题,在面试的时候最好还是用自己的

语言回答会更加靠谱,不要背网上所谓的标准答案。

44、什么时候用断言(assert)?

断言在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。一

般来说,断言用于保证程序最基本、关键的正确性。断言检查通常在开发和测试

时开启。为了保证程序的执行效率,在软件发布后断言检查通常是关闭的。断言

是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为 true;如果表

断言的使用如下面的代 码所示:

Expression2 可以是得出一个值的任意表达式;这个值用于生成显示更多调试信

标记。要在系统类中启用或禁用断言,可使用-esa 或-dsa 标记。还可以在包的基

础上启用或者禁用断言。

注意:断言不应该以任何方式改变程序的状态。简单的说,如果希望在不满足某

些条件时阻止代码的执行,就可以考虑用断言来阻止它。

Error 表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情

况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况;

Exception 表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;

也就是说,它表示如果程序运行正常,从不会发生的情况。

时也可能会遭遇 StackOverflowError,这是一个无法恢复的错误,只能重新修改

代码了,这个面试题的答案是 c。如果写了不能迅速收敛的递归,则很有可能引发

栈溢出的错误,如下所示:

提示:用递归编写程序时一定要牢记两点:1. 递归公式;2. 收敛条件(什么时候

finally{}里的代码会不会被执行,什么时候被执行,在 return

答: 会执行,在方法返回调用者前执行。

注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try

中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完

毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改

后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中

直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升

编译器的语法检查级别来产生警告或错误,Eclipse 中可以在如图所示的地方进行

设置,强烈建议将此项设置为编译错误.

Java 通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了

良好的接口。在 Java 中,每个异常都是一个对象,它是 Throwable 类或其子类

的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,

调用这个对象的方法可以捕获到这个异常并可以对其进行处理。

一般情况 下是用 try 来执行一段程序,如果系统会抛出(throw)一个异常对象,可以通过

它的类型来捕获(catch)它,或通过总是执行代码块(finally)来处理;try 用

来指定一块预防所有异常的程序;catch 子句紧跟在 try 块后面,用来指定你想要

捕获的异常的类型;throw 语句用来明确地抛出一个异常;throws 用来声明一个

方法可能抛出的各种异常(当然声明异常时允许无病呻吟);finally 为确保一段

代码不管发生什么异常状况都要被执行;try 语句可以嵌套,每当遇到一个 try 语

句,异常的结构就会被放入异常栈中,直到所有的 try 语句都完成。如果下一级的

try 语句没有对某种异常进行处理,异常栈就会执行出栈操作,直到遇到有处理这

种异常的 try 语句或者最终将异常抛给 JVM。

48、运行时异常与受检异常有何异同?

异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常

操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就

不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可

能因使用的问题而引发。Java 编译器要求方法必须声明抛出可能发生的受检异常,

但是并不要求必须声明抛出未被捕获的运行时异常。异常和继承一样,是面向对

象程序设计中经常被滥用的东西,在 Effective Java 中对异常的使用给出了以下指

不要将异常处理用于正常的控制流(设计良好的 API 不应该强迫它的调

用者为了正常的控制流而使用异常)

对可以恢复的情况使用受检异常,对编程错误使用运行时异常

避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发

每个方法抛出的异常都要有文档

不要在 catch 中忽略掉捕获到的异常

49、列出一些你常见的运行时异常?

final:修饰符(关键字)有三种用法:如果一个类被声明为 final,意味

着它不能再派生出新的子类,即不能被继承,因此它和 abstract 是反义词。将

变量声明为 final,可以保证它们在使用中不被改变,被声明为 final 的变量必须

在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为 final 的方

法也同样只能使用,不能在子类中被重写。

finally:通常放在 try…catch…的后面构造总是执行代码块,这就意味着

程序无论正常执行还是发生异常,这里的代码只要 JVM 不关闭都能执行,可以

将释放外部资源的代码写在 finally 块中。

圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收

集器在销毁对象时调用的,通过重写 finalize()方法可以整理系统资源或者执行

由于篇幅字数有限,后面会持续更新滴........

上面的这些面试题都整理成了PDF文档,希望能帮助到你面试前的复习并找到一个好的工作,相对来说也节省了你在网上搜索资料的时间来学习!!!

附欢迎关注我的公种号:it资源之家 ,扫描下面二维码即可领取更多一线大厂Java面试题资料!

欢迎大家评论区一起交流,相互提升;整理资料不易,如果喜欢文章记得点个赞哈,感谢大家支持!!!

内容来源于网络如有侵权请私信删除

C++是基于C语言扩展发展而来的面向对象的程序设计语言,本文将主要讨论C++语言基于C语言扩展的方面。

C语言中变量的定义必须在作用域开始的位置进行定义。

MinGW编译器已经对func函数进行了内联。 ### 4、内联函数使用的限制 C++中使用inline关键字内联编译函数的限制: A、不能存在任何形式的循环语句 B、不能存在过多的条件判断语句 C、函数体不能过于庞大 D、不能对函数进行取地址操作 E、函数内联声明必须在调用语句前 对于现代C++编译器的扩展语法提供的强制内联不受上述条件限制。 ## 十一、类型强制转换 C语言中的类型转换是强制转换,任何类型间都可以转换,过于粗暴。 ### 2、静态类型转换 静态类型转换是在编译期内即可决定其类型的转换。 静态类型转换的使用场合: A、用于基本类型间的转换 B、不能用于基本类型指针间的转换 C、用于有继承关系类对象间的转换和类指针间的转换(转换一般从子对象向父对象转换)
### 3、动态类型转换
动态类型转换的使用场合:
A、用于有继承关系的类指针间的转换
B、有交叉关系的类指针间的转换
用于有直接或间接继承关系的指针(引用)的强制转换
转换指针成功将会得到目标类型的指针,转换失败将得到一个空指针;
转换引用成功将得到目标类型的引用,转换失败将得到一个异常操作信息。
### 4、常量类型转换
常量类型转换的使用场合:
A、用于去除变量的只读属性
B、目标类类型只能是指针或引用
### 5、重解释类型转换
重解释类型转换使用场合:
A、用于指针类型间的强制转换
B、用于整数和指针类型间的强制转换
为数据的二进制形式重新解释,但是不改变其值。
### 1、命令空间简介 C语言中,只有一个全局作用域,所有的全局标识符共享一个作用域,因此标识符之间可能存在冲突。 C++语言中,提出了命名空间的概念。命名空间将全局作用域分为不同的部分,不同命令空间中的标识符可以重名而不会发生冲突,命名空间可以嵌套。全局作用域即默认命名空间。 global scope是一个程序中最大的scope,是引起命名冲突的根源。C语言没有从语言层面提供命名空间机制来解决。global scope是无名的命名空间。 ### 3、命名空间的划分 NameSpace是对全局区域的再次划分。
### 4、命名空间的使用方法
### 5、命名空间使用示例
可以使用块语句将命名空间限定在块语句内部。
### 6、命名空间嵌套
### 7、使用命名空间进行协作开发
在实际项目开发中,可以将一个类或者具有相同属性的多个类声明在一个命名空间内,在使用时只需要声明命名空间即可。

除了使用字符数组来处理字符串以外,C++引入了字符串类型。可以定义字符串变量。 ### 1、定义和初始化 string数组是高效的,如果用二维数组来存入字符串数组的话,则容易浪费空间,此时列数是由最长的字符串决定。如果用二级指针申请堆空间,依据大小申请相应的空间,虽然解决了内存浪费的问题,但是操作麻烦。用 string 数组存储,字符串数组的话,效率即高又灵活。

c);//把字符串当前大小置为len,并用字符c填充不足的部分

4.23 说说你对读写锁的了解
4.28 LongAdder解决了什么问题,它是如何实现的?
4.31 介绍一下线程池
4.32 介绍一下线程池的工作流程
4.33 线程池都有哪些状态?
4.34 谈谈线程池的拒绝策略
4.35 线程池的队列大小你通常怎么设置?
4.36 线程池有哪些参数,各个参数的作用是什么?

4.31 介绍一下线程池

系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。

与库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个空闲的线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()方法。

从Java 5开始,Java内建支持线程池。Java 5新增了一个Executors工厂类来产生线程池,该工厂类包含如下几个静态工厂方法来创建线程池。创建出来的线程池,都是通过ThreadPoolExecutor类来实现的。

  • newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
  • newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。
  • ExecutorService newWorkStealingPool():该方法是前一个方法的简化版本。如果当前机器有4个CPU,则目标并行级别被设置为4,也就是相当于为前一个方法传入4作为参数。

4.32 介绍一下线程池的工作流程

线程池的工作流程如下图所示:

  1. 判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。
  2. 判断任务队列是否已满,没满则将新提交的任务添加在工作队列。
  3. 判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和(拒绝)策略。

4.33 线程池都有哪些状态?

线程池一共有五种状态, 分别是:

  1. RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务。
  2. SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。
  3. STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态。
    • 线程池不是RUNNING状态;

下图为线程池的状态转换过程:

4.34 谈谈线程池的拒绝策略

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

  1. DiscardPolicy:也是丢弃任务,但是不抛出异常。
  2. DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)。

4.35 线程池的队列大小你通常怎么设置?

  1. 尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

  2. 可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

  3. 可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。


4.36 线程池有哪些参数,各个参数的作用是什么?

线程池主要有如下6个参数:

  1. corePoolSize(核心工作线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时。
  2. maximumPoolSize(最大线程数):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
  3. AliveTime(多余线程存活时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
  4. workQueue(队列):用于传输和保存等待执行任务的阻塞队列。
  5. handler(拒绝策略):当线程池和队列都满了,再加入线程会执行此策略。

Interface(本地库接口),下图可以大致描述 JVM 的结构。

JVM 是执行 Java 程序的虚拟计算机系统,那我们来看看执行过程:首先需要准备好编译好的 Java 字节码文件(即class文件),计算机要运行程序需要先通过一定方式(类加载器)将 class 文件加载到内存中(运行时区),但是字节码文件是JVM定义的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解释器(执行引擎)将字节码翻译成特定的操作系统指令集交给 CPU 去执行,这个过程中会需要调用到一些不同语言为 Java 提供的接口(例如驱动、地图制作等),这就用到了本地 Native 接口(本地库接口)。

  • Stack(本地方法栈)。几乎所有的关于 Java 内存方面的问题,都是集中在这块。
  • 指令集翻译为操作系统指令集。
  • lib。原本多用于一些专业领域,如JAVA驱动,地图制作引擎等,现在关于这种本地方法接口的调用已经被类似于Socket通信,WebService等方式取代。

JVM的启动过程分为如下四个步骤:

  1. JVM的装入环境和配置

    java.exe负责查找JRE,并且它会按照如下的顺序来选择JRE:

  2. 查注册中注册的JRE。
  3. 函数指针变量上。JVM的装载工作完成。

  4. 初始化JVM,获得本地调用接口


5.3 Java程序是怎么运行的?

概括来说,写好的 Java 源代码文件经过 Java 编译器编译成字节码文件后,通过类加载器加载到内存中,才能被实例化,然后到 Java 虚拟机中解释执行,最后通过操作系统操作 CPU 执行获取结果。如下图:

5.4 本地方法栈有什么用?

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

5.5 没有程序计数器会怎么样?

没有程序计数器,Java程序中的流程控制将无法得到正确的控制,多线程也无法正确的轮换。

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

5.6 说一说Java的内存分布情况

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时区域。

  1. 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

    由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

    如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

  2. 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧[插图](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

    在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

  3. 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

    《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

  4. Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”,而这里笔者写的“几乎”是指从实现角度来看,随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。

    根据《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。

    Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

  5. 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。

    根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

  6. 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

    既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

  7. 直接内存(Direct Memory)并不是虚拟机运行时区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

    显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。


5.7 类存放在哪里?

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来

5.8 局部变量存放在哪里?

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

存放了编译期可知的各种Java虚拟机基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

5.9 介绍一下Java代码的编译过程

从Javac代码的总体结构来看,编译过程大致可以分为1个准备过程和3个处理过程,它们分别如下所示。

  1. 准备过程:初始化插入式注解处理器。

  2. 解析与填充符号表过程,包括:

    • 词法、语法分析,将源代码的字符流转变为标记集合,构造出抽象语法树。
    • 填充符号表,产生符号地址和符号信息。
  3. 插入式注解处理器的注解处理过程:

  4. 分析与字节码生成过程,包括:

    • 标注检查,对语法的静态信息进行检查。
    • 流及控制流分析,对程序动态运行过程进行检查。
    • 解语法糖,将简化代码编写的语法糖还原为原有的形式。
    • 字节码生成,将前面各个步骤所生成的信息转化成字节码。

上述3个处理过程里,执行插入式注解时又可能会产生新的符号,如果有新的符号产生,就必须转回到之前的解析、填充符号表的过程中重新处理这些新符号,从总体来看,三者之间的关系与交互顺序如图所示。

5.10 介绍一下类加载的过程

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。这七个阶段的发生顺序如下图所示。

在上述七个阶段中,包括了类加载的全过程,即加载、验证、准备、解析和初始化这五个阶段。

“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,在加载阶段,Java虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种的访问入口。

加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了,方法区中的存储格式完全由虚拟机实现自行定义,《Java虚拟机规范》未规定此区域的具体结构。类型妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的类型的外部接口。

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元验证、字节码验证和符号引用验证。

  1. 第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

  2. 第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求。

  3. 第三阶段是通过流分析和控制流分析,确定程序语义是合法的、符合逻辑的。

  4. 符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的。而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了。

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用在Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现,那解析阶段中所说的直接引用与符号引用又有什么关联呢?

References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。

直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。

进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器<clinit>()方法的过程。<clinit>()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。

5.11 介绍一下对象的实例化过程

对象实例化过程,就是执行类构造函数对应在字节码文件中的<init>()方法(实例构造器),<init>()方法由非静态变量、非静态代码块以及对应的构造器组成。

  • <init>()方法中的代码执行顺序为:父类变量初始化、父类代码块、父类构造器、子类变量初始化、子类代码块、子类构造器。

静态变量、静态代码块、普通变量、普通代码块、构造器的执行顺序如下图:

具有父类的子类的实例化顺序如下:

Java是一门面向对象的编程语言,Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象通常(例外:复制、反序列化)仅仅是一个new关键字而已,而在虚拟机中,对象(文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样一个过程呢?

当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer)。但如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配是指针碰撞,既简单又高效;而当使用CMS这种基于清除(Sweep)的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。

除如何划分可用空间之外,还有另外一个需要考虑的问题:对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的类型所对应的零值。

接下来,Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始——构造函数,即Class文件中的<init>()方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说(由字节码流中new指令后面是否跟随invokespecial指令所决定,Java编译器会在遇到new关键字的地方同时生成这两条字节码指令,但如果直接通过其他方式产生的则不一定如此),new指令之后会接着执行<init>()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

5.12 元空间在栈内还是栈外?

在栈外,元空间占用的是本地内存。

许多Java程序员都习惯在HotSpot虚拟机上开发、部署程序,很多人都更愿意把方法区称呼为“永久代“,或将两者混为一谈。本质上这两者并不是等价的,因为仅仅是当时的HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。但是对于其他虚拟机实现,譬如BEAJRockit、 J9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。

现在回头来看,当年使用永久代来实现方法区的决定并不是一个好主意,这种设计导致了Java应用更容易遇到内存溢出的问题(永久代有-XX:MaxPermSize的上限,即使不设置也有默认大小,而J9和JRockit只要没有触碰到进程可用内存的上限,例如32位系统中的4GB限制,就不会出问题),而且有极少数方法(例如String::intern())会因永久代的原因而导致不同虚拟机下有不同的表现。

当Oracle收购BEA获得了JRockit的所有权后,准备把JRockit中的优秀功能,譬如Java Mission Control管理工具,移植到HotSpot虚拟机时,但因为两者对方法区实现的差异而面临诸多困难。考虑到HotSpot未来的发展,在JDK 6的时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了,到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。

5.13 谈谈JVM的类加载器,以及双亲委派模型

Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

自JDK1.2以来,Java一直保持着三层类加载器、双亲委派的类加载架构。对于这个时期的Java应用,绝大多数Java程序都会使用到以下3个系统提供的类加载器来进行加载。

    Loader):这个类加载器负责加载存放在\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可。 Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。根据“扩展类加载器”这个名称,就可以推断出这是一种Java系统类库的扩展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,在JDK 9之后,这种扩展机制被模块化带来的天然的扩展能力所取代。由于扩展类加载器是由Java代码实现的,开发者可以直接在程序中使用扩展类加载器来加载Class文件。 Loader):这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

这些类加载器之间的协作关系“通常”会如下图所示,图中展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

双亲委派模型对于保证Java程序的稳定运作极为重要,但它的实现却异常简单,用以实现双亲委派的代码只有短短十余行,全部集中在java.lang.ClassLoader的loadClass()方法之中。

这段代码的逻辑清晰易懂:先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败,抛出ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。

5.14 双亲委派机制会被破坏吗?

双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委派模型主要出现过3次较大规模“被破坏”的情况。

双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2面世以前的“远古”时代。由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。双亲委派的具体逻辑就实现在这里面,按照loadClass()方法的逻辑,如果父类加载失败,会自动调用自己的findClass()方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?

这并非是不可能出现的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?

为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。Java中涉及SPI的加载基本上都采用这种方式来完成,例如JNDI、JDBC、JCE、JAXB和JBI等。不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的实现方式,在JDK

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(HotSwap)、模块热部署(Hot Deployment)等。说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。对于个人电脑来说,重启一次其实没有什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发者,尤其是大型系统或企业级软件开发者具有很大的吸引力。

早在2008年,在Java社区关于模块化规范的第一场战役里,由Sun/Oracle公司所提出的JSR-294、JSR-277规范提案就曾败给以公司主导的JSR-291(即OSGi R4.2)提案。尽管Sun/Oracle并不甘心就此失去Java模块化的主导权,随即又再拿出Jigsaw项目迎战,但此时OSGi已经站稳脚跟,成为业界“事实上”的Java模块化标准。曾经在很长一段时间内,凭借着OSGi广泛应用基础让Jigsaw吃尽苦头,其影响一直持续到Jigsaw随JDK 9面世才算告一段落。而且即使Jigsaw现在已经是Java的标准功能了,它仍需小心翼翼地避开OSGi运行期动态热部署上的优势,仅局限于静态地解决模块间封装隔离和访问控制的问题,现在我们先来简单看一看OSGi是如何通过类加载器实现热部署的。

OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:

  1. 将以java.*开头的类,委派给父类加载器加载。
  2. 否则,将委派列表名单内的类,委派给父类加载器加载。
  3. 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
  4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。

上面的查找顺序中只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行的。

5.15 介绍一下Java的垃圾回收机制

在Java内存运行时区域的各个部分中,堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理,我们平时所说的内存分配与回收也仅仅特指这一部分内存。

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

但是,在Java领域,至少主流的Java虚拟机里面都没有选用引用计数来管理内存,主要原因是,这个看似简单的有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。

举个简单的例子:对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数也就无法回收它们。

当前主流的商用程序语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

如下图所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。回收废弃常量与回收Java堆中的对象非常类似。举个常量池中字面量回收的例子,假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。

判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(GenerationalCollection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。

最早出现也是最基础的垃圾收集是“标记-清除”(Mark-Sweep),如它的名字一样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

它的主要缺点有两个:第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。标记-清除的执行过程如下图所示。

为了解决标记-清除面对大量可回收对象时执行效率低的问题,1969年Fenichel提出了一种称为“半区复制”(Semispace Copying)的垃圾收集,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。标记-复制的执行过程如下图所示。

Appel针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略,现在称为“Appel式回收”。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Surr空间,每次分配内存只使用Eden和其中一块Surr。发生垃圾搜集时,将Eden和Surr中仍然存活的对象一次性复制到另外一块Surr空间上,然后直接清理掉Eden和已用过的那块Surr空间。HotSpot虚拟机默认Eden和Surr的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Surr的10%),只有一个Surr空间,即10%的新生代是会被“浪费”的。当然,98%的对象可被回收仅仅是“普通场景”下测得的,任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Surr空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。

标记-复制在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种。

针对老年代对象的存亡特征,1974年Edward Lueders提出了另外一种有针对性的“标记-整理”(Mark-Compact),其中的标记过程仍然与“标记-清除”一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存,“标记-整理”的示意图如下图所示。

5.16 请介绍一下分代回收机制

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(GenerationalCollection)[插图]的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。把分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

分代收集并非只是简单划分一下内存区域那么容易,它至少存在一个明显的困难:对象不是孤立的,对象之间会存在跨代引用。假如要现在进行一次只局限于新生代区域内的收集,但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。遍历整个老年代所有对象的方案虽然理论上可行,但无疑会为内存回收带来很大的性能负担。为了解决这个问题,就需要对分代收集理论添加第三条经验法则:

依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的结构(称为“记忆集”,RememberedSet),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

5.17 JVM中一次完整的GC流程是怎样的?

新创建的对象一般会被分配在新生代中,常用的新生代的垃圾回收器是 ParNew 垃圾回收器,它按照 8:1:1 将新生代分成 Eden 区,以及两个 Surr 区。某一时刻,我们创建的对象将 Eden 区全部挤满,这个对象就是挤满新生代的最后一个对象。此时,Minor GC 就触发了。

在正式 Minor GC 前,JVM 会先检查新生代中对象,是比老年代中剩余空间大还是小。为什么要做这样的检查呢?原因很简单,假如 Minor GC 之后 Surr 区放不下剩余对象,这些对象就要进入到老年代,所以要提前检查老年代是不是够用。这样就有两种情况:

  1. 老年代剩余空间大于新生代中的对象大小,那就直接Minor GC,GC完surr不够放,老年代也绝对够放;

  2. 老年代剩余空间小于新生代中的对象大小,这个时候就要查看是否启用了“老年代空间分配担保规则”,具体来说就是看 -XX:-HandlePromotionFailure 参数是否设置了。

    老年代空间分配担保规则是这样的,如果老年代中剩余空间大小,大于历次 Minor GC 之后剩余对象的大小,那就允许进行 Minor GC。因为从概率上来说,以前的放的下,这次的也应该放的下。那就有两种情况:

    老年代中剩余空间大小,大于历次Minor GC之后剩余对象的大小,进行 Minor GC;

    老年代中剩余空间大小,小于历次Minor GC之后剩余对象的大小,进行Full GC,把老年代空出来再检查。

开启老年代空间分配担保规则只能说是大概率上来说,Minor GC 剩余后的对象够放到老年代,所以当然也会有万一,Minor GC 后会有这样三种情况:

  1. Minor GC 之后的对象足够放到 Surr 区,皆大欢喜,GC 结束;
  2. Minor GC 之后的对象不够放到 Surr 区,接着进入到老年代,老年代能放下,那也可以,GC 结束;
  3. Minor GC 之后的对象不够放到 Surr 区,老年代也放不下,那就只能 Full GC。

前面都是成功 GC 的例子,还有 3 中情况,会导致 GC 失败,报 OOM:

  1. 紧接上一节 Full GC 之后,老年代任然放不下剩余对象,就只能 OOM;
  2. 未开启老年代分配担保机制,且一次 Full GC 后,老年代任然放不下剩余对象,也只能 OOM;
  3. 开启老年代分配担保机制,但是担保不通过,一次 Full GC 后,老年代任然放不下剩余对象,也是能 OOM。

GC完整流程,参考下图:

当 Eden 区的空间耗尽时 Java 虚拟机便会触发一次 Minor GC 来收集新生代的垃圾,存活下来的对象,则会被送到 Surr 区,简单说就是当新生代的Eden区满的时候触发 Minor GC。

serial GC 中,老年代内存剩余已经小于之前年轻代晋升老年代的平均大小,则进行 Full GC。而在 CMS 等并发收集器中则是每隔一段时间检查一下老年代内存的使用量,超过一定比例时进行 Full GC 回收。

可以采用以下措施来减少Full GC的次数:

  1. 使用标记-整理,尽量保持较大的连续内存空间;
  2. 排查代码中无用的大对象。

5.20 如何确定对象是可回收的?

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

但是,在Java领域,至少主流的Java虚拟机里面都没有选用引用计数来管理内存,主要原因是,这个看似简单的有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。

举个简单的例子:对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数也就无法回收它们。

当前主流的商用程序语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

如下图所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

5.21 对象如何晋升到老年代?

虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次MinorGC后仍然存活,并且能被Surr容纳的话,该对象会被移动到Surr空间中,并且将其对象年龄设为1岁。对象在Surr区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

5.22 为什么老年代不能使用标记复制?

因为老年代保留的对象都是难以消亡的,而标记复制在对象存活率较高时就要进行较多的复制操作,效率将会降低,所以在老年代一般不能直接选用这种。

5.23 新生代为什么要分为Eden和Surr,它们的比例是多少?

现在的商用Java虚拟机大多都优先采用了“标记-复制”去回收新生代,该早期采用“半区复制”的机制进行垃圾回收。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。

实际上,新生代中的对象有98%熬不过第一轮收集,因此并不需要按照1∶1的比例来划分新生代的内存空间。在1989年,Andrew Appel提出了一种更优化的半区复制分代策略,现在称为“Appel式回收”。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Surr空间,每次分配内存只使用Eden和其中一块Surr。发生垃圾搜集时,将Eden和Surr中仍然存活的对象一次性复制到另外一块Surr空间上,然后直接清理掉Eden和已用过的那块Surr空间。

HotSpot虚拟机默认Eden和Surr的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Surr的10%),只有一个Surr空间,即10%的新生代是会被“浪费”的。

5.24 为什么要设置两个Surr区域?

设置两个 Surr 区最大的好处就是解决内存碎片化。

我们先假设一下,Surr 只有一个区域会怎样。Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Surr 区,而之前 Surr 区中的对象,可能也有一些是需要被清除的。问题来了,这时候我们怎么清除它们?在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为 Surr 有 2 个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。

这种机制最大的好处就是,整个过程中,永远有一个 Surr space 是空的,另一个非空的 Surr space 是无碎片的。那么,Surr 为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果 Surr 区再细分下去,每一块的空间就会比较小,容易导致 Surr 区满,两块 Surr 区可能是经过权衡之后的最佳方案。

5.25 说一说你对GC的了解。

最早出现也是最基础的垃圾收集是“标记-清除”(Mark-Sweep),如它的名字一样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

它的主要缺点有两个:第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。标记-清除的执行过程如下图所示。

为了解决标记-清除面对大量可回收对象时执行效率低的问题,1969年Fenichel提出了一种称为“半区复制”(Semispace Copying)的垃圾收集,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。标记-复制的执行过程如下图所示。

Appel针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略,现在称为“Appel式回收”。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Surr空间,每次分配内存只使用Eden和其中一块Surr。发生垃圾搜集时,将Eden和Surr中仍然存活的对象一次性复制到另外一块Surr空间上,然后直接清理掉Eden和已用过的那块Surr空间。HotSpot虚拟机默认Eden和Surr的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Surr的10%),只有一个Surr空间,即10%的新生代是会被“浪费”的。当然,98%的对象可被回收仅仅是“普通场景”下测得的,任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Surr空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。

标记-复制在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种。

针对老年代对象的存亡特征,1974年Edward Lueders提出了另外一种有针对性的“标记-整理”(Mark-Compact),其中的标记过程仍然与“标记-清除”一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存,“标记-整理”的示意图如下图所示。

5.26 为什么新生代和老年代要采用不同的回收?

如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间。如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。

5.27 请介绍G1垃圾收集器

Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。G1收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。

虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Surr空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数 -XX:G1HeapRegionSize 设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous

虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。从名字上就可以看出CMS收集器是基于标记-清除实现的,它的运作过程分为四个步骤,包括:

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的阶段。

CMS收集器还远达不到完程度,它至少有以下三个明显的缺点:

首先,CMS收集器对处理器资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量。

还有最后一个缺点,CMS是一款基于“标记-清除”实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。

5.29 内存泄漏和内存溢出有什么区别?

内存泄漏(memory leak):内存泄漏指程序运行过程中分配内存给临时变量,用完之后却没有被GC回收,始终占用着内存,既不能被使用也不能分配给其他程序,于是就发生了内存泄漏。

内存溢出(out of memory):简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出。

5.30 什么是内存泄漏,怎么解决?

内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象已经不再需要,但由于长生命周期对象持有它的引用而导致不能被回收。以发生的方式来分类,内存泄漏可以分为4类:

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以环境和方法对检测内存泄漏至关重要。
  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于上的缺陷,导致总会有一块仅且一块内存发生泄漏。
  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

避免内存泄漏的几点建议:

  1. 尽早释放无用对象的引用。
  2. 避免在循环中创建对象。
  3. 尽量少使用静态变量,因为静态变量存放在永久代,基本不参与垃圾回收。

5.31 什么是内存溢出,怎么解决?

内存溢出(out of memory):简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出。

引起内存溢出的原因有很多种,常见的有以下几种:

  1. 内存中加载的量过于庞大,如一次从库取出过多;
  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  3. 代码中存在死循环或循环产生过多重复的对象实体;
  4. 使用的第三方软件中的BUG;
  5. 启动参数内存值设定的过小。
  • 第一步,修改JVM启动参数,直接增加内存。
  • 第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
  • 第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
  • 第四步,使用内存查看工具动态查看内存使用情况。

除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OOM异常的可能。

  1. Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。

  2. 虚拟机栈和本地方法栈溢出

    HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。

  3. 方法区和运行时常量池溢出

    方法区溢出也是一种常见的内存溢出异常,在经常运行时生成大量动态类的应用场景里,就应该特别关注这些类的回收状况。这类场景常见的包括:程序使用了CGLib字节码增强和动态语言、大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

    在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,即常量池是方法去的一部分,所以上述问题在常量池中也同样会出现。而HotSpot从JDK 7开始逐步“去永久代”的计划,并在JDK 8中完全使用元空间来代替永久代,所以上述问题在JDK 8中会得到避免。

  4. 直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致。如果直接通过反射获取Unsafe实例进行内存分配,并超出了上述的限制时,将会引发OOM异常。


我要回帖

更多关于 打开3d显示无效的文件 的文章

 

随机推荐