【面试】JVM+ 反射 + 异常

一. 类加载

详细参考
符号引用;直接引用
类加载具体例子参考
java对象在堆中存储结构
方法区中存的就是代码,也就是方法的指令数据(第一步执行什么,第二部执行什么)

二. 双亲委派

1. 回忆类加载过程

文字上描述 :加载;连接(验证;准备;解析);初始化。
在这里插入图片描述

加载;连接(验证;准备;解析);初始化。这些过程实际上都是由类加载器完成的。从底层考虑:类是如何加载的:都是依靠类加载器完成的

2. 类加载器

  • 启动类加载器(Bootstrap ClassLoader):C++实现,在java里无法获取,负责加载<JAVA_HOME>/lib下的类,比如rt.jar…等。
  • 扩展类加载器(Extension ClassLoader): Java实现,可以在java里获取,负责加载<JAVA_HOME>/lib/ext下的类。
  • 应用程序类加载器(Application ClassLoader):是与我们接触对多的类加载器,我们写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。

3. 双亲委派

  • 为什么使用双亲委派:
    对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在Java虚拟机中的唯一性。
    基于上述的问题:如果不是同一个类加载器加载,即时是相同的class文件,也会出现判断不想同的情况,从而引发一些意想不到的情况,为了保证相同的class文件,在使用的时候,是相同的对象,jvm设计的时候,采用了双亲委派的方式来加载类。

  • 使用双亲委派的作用:
    1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
    2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
    在这里插入图片描述

  • 双亲委派定义
    双亲委派:如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。

  • 例子:
    在这里插入图片描述自己定义了一个java.long.String类,里面写了一个main主函数,运行,发现报错,说没有主函数。
    究其原因:类加载采用双亲委派,先委派给父类-----扩展类加载器,发现没加载过,然后继续委派给父类----引导类加载器,发现有java.long.String,所以加载到内存中,也就是最终加载的实际上是,java自带的类,而不是自己定义的那个,所以会出现错误,说没有主方法。

    例如类java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

4. 破坏双亲委派

破坏双亲委派
双亲委派模型,是一种加载类的约定,双亲委派的一个用处为了保证安全的。你用的String类一定是被BootstrapClasserLoader加载的/lib下的那个rt.jar的那个java/lang/String.class
双亲委派这个模式虽然“安全“,但是损失了一丢丢灵活性。就比如java.sql.Driver这个东西。JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商。提供商的库总不能放JDK目录里吧。
Java从1.6搞出了SPI就是为了优雅的解决这类问题——JDK提供接口,供应商提供服务。编程人员编码时面向接口编程,然后JDK能够自动找到合适的实现。

提供商提供的类不能放JDK里的lib目录下,所以也就没法用BootstrapClassLoader加载了。所以当你代码写了Class clz = Class.forName("java.sql.Driver");
Driver d = (Driver)clz.newInstance();时,这个代码会用Bootstrap ClassLoader尝试去加载.问题是java.sql.Driver是个接口,无法真的实例化,就报错了。

使用SPI后,代码大致会这样

Connection connection = 
DriverManager.getConnection("jdbc:mysql://xxxxxx/xxx", "xxxx", "xxxxx");

DriverManager就根据"jdbc:mysql"这个提示去找具体实现去了。然后 System.out.println(connection.getClass().getClassLoader());就会看到这里的结果是Application ClassLoader。这就好像Application ClassLoader加载了本来应该由BootstrapClassLoader加载的java.sql.Connection一样。看起来像是违反了双亲委派模型。但实际上,这里的Connection的类型实际上是“com.mysql.jdbc.JDBC4Connection“,也是个第三方类。AppClassLoader加载一个第三方类看起来并没有违反模型。。

因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况

三. 垃圾回收

GC机制具体知识讲解
参考
程序结束之后对象会被怎么处理(垃圾回收)
>什么是堆内存?堆是在 JVM 启动时创建的,主要用来维护运行时数据,如运行过程中创建的对象和数组都是基于这块内存空间。
什么是垃圾?无任何对象引用的对象。
怎么判断是否是垃圾? 引用计数法;可达性分析法,可以作为GCRoot的是哪些对象。
有哪些方法回收这些垃圾?标记清理法;标记整理法;复制法;分代收集法
什么是分代回收机制?新生代;老年代;永久代。minor GC;major GC

四. 反射

1. 反射的引入

Java中编译类型有两种:

  • 静态编译:在编译时确定类型,绑定对象即通过。
  • 动态编译:运行时确定类型,绑定对象。动态编译最大限度地发挥了Java的灵活性,体现了多态的应用,可以减低类之间的耦合性。

一句话概括就是使用反射可以赋予jvm动态编译的能力,否则类的元数据信息只能用静态编译的方式实现,例如热加载,Tomcat的classloader等等都没法支持。
在日常的第三方应用开发过程中,经常会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放,这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法。
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

2. 反射机制的相关类

