0、Java基础考点
1、谈谈你对Java的理解
- 平台无关性(一次编译,到处运行)
- GC(垃圾清理)
- 语言特性(泛型、反射)
- 面向对象(封装、继承、多态)
- 类库
- 异常处理
2、Java是如何实现平台无关性的(一处编译,到处运行)
- 编译时(语法和语义进行检测)
- 运行时
javac将源码编译为.class字节码文件。
javap java自带的反编译工具
javac test.java
javap -c test
Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。
为什么JVM不直接将源码解析成机器码去执行?
- 准备工作:每次执行都需要各种检查。(编译时先进行语法和语义检查)
- 兼容性:也可以将别的语言解析成字节码。(别的语言也可以编译为.class字节码文件)
- 平台无关性:可以实现平台无关性。一次编译,到处执行。
3、JVM如何加载 .class文件
Java虚拟机
JVM屏蔽底层操作系统的不同,减少基于原生语言开发的复杂性。
- 内存结构模型
- GC
- Class loader:依据特定格式,加载dass文件到內存。
- Execution Engine:对命令进行解析。
- Native Interface:融合不同开发语言的原生库为Java所用。
- RuntimeDataArea:JM内存空间结构模型。
JVM主要有Class Loader、RuntimData Area 、Execution Engine、Native Interface四部分组成,主要通过Class Loader将符合格式要求的文件,加载到内存中。并通过Execution Engine解析内部字节码,提交给操作系统执行。
4、谈谈反射
- JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;
- 对于任意一个对象,都能够调用它的任意方法和属性;
- 这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
待调用方法:
public class Robot {
private String name;
// 公共的方法
public void sayHi(String hello) {
System.out.println(hello + ":" + name);
}
// 私有化的方法
private String throwHello(String tag) {
return "Hello :" + tag;
}
}
调用:
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
Class robot = Class.forName("com.lydms.IO.Robot");
Robot r = (Robot) robot.newInstance();
System.out.println("Class name is" + robot.getName());
// 1、执行private方法throwHello,并接受返回值
Method throwHello = robot.getDeclaredMethod("throwHello", String.class);
// 将私有化方法设置为可见
throwHello.setAccessible(true);
// 执行方法,并传入参数,获取返回值
Object str = throwHello.invoke(r, "Bob");
System.out.println("Result is " + str);
// 2、执行publi方法sayHi,并指定name的值
Method sayHi = robot.getMethod("sayHi", String.class);
sayHi.invoke(r, "Welcome");
// 3、执行publi方法sayHi,并指定name的值
// 获取私有变量的对象
Field name = robot.getDeclaredField("name");
// 给私有变量设置可见
name.setAccessible(true);
name.set(r, "Alice");
sayHi.invoke(r, "Welcome");
}
}
类从编译到执行的过程
- 编译器将 Robot java源文件编译为 Robot.class字节码文件
- Classloader将字节码转换为JVM中的 Class< Robot>对象
- JVM利用Class< Robot>对象实例化为 Robot对象
5、谈谈ClassLoader
Classloader 在 Java 中有着非常重要的作用,它主要工作在 Class装载的加载阶段,其主要作用是从系统外部获得 Class二进制数据流。它是Java的核心组件,所有的Class都是由 Classloader进行加载的,Classloader负责通过将 Class文件里的二进制数据流装载进系统,然后交绐Java虚拟机进行连接、初始化等操作。
ClassLoader的种类
- BootstrapClassLoader:C++编写,加载核心库java.*。
- Extclassloader:Java编写,加载扩展库 Javax.*。
- AppClassLoader:Java编写,加载程序所在目录方法。
- 自定义 Classloader:Java编写,定制化加载。
Extclassloader:(扩展库 Javax.*)
String property = System.getProperty("java.ext.dirs");
AppClassLoader:(加载程序所在目录)
String property = System.getProperty("java.class.path");
自定义ClassLoader的实现
关键函数
寻找class文件,读取二进制文件
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
定义类(将字节流转为class)
protected final Class<?> defineClass(byte[] b, int off, int len)throws ClassFormatError{
return defineClass(null, b, off, len, null);
}
根据名称/位置加载Class文件,调用defineClass去解析字节流
待执行文件:
public class Wali{
static{
System.out.println("Hello Wali");
}
}
自定义类加载器
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String path;
private String classLoaderName;
public MyClassLoader(String path, String classLoaderName) {
this.path = path;
this.classLoaderName = classLoaderName;
}
// 用于寻找类文件
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
// 用于加载类文件
private byte[] loadClassData(String name) {
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(new File(name));
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
}
return out.toByteArray();
}
}
调用类加载器
public class ClassLoaderChecker {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader m = new MyClassLoader("D:\\", "myClass");
Class<?> c = m.loadClass("Wali");
// 会调用自定义的findClass( )
ClassLoader classLoader = c.getClassLoader();
System.out.println(classLoader);
// 执行加载文件里的方法
c.newInstance();
}
}
类加载器的双亲委派机制
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1、首先,检查是否已经加载的类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
2、没有的话,代用父类的加载器
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果类没有找到抛出ClassNotFoundException 没有父类加载
}
if (c == null) {
// 如果仍然没有找到,然后调用findClass去寻找class
long t1 = System.nanoTime();
c = findClass(name);
这是定义的类装入器;记录数据;
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
为什么要使用双亲委派机制去加载类?
- 避免对分同样字节码的加载
没必要保存相同的类对象class。不使用委托机制,则每个类加载一次Class文件,内存中会有多份class文件。使用委托机制,自定义classLoader开始逐层向上查询是否装载,如果有则不再装载。保证内存中只有一份class文件。
6、类的加载方式
- 隐式加载:new
- 显示加载:loadClass,forName等
类的装载过程
loadClass和forName的区别
- Class.forName得到的 class 是已经初始化完成的(完成加载、链接、初始化)
- Classloder.loadClass得到的 class 是还没有链接的(完成加载)
package com.lydms.IO;
public class Robot {
static {
System.out.println("Hello Robot");
}
}
采用classLoader方式加载(未执行static方法)
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = Robot.class.getClassLoader();
}
采用Class.forName( )方式加载(执行static中方法)
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("com.lydms.IO.Robot");
}
7、Java的内存模型
在程序执行的过程中,需要不断将内存的逻辑地址与物理地址进行映射,找到相关的指令以及数据去执行,java运行时面对着与其它进程完全相同的内存限制即受限于操作系统架构提供的可寻址地址空间,操作系统架构提供的可寻址地址空间由操作系统的位数决定,32位处理器提供了2^32的可寻址范围即4GB.
内核空间:内核是主要的操作系统程序和C运行时的空间,包含用于连接计算机硬件,调度程序,提供联网和虚拟内存等服务的逻辑和基于C的进程。
用户空间:用户空间是java进程实际运行时使用的内存空间,32位系统用户最多可以访问3GB,内核可以访问所有物理内存,而64位系统用户进程最大可以访问超过512gb,内核代码同样可以访问所有物理空间。
内存简介
- 32位处理器:2^32的可寻址范围
- 64位处理器:2^64的可寻址范围
地址空间的划分
- 内核空间(主要操作系统程序和C运行位置)
- 用户空间(Java程序运行位置)
Java内存模型(Runtime Data Area)
JVM内存模型—JDK8
程序计数器(Program Counter Register)
- 当前线程所执行的字节码行号指示器(逻辑)。
- 改变计数器的值来选取下一条需要执行的字节码指令。
- 和线程是一对一的关系即“线程私有”。
- 对Java方法计数,如果是Native方法则计数器值为 Undefined
- 不会发生内存泄露。
Java虚拟机栈(Stack)
- Java方法执行的内存模型
- 包含多个栈针
局部变量表和操作数栈
- 局部变量表:包含方法执行过程中的所有变量。
- 操作数栈:入栈、出栈、复制、交换、产生消费变量。
public class ByteCodeSample{
public static int add(int a, int b){
int c=0;
c = a + b;
return c;
}
}
javap 反编译后
执行 add(1,2)
0:iconst_0:0压入操作数栈,1/2放入局部变量表
- 1: istore_2:将栈中元素弹出,放到局部变量表中2位置。
- 2: iload_0:将局部变量中第0个元素,压入栈中。
- 3: iload_1:将局部变量中第1个元素,压入栈中。
- 4: iadd:将栈中0和1进行计算,并将结果压入栈顶。
- 5: istore_2:将栈中元素弹出,放到局部变量2位置
- 6: iload_2:将局部变量2中元素,压入栈中。
- 7: ireturn:将栈中元素返回
递归为什么会引发 java.lang StackOverflow Error 异常
当线程执行一个方法时,就创建一个栈针,并将创建的栈针压入虚拟机栈中,当方法执行完毕时,将栈针出栈。一直自己调用自己,不能释放栈针,递归过深,栈帧数超过虚拟栈深度,抛出StackOverflow Error异常。
递归过深,栈帧数超出虚拟栈深度。
虚拟机栈过多会引发 java.lang.OutofMemory Error(内存溢出)异常
栈内存不需要通过GC回收,就能够自动释放内存。
本地方法栈(Native Method Stack)
与虚拟机栈相似,主要作用于标注了 native的方法
8、元空间(MetaSpace)与永久代(PermGen)的区别
元空间:JDK1.8后,类的元数据放在本地堆内存中,叫做元空间(MetaSpace)。这一块区域在JDK1.7以前属于永久代,元空间与永久代都是用来存储class的相关信息,包括class的method与field等,元空间与永久代都是方法区的实现,只是实现方法有所不同.所以说方法区只是JVM的规范,原先位于方法区的JVM常量池已被移动到堆中,并且在jdk8以后用元空间替代了永久代。
,
- 元空间使用本地内存,而永久代使用的是JVM的内存。
- 此异常不存在了: java.lang.OutOfMemoryError:PermGen space
MetaSpace相比 Permgen的优势
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出。
- 类和方法的信息大小难易确定,给永久代的大小指定带来困难。
- 永久代会为GC带来不必要的复杂性。
- 方便 Hotspot与其他JVM如 Jrockit 的集成。
Java堆(Heap)
- 对象实例的分配区域。
- GC管理的主要区域。
java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,所有的对象实例都在这里分配内存.也是GC管理的主要区域。
32位处理器java进程的内存布局:
现在收集器基本都采用分代收集算法,所以java堆还可以分为新生代和老年代:
8、Java内存模型之常考习题
JVM三大性能调优参数-Xms-Xmx-Xss的含义
- -Xms:堆的初始值。
- -Xmx:堆能达到的最大值。
- -Xss:规定了每个线程虚拟机栈(堆栈)的大小
- -Xmn10m:设置新生代大小为20m。
通常将-Xms与-Xmx写成一样的,避免内存抖动,不必再为扩展内存空间而消耗性能;
Java内存模型中堆和栈的区别->内存分配策略
程序运行时有三种内存分配策略,静态的,栈式的,堆式的。
- 静态存储:编译时确定每个数据目标在运行时的存储空间需求。(静态存储是指在编译时能确定每个数据目标在运行时的存储空间需求,因而在编译时就能分配给它们固定的内存空间,这种程序分配策略要求代码中不能有可变数据集,以及嵌套,递归的结构出现)
- 栈式存储:数据区需求在编译时未知,运行时模块入口前确定。(该程序可被动态的存储分配,程序对数据区的要求是编译时是完全未知的,运行时才能知道,但是规定在运行到数据模块时必须知道该程序所需内存的大小以分配其内存)
- 堆式存储:编译时或运行时模块入口都无法确定,动态分配.
Java内存模型中堆和栈的区别
- 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址
区别:
- 管理方式:栈自动释放,堆需要GC。
- 空间大小:栈比堆小。
- 碎片相关:栈产生的碎片远小于堆。
- 分配方式:栈支持静态和动态分配,而堆仅支持动态分配。
- 效率:栈的效率比堆高。
元空间、堆、线程独占部分间的联系—内存角度
public class HelloWorld {
private String name;
public void getName() {
System.out.println("Hello" + name);
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
int a = 1;
HelloWorld hw = new HelloWorld();
hw.setName("Test");
hw.getName();
}
}
不同版本之间的intern( )方法的区别—JDK6 VS JDK6+
String s = new String( "a")
s .intern( );
- JDK6:当调用 intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
- JDK6+:当调用 intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。
10、字符串常量池
JVM中的常量池从永久代中,移动到了堆中。
public class PermGenErrTest {
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
// 将返回的随机字符串添加到字符串常量池中
getRandomString(100000).intern();
}
System.out.println("Creat End");
}
// 返回指定长度的随机字符串
private static String getRandomString(int length) {
// 字符串源
String str = "asdfasdfiouwerwelsdfkjsafjiasfnsnfsdafsfs";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(20);
sb.append(str.charAt(number));
}
return sb.toString();
}
}
设置永久代的大小
-XX: MaxPermSize=6M 永久代最大为6M
-XX: PermSize=6M 初始时永久代大小
-XX:MaxDirectMemorySize 可以设置java堆外内存的峰值
JDK6(报出异常—永久代内存溢出)
JDK7+(正常执行)
public class Test {
public static void main(String[] args) {
// 存在堆中
String s = new String("a");
// 想放到常量池中,但是其中已经存在,放失败
s.intern();
// 常量池中
String s2="a";
System.out.println("s==s2: "+(s==s2));
// 拼接为"aa",在进行比较
String s3 = new String("a") + new String("a");
s3.intern();
String s4="aa";
System.out.println("s3==s4: "+(s3==s4));
}
}
输出结果:
JDK6中:
s==s2: false
s3==s4: false
JDK6+中:
s==s2: false
s3==s4: true
解析:
是在常量池中创建"a’,再在堆中创建"a"的空间。也就是占用2份内存空间。
String s = new String("a");
在堆中创建出"aa",并将"aa"放置到常量池中
String s3 = new String("a") + new String("a");
s3.intern();
s==s2: false
String s = new String("a");在堆中创建内存空间,s指的是堆内存地址。
String s2="a";引用的是常量池中的地址。
s3==s4: true
JDK6:中副本,指向的是常量池和堆中
JDK6+:常量池中可以放置引用。
生成的s3值"aa",存在于堆中,将其引用放在常量池中。
s4创建的时候,查看常量池中有"aa"的引用(指向堆中),则使用该引用。则S3==S4(true)
堆中"aa",常量池中有"aa"的引用。