1、Java 中会存在内存泄漏吗,请简单描述。
答:
理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被
广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无
用但可达的对象,这些对象不能被 GC 回收,因此也会导致内存泄露的发生。例如
Hibernate 的 Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收
这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)
或清空(flush)一级缓存就可能导致内存泄露。
下面例子中的代码也会导致内存泄露。
import java.util.Arrays;
import java.util.EmptyStackException;
public class MyStack<T> {
private T[] elements;
private int size = 0;
private static final int INIT_CAPACITY = 16;
public MyStack() {
elements = (T[]) new Object[INIT_CAPACITY];
}
public void push(T elem) {
ensureCapacity();
elements[size++] = elem;
}
public T pop() {
if(size == 0)
throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
上面的代码实现了一个栈(先进后出(FILO))结构,乍看之下似乎没有什么明
显的问题,它甚至可以通过你编写的各种单元测试。然而其中的 pop 方法却存在
内存泄露的问题,当我们用 pop 方法弹出栈中的对象时,该对象不会被当作垃圾
回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过
期引用(obsolete reference)。在支持垃圾回收的语言中,内存泄露是很隐蔽的,
这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起
来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象,
即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,
从而对性能造成重大影响,极端情况下会引发 Disk Paging(物理内存与硬盘的虚
拟内存交换数据),甚至造成 OutOfMemoryError。
2、抽象的(abstract)方法是否可同时是静态(static), 是否可同时是本地方法(native),是否可同时被synchronized 修饰?
答:
都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛
盾的。本地方法是由本地代码(如 C 代码)实现的方法,而抽象方法是没有实现
的,也是矛盾的。synchronized 和方法的实现细节有关,抽象方法不涉及实现细
节,因此也是相互矛盾的。
3、阐述静态变量和实例变量的区别。
静态变量是被 static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的
任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷
贝;实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。
静态变量可以实现让多个对象共享内存。
4、是否可以从一个静态(static)方法内部发出对非静态 (non-static)方法的调用?
答:
不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在
调用静态方法时可能对象并没有被初始化。
5、如何实现对象克隆?
答:
有两种方式:
1). 实现 Cloneable 接口并重写 Object 类中的 clone()方法;
2). 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下。
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class MyUtil {
private MyUtil() {
throw new AssertionError();
}
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj) throws
Exception {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bout);
oos.writeObject(obj);
ByteArrayInputStream bin = new
ByteArrayInputStream(bout.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bin);
return (T) ois.readObject();
// 说明:调用 ByteArrayInputStream 或 ByteArrayOutputStream
对象的 close 方法没有任何意义
// 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这
一点不同于对外部资源(如文件流)的释放
}
}
测试方法
import java.io.Serializable;
/**
* 人类
* @author 骆昊
*
*/
class Person implements Serializable {
private static final long serialVersionUID = -9102017020286042305L;
private String name; // 姓名
private int age; // 年龄
private Car car; // 座驾
public Person(String name, int age, Car car) {
this.name = name;
this.age = age;
this.car = car;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Car getCar() {
return car;
}
public void setCar(Car car) {
this.car = car;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", car=" +
car + "]";
}
}
/**
* 小汽车类
* @author 骆昊
*
*/
class Car implements Serializable {
private static final long serialVersionUID = -5713945027627603702L;
private String brand; // 品牌
private int maxSpeed; // 最高时速
public Car(String brand, int maxSpeed) {
this.brand = brand;
this.maxSpeed = maxSpeed;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public int getMaxSpeed() {
return maxSpeed;
}
public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}
@Override
public String toString() {
return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed +
"]";
}
}
class CloneTest {
public static void main(String[] args) {
try {
Person p1 = new Person("Hao LUO", 33, new Car("Benz",
300));
Person p2 = MyUtil.clone(p1); // 深度克隆
p2.getCar().setBrand("BYD");
// 修改克隆的 Person 对象 p2 关联的汽车对象的品牌属性
// 原来的 Person 对象 p1 关联的汽车不会受到任何影响
// 因为在克隆 Person 对象时其关联的汽车对象也被克隆了
System.out.println(p1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛
型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,
不是在运行时抛出异常,这种是方案明显优于使用 Object 类的 clone 方法克隆对
象。让问题在编译的时候暴露出来总是好过把问题留到运行时。
6、GC 是什么?为什么要有 GC?
答:
GC 是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误
的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动
监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放
已分配内存的显示操作方法。Java 程序员不用担心内存管理,因为垃圾收集器会
自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或
Runtime.getRuntime().gc() ,但 JVM 可以屏蔽掉显示的垃圾回收调用。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通
常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死
亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回
收器对某个对象或所有对象进行垃圾回收。在 Java 诞生初期,垃圾回收是 Java
最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过
境迁,如今 Java 的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常
觉得 iOS 的系统比 Android 系统有更好的用户体验,其中一个深层次的原因就在
于 Android 系统中垃圾回收的不可预知性。
补充:垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量
垃圾回收等方式。标准的 Java 进程既有栈又有堆。栈保存了原始型局部变量,堆
保存了要创建的对象。Java 平台对堆内存回收和再利用的基本算法被称为标记和
清除,但是 Java 对其进行了改进,采用“分代式垃圾收集”。这种方法会跟 Java
对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象
移动到不同区域:
伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说,
这里是它们唯一存在过的区域。
幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。
终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集
(Minor-GC)过程是不会触及这个地方的。当年轻代收集不能把对象放进终身
颐养园时,就会触发一次完全收集(Major-GC),这里可能还会牵扯到压缩,
以便为大对象腾出足够的空间。
与垃圾回收相关的 JVM 参数:
-Xms / -Xmx — 堆的初始大小 / 堆的最大大小
-Xmn — 堆中年轻代的大小
-XX:-DisableExplicitGC — 让 System.gc()不产生任何作用
-XX:+PrintGCDetails — 打印 GC 的细节第 244 页 共 485 页
-XX:+PrintGCDateStamps — 打印 GC 操作的时间戳
-XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
-XX:NewRatio — 可以设置老生代和新生代的比例
-XX:PrintTenuringDistribution — 设置每次新生代 GC 后输出幸存者
乐园中对象年龄的分布
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老
年代阀值的初始值和最大值
-XX:TargetSurvivorRatio:设置幸存区的目标使用率
7、Anonymous Inner Class(匿名内部类)是否可以继承它 类?是否可以实现接口?
答:
可以继承其他类或实现其他接口,在 Swing 编程和 Android 开发中常用此方式来
实现事件监听和回调。
8、内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制?
答:
一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。
9、Java 中的 final 关键字有哪些用法?
(1)修饰类:表示该类不能被继承;(2)修饰方法:表示方法不能被重写;(3)修饰变
量:表示变量只能一次赋值以后值不能被修改(常量)。
10、日期和时间:
如何取得年月日、小时分钟秒?
如何取得从 1970 年 1 月 1 日 0 时 0 分 0 秒到现在的毫秒数?
如何取得某月的最后一天?
如何格式化日期?
答:
问题 1:创建 java.util.Calendar 实例,调用其 get()方法传入不同的参数即可获
得参数所对应的值。Java 8 中可以使用 java.time.LocalDateTimel 来获取,代码
如下所示。
public class DateTimeTest {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
System.out.println(cal.get(Calendar.YEAR));
System.out.println(cal.get(Calendar.MONTH)); // 0 - 11
System.out.println(cal.get(Calendar.DATE));
System.out.println(cal.get(Calendar.HOUR_OF_DAY));
System.out.println(cal.get(Calendar.MINUTE));
System.out.println(cal.get(Calendar.SECOND));
// Java 8
LocalDateTime dt = LocalDateTime.now();
System.out.println(dt.getYear());
System.out.println(dt.getMonthValue()); // 1 - 12
System.out.println(dt.getDayOfMonth());
System.out.println(dt.getHour());
System.out.println(dt.getMinute());
System.out.println(dt.getSecond());
}
}
Calendar.getInstance().getTimeInMillis();
System.currentTimeMillis();
Clock.systemDefaultZone().millis(); // Java 8
Calendar time = Calendar.getInstance();
time.getActualMaximum(Calendar.DAY_OF_MONTH);
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;
class DateFormatTest {
public static void main(String[] args) {
SimpleDateFormat oldFormatter = new
SimpleDateFormat("yyyy/MM/dd");
Date date1 = new Date();
System.out.println(oldFormatter.format(date1));
// Java 8
DateTimeFormatter newFormatter =
DateTimeFormatter.ofPattern("yyyy/MM/dd");
LocalDate date2 = LocalDate.now();
System.out.println(date2.format(newFormatter));
}
}