类名用途
Class类代表类的实体,在运行的Java应用程序中表示类和接口
Field类代表类的成员变量(成员变量也称为类的属性)
Method类代表类的方法
Constructor类代表类的构造方法

以下关于反射的使用例子参考

获得反射各类的方法参考

3. 获取Class对象

  • getClass方法
  • Class.forName

(1)getClass方法

 String name = "Huanglinqing"; //获取某个类,就写某个的  的【包名.类名】
 Class c1 = name.getClass();
 System.out.println(c1.getName());
 //输出:java.lang.String

(2)Class.forName

   String name = "java.lang.String";//获取某个类,就写某个的  的【包名.类名】。
   Class c1 = null;
   try {
          c1 = Class.forName(name);
          System.out.println(c1.getName());
      } catch (ClassNotFoundException e) {
  }
  //输出:java.lang.String

4. 获取类的构造函数

  • 获取类的所有构造方法:getDeclaredConstructors()
  • 获取类中特定的构造方法:通过getDeclaredConstructor()方法传参获取特定参数类型的构造方法
  • 调用私有构造方法:设置constructors.setAccessible(true)
  • 调用构造方法:借助于newInstance方法

为便于测试,定义一个测试类:
Test类中我们定义是三个私有变量,生成两个公有的含参构造方法和一个私有的含参构造方法以及一个公有的无参构造方法。

public class Test {
    private int age;
    private String name;
    private int testint;
 
    public Test(int age) {
        this.age = age;
    }
    public Test(int age, String name) {
        this.age = age;
        this.name = name;
    }
    private Test(String name) {
        this.name = name;
    }
    public Test() {
    }
 }

(1)获取类的所有构造方法:getDeclaredConstructors(),包括私有方法

//获取类的所有构造方法
 Test test = new Test();
 Class c = test.getClass();
 Constructor[] constructors = c.getDeclaredConstructors() ;
 //具体想要输出看看结果:可以参考上面的博客。

(2)获取类中特定的构造方法:通过getDeclaredConstructor()方法传参获取特定参数类型的构造方法

Test test = new Test();
Class c = test.getClass();
//获取无参构造方法直接不传参数
Constructor c1 = c.getDeclaredConstructor();
//想获取有两个参数分别为int和String类型的构造方法,
Constructor c2 = c.getDeclaredConstructor(int.class,String.class);

(3)调用私有构造方法:设置constructors.setAccessible(true)
参考
(4)调用构造方法:借助于newInstance方法

 constructors = c.getDeclaredConstructor(int.class,String.class);
 constructors.newInstance(24,"HuangLinqing");

那么调用私有构造方法呢,和上面一样,只是我们要设置constructors.setAccessible(true);代码如下:

  constructors = c4.getDeclaredConstructor(String.class);
  constructors.setAccessible(true);
  constructors.newInstance("HuangLinqing");

5. 获取普通方法

  • 获取所有方法(包括父类):getMethods(),获取包括自身和继承过来的所有的public方法
  • 获取本类的所有方法:getDeclaredMethods(),:获取自身所有的方法(不包括继承的,和访问权限无关),可以获得私有的方法。
  • 根据方法名获取特定的方法:getMethod(“方法名字”, 方法参数),表示调用指定的一个公共的方法(包括继承的)
  • 通过invoke执行非私有方法:method.invoke(类的实例化对象),Object invoke(Object instance, Object... parameters);
  • 通过invoke执行私有方法:method.invoke(类的实例化对象),设置method.setAccessible(true);

(1)获取所有方法(包括父类):getMethods()获取包括自身和继承过来的所有的public方法

//甚至连Object的方法都获取到了
Method[] methods = cls.getMethods();

(2)获取本类的所有方法:getDeclaredMethods(),包括私有方法:获取自身所有的方法(不包括继承的,和访问权限无关)

Method[] methods = cls.getDeclaredMethods();

(3)根据方法名获取特定的方法:getMethod(“方法名字”,参数列表),表示调用指定的一个公共的方法(包括继承的)

Method methods = cls.getMethod("toString");//如果方法需要传参,就在后面传入参数。

(4)通过invoke执行非私有方法:method.invoke(类的实例化对象)

//package net.xsoftlab.baike;

import java.lang.reflect.Method;

public class Main34 {
    public void run(){
        System.out.println("调用Person类的run方法");
    }
    public void Speak(int age, String name){
        System.out.println("调用Person类的Speak方法");
        System.out.println("age -> " + age + ". name -> " + name);
    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("Main34");
        // 调用Person类中的run方法
        Method method = clazz.getMethod("run");
        method.invoke(clazz.newInstance());
        // Java 反射机制 - 调用某个类的方法1.
        // 调用Person的Speak方法
        method = clazz.getMethod("Speak", int.class, String.class);
        method.invoke(clazz.newInstance(), 22, "小明");
        // Java 反射机制 - 调用某个类的方法2.
        // age -> 22. name -> 小明
    }
}
/*
调用Person类的run方法
调用Person类的Speak方法
age -> 22. name -> 小明
*/

(5)通过invoke执行私有方法:

import java.lang.reflect.Method;

public class Main34 {
    private void welcome(String name){
        System.out.println(name);
    }

    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("Main34");
        Method method = clazz.getDeclaredMethod("welcome", String.class);//记住要获得私有方法,得通过这种方式,不能使用getMethod方法
        method.setAccessible(true);
        method.invoke(clazz.newInstance(),"Bob");
    }
}

6. 获得类的属性

  • getFields获取类中以及父类中所有的public属性
  • Field[] fields = 类名.getDeclaredFields();获取自己类中所有属性,包括私有的和保护的属性
  • 获得子类以及父类中public属性的某一个属性:getField
  • 获取自己类中所有类型public;private等所有属性中的某一个属性getDeclaredField

(1)getFields获取类中以及父类中所有的public属性

public class Main33{
    public int age;
    private String name;
    protected int pro;
}

public class Main34 extends Main33{
    public int n;
    private String son;
    protected String proson;
    private Class clazz;

    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("Main34");
        Field[] fields = clazz.getFields();
        for(int i=0;i<fields.length;i++){
            System.out.println(fields[i]);
        }
    }
}
/*
public int Main34.n
public int Main33.age
*/

(2)Field[] fields = 类名.getDeclaredFields();获取自己类中所有属性,包括私有的和保护的属性
上述代码只改变一句话:Field[] fields = clazz.getDeclaredFields();
输出为:

public int Main34.n
private java.lang.String Main34.son
protected java.lang.String Main34.proson

(3)获得子类以及父类中public属性的某一个属性:getField

public class Main33{
    public int age;
    private String name;
    protected int pro;
}

public class Main34 extends Main33{
    public int n;
    private String son;
    protected String proson;

    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("Main34");
        Field f = clazz.getField("n");
        System.out.println(f);
        Field f1 = clazz.getField("age");
        System.out.println(f1);
    }
}
/*
public int Main34.n
public int Main33.age
*/

(4)获取自己类中所有类型public;private等所有属性中的某一个属性getDeclaredField

public class Main34 extends Main33{
    public int n;
    private String son;
    protected String proson;

    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("Main34");
        Field f = clazz.getDeclaredField("son");
        System.out.println(f);
    }
}
//private java.lang.String Main34.son

五. 异常

(1)运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
当出现RuntimeException的时候,我们可以不处理。当出现这样的异常时,总是由虚拟机接管。
出现运行时异常后,如果没有捕获处理这个异常(即没有catch),系统会把异常一直往上层抛,一直到最上层,如果是多线程就由Thread.run()抛出,如果是单线程就被main()抛出。抛出之后,如果是线程,这个线程也就退出了。如果是主程序抛出的异常,那么这整个程序也就退出了。运行时异常是Exception的子类,也有一般异常的特点,是可以被catch块处理的。只不过往往我们不对他处理罢了。也就是说,你如果不对运行时异常进行处理,那么出现运行时异常之后,要么是线程中止,要么是主程序终止。

(2)非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类。如IOException、SQLException等以及用户自定义的Exception异常。对于这种异常,JAVA编译器强制要求我们必需对出现的这些异常进行catch并处理,否则程序就不能编译通过。所以,面对这种异常不管我们是否愿意,只能自己去写一大堆catch块去处理可能的异常。

常见的五个编译时异常和常见的五个运行时异常

  • 常见的编译时异常
    1.FileNotFoundException
    2.ClassNotFoundException
    3.SQLException
    4.NoSuchFieldException
    5.NoSuchMethodException
  • 常见的运行时异常
    1.NullPointerException
    2.ArithmeticException
    3.ClassCastException
    4.ArrayIndexOutOfBoundsException
    5.StringIndexOutOfBoundsException

六. java内存泄露

1. 内存泄露的例子

Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的回收,而不需要程序员自己来释放内存。理论上Java中所有不会再被利用的对象所占用的内存,都可以被GC回收,但是Java也存在内存泄露。
内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。
我们知道,对象都是有生命周期的,有的长,有的短,如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。
一个内存泄露的简单例子:

public class Simple {
    Object object;
    public void method1(){
        object = new Object();
    //...其他代码
    }
}

这里的object实例,其实我们期望它只作用于method1()方法中,且其他地方不会再用到它,但是,当method1()方法执行完成后,object对象所分配的内存不会马上被认为是可以被释放的对象,只有在Simple类创建的对象被释放后才会被释放,严格的说,这就是一种内存泄露。解决方法就是将object作为method1()方法中的局部变量。当然,如果一定要这么写,可以改为这样:

public class Simple {
    Object object;
    public void method1(){
        object = new Object();
        //...其他代码
        object = null;//********************
    }
}

这样,之前“new Object()”分配的内存,就可以被GC回收。

2. 一些容易发生内存泄露的例子和解决方法

  1. 像上面例子中的情况很容易发生,也是我们最容易忽略并引发内存泄露的情况,解决的原则就是尽量减小对象的作用域
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页