栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java 注解和反射

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java 注解和反射

文章目录
    • 注解
      • 自定义注解
    • 反射
      • 反射机制介绍
      • 反射入门案例
      • 反射机制原理示意图
      • 反射机制
      • 反射调用优化
      • Class类
        • 哪些类型有Class对象
        • 如何获取Class类对象
      • 类的加载过程
      • 类的加载
        • 类加载的三个阶段
      • 类加载器
      • 类加载内存分析
        • 一、类加载的步骤
          • (一)加载阶段
          • (二)连接阶段
          • (三)初始化阶段
        • 二、类的对象
        • 三、内存溢出

注解 自定义注解
package com.kuang.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


//自定义注解
public class Test01 extends Object{
    //注解可以显示赋值,如果没有默认值,我们就必须给注解赋值
    @MyAnnotation2(age = 18)
    public void test() {

    }

    @MyAnnotation3("kuangshen")
    public void test2(){

    }
}

@Target({ElementType.ANNOTATION_TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation2{
    //注解的参数:参数类型 + 参数名();
    String name() default "";
    int age();
    int id() default -1;  //如果默认值为-1,代表不存在
    String[] schools() default {"西部开源","清华大学"};
}

@Target({ElementType.ANNOTATION_TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation3{
    String value();
}
反射

反射机制介绍

反射入门案例
  1. 新建项目,在src下新建配置文件re.properties
classfullpath = com.kuang.reflection.Cat
method = cry
  1. 新建Cat类
package com.kuang.reflection;


public class Cat {
    private String name = "招财猫";

    public void hi() {   //常用方法
        System.out.println("hi " + name);
    }
    public void cry(){
        System.out.println(name+" 喵喵~");
    }
}
  1. 新建ReflectionQuestion类
package com.kuang.reflection.question;

import com.kuang.reflection.Cat;

import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;


@SuppressWarnings("all")
public class ReflectionQuestion {
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //根据配置文件re.properties 指定信息,创建Cat对象并调用方法hi
//        Properties properties = new Properties();
//        properties

        //传统方式 new 对象   -> 调用方法
//        Cat cat = new Cat();
//        cat.hi();

        //1. 使用Properties类,可以读写配置文件
        Properties properties = new Properties();
        properties.load(new FileInputStream("src/re.properties"));
        String classfullpath = properties.get("classfullpath").toString();
        String method = (String) properties.get("method");

        //2. 创建对象 , 传统的方法行不通 -> 使用反射机制
//        new classfullpath()
        //3. 使用反射机制解决
        //(1)加载类,返回一个Class类型的对象
        Class cls = Class.forName(classfullpath);
        //(2) 通过aClass得到你加载的类 com.kuang.reflection.Cat 的对象的实例
        Object o = cls.newInstance();
        System.out.println("o的运行类型="+ o.getClass());  //运行类型
        //(3) 通过cls 得到你加载的类 com.kuang.reflection.Cat 的methodName "hi" 的方法对象、
        //即;在反射中,可以把方法视为对象(万物皆对象)
        Method method1 = cls.getMethod(method);
        //(4)通过method1 调用方法:即通过方法对象 调用方法
        System.out.println("=============================");
        method1.invoke(o);  // 传统方法: 对象.方法() , 反射机制: 方法对象.invoke(对象)
    }
}
  1. 打印结果:
o的运行类型=class com.kuang.reflection.Cat
=============================
招财猫 喵喵~
反射机制原理示意图

反射机制

Java反射机制可以完成:

  1. 在运行时判断任何一个对象所属的类
  2. 在运行时构造任何一个类的对象
  3. 在运行时得到任意一个类所具有的成员变量和方法
  4. 在运行时调用任意一个对象的成员变量和方法
  5. 生成动态代理

反射相关的主要类:

  1. java.lang.Class:代表一个类,Class对象表示某个类加载后在堆中的对象
  2. java.lang.reflect.Method:代表类的方法,Method对象就表示某个类的方法
  3. java.lang.reflect.Filed:代表类的成员变量,Filed对象就代表某个类的成员变量。
  4. java.lang.reflect.Constructor:代表类的构造方法,Constructor对象就代表某个类的构造器。
  5. 这些类在java.lang.reflection中。
package com.kuang.reflection;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;


public class Reflection01 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        //1. 使用Properties类,可以读写配置文件
        Properties properties = new Properties();
        properties.load(new FileInputStream("src/re.properties"));
        String classfullpath = properties.get("classfullpath").toString();
        String method = (String) properties.get("method");

        //3. 使用反射机制解决
        //(1)加载类,返回一个Class类型的对象
        Class cls = Class.forName(classfullpath);
        //(2) 通过aClass得到你加载的类 com.kuang.reflection.Cat 的对象的实例
        Object o = cls.newInstance();
        System.out.println("o的运行类型="+ o.getClass());  //运行类型
        //(3) 通过cls 得到你加载的类 com.kuang.reflection.Cat 的methodName "hi" 的方法对象、
        //即;在反射中,可以把方法视为对象(万物皆对象)
        Method method1 = cls.getMethod(method);
        //(4)通过method1 调用方法:即通过方法对象 调用方法
        System.out.println("=============================");
        method1.invoke(o);  // 传统方法: 对象.方法() , 反射机制: 方法对象.invoke(对象)

        //java.lang.reflect.Filed:代表类的成员变量,Field对象表示某个类的成员变量
        //getField 不能得到私有的属性
        Field ageField = cls.getField("age");
        System.out.println(ageField.get(o));  //传统写法: 对象.成员变量, 反射:成员变量对象.get(对象)

        //java.lang.reflect.Constructor: 代表类的构造方法,Constructor 对象表示构造器
        Constructor constructor = cls.getConstructor();  //()中可以指定构造器参数类型,返回无参构造器
        System.out.println(constructor);  //输出无参构造器

        Constructor constructor1 = cls.getConstructor(String.class);   //传入的String.class是String类的Class对象
        System.out.println(constructor1);  //输出有参构造器
    }
}

打印结果:

18
public com.kuang.reflection.Cat()
public com.kuang.reflection.Cat(java.lang.String)
反射调用优化

反射的优点:
可以动态的创建和使用对象(也是框架底层核心),使用灵活,没有反射机制,框架技术就失去底层支撑。
反射的缺点:
使用反射基本是解释执行,对执行速度有影响。

应用实例:

package com.kuang.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;


//测试反射调用的性能和优化
public class Reflection02 {
//    Field
//    Constructor
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        m1();
        m2();
        m3();
    }

    //传统方法调用hi
    public static void m1(){
        long start = System.currentTimeMillis();
        Cat cat = new Cat();
        for (int i = 0; i<90000000;i++){
            cat.hi();
        }
        long end = System.currentTimeMillis();
        System.out.println("传统方法来调用hi 耗时="+(end - start));
    }

    //反射机制来调用hi
    public static void m2() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//        Cat cat = new Cat();
        long start = System.currentTimeMillis();
        Class cls = Class.forName("com.kuang.reflection.Cat");
        Object o = cls.newInstance();
        Method hi = cls.getMethod("hi");

        for (int i = 0;i<90000000;i++){
            hi.invoke(o);  //反射调用方法
        }
        long end = System.currentTimeMillis();
        System.out.println("反射机制来调用hi 耗时="+(end-start));
    }

    //反射调用优化 + 关闭访问检查
    public static void m3() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

        long start = System.currentTimeMillis();
        Class cls = Class.forName("com.kuang.reflection.Cat");
        Object o = cls.newInstance();
        Method hi = cls.getMethod("hi");
        hi.setAccessible(true);  //在反射调用方法时,取消访问检查
        for (int i = 0;i<90000000;i++){
            hi.invoke(o);  //反射调用方法
        }
        long end = System.currentTimeMillis();
        System.out.println("反射机制来调用hi 耗时="+(end-start));
    }
}

打印结果:

传统方法来调用hi 耗时=4
反射机制来调用hi 耗时=186
反射机制来调用hi 耗时=158

反射调用优化 - 关闭访问检查

  1. Method和Field、Constructor对象都有setAccessible()方法
  2. setAccessible作用是启动和禁用访问安全检查的开关。
  3. 参数值为true表示反射的对象在使用时取消访问检查,提高反射的效率。参数值为false则表示反射的对象执行访问检查

Class类

在Object类中定义了以下方法,此方法将被所有子类继承。
public final Class getClass()
以上的方法返回值的类型是一个Class类,此类是java反射的源头,实际上,反射从程序的运行结果来看很好理解,即:可以通过对象反射求出类的名称。

package com.kuang.annotation;

import java.lang.reflect.Field;


//什么叫反射
public class Test02 {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchFieldException {
        //1.通过反射获取类的class对象
        Class c1 = Class.forName("com.kuang.annotation.User");
        System.out.println(c1);  //显示cls对象,是哪个类的class对象  com.kuang.annotation.User
        //2.输出c1运行类型  java.lang.Class
        System.out.println(c1.getClass());
        //3.得到包名
        System.out.println(c1.getPackage().getName()); //包名
        //4.得到全类名
        System.out.println(c1.getName());
        //5.通过c1创建对象实例
        User user = (User)c1.newInstance();
        System.out.println(user);
        //6.通过反射获取属性id
        Field id = c1.getField("id");
        System.out.println(id.get(user)); //2
        //7.通过反射给属性赋值
        id.set(user,8);
        System.out.println("id为:"+id.get(user));
        //8.获取所有的(字段)属性
        System.out.println("========所有的字段属性==========");
        Field[] fields = c1.getFields();
        for (Field f:fields){
            System.out.println(f.getName());  //名字
        }

        Class c2 = Class.forName("com.kuang.annotation.User");
        Class c3 = Class.forName("com.kuang.annotation.User");
        Class c4 = Class.forName("com.kuang.annotation.User");

        //一个类在内存中只有一个class对象
        //一个类被加载后,类的整个结构都会被封装在class对象中。
        System.out.println(c2.hashCode());
        System.out.println(c3.hashCode());
        System.out.println(c4.hashCode());
    }
}

//实体类 :pojo ,entity
class User {
    public String name;
    public int id = 2;
    public int age;

    public User() {
    }

    public User(String name, int id, int age) {
        this.name = name;
        this.id = id;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + ''' +
                ", id=" + id +
                ", age=" + age +
                '}';
    }
}

打印结果:

class com.kuang.annotation.User
class java.lang.Class
com.kuang.annotation
com.kuang.annotation.User
User{name='null', id=2, age=0}
2
id为:8
========所有的字段属性==========
name
id
age
1555009629
1555009629
1555009629

对象照镜子(getClass())后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。
对于每个类而言,JRE都为其保留一个不变的Class类型的对象。一个Class对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[ ])的有关信息。

  • Class本身也是一个类
  • Class对象只能由系统建立对象
  • 一个加载的类在JVM中只会有一个Class实例
  • 一个Class对象对应的是一个加载到JVM中的一个.class文件
  • 每个类的实例都会记得自己是由哪个Class实例所生成。
  • 通过Class可以完整地得到一个类中的所有被加载的结构。
  • Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象

哪些类型有Class对象
  1. 外部类、成员内部类、静态内部类、局部内部类、匿名内部类
  2. interface:接口
  3. 数组
  4. enum:枚举
  5. annotation:注解
  6. 基本数据类型
  7. void
如何获取Class类对象

package com.kuang.reflection;



//测试class类的创建方式有哪些
public class Test03 {
    public static void main(String[] args) throws ClassNotFoundException {
        //向上转型
        Person person = new Student();
        System.out.println("这个人是:"+ person.name);
        //方式一:父类引用指向子类对象
        Class c1 = person.getClass();
        System.out.println(c1.hashCode());

        //方式二: forName 获得
        Class c2 = Class.forName("com.kuang.reflection.Student");
        System.out.println(c2.hashCode());

        //方式三: 通过类名.class获得
        Class c3 = Student.class;
        System.out.println(c3.hashCode());

        //方式四:基本内置类型的包装类都有一个Type属性
        Class c4 = Integer.TYPE;
        System.out.println(c4);

        //获得父类类型
        Class c5 = c1.getSuperclass();
        System.out.println(c5);
    }
}

class Person{
    public String name;

    public Person(String name) {
        this.name = name;
    }

    public Person() {
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + ''' +
                '}';
    }
}

class Student extends Person{
    public Student() {
        this.name = "学生";
    }
}

class Teacher extends Person{
    public Teacher(){
        this.name = "老师";
    }
}

基本数据类型(int,char,boolean,float,double,byte,long,short)获取Class类对象:
Class cls = 基本数据类型.class;
基本数据类型对应的包装类,可以通过.TYPE 得到Class类对象:
Class cls = 包装类.TYPE;

package com.kuang.reflection;

import java.lang.annotation.ElementType;


//所有类型的Class对象
public class Test04 {
    public static void main(String[] args) {
        Class c1 = Object.class;  //类
        Class c2 = Comparable.class;  //接口
        Class c3 = String[].class;  //一维数组
        Class c4 = int[][].class;  //二维数组
        Class c5 = Override.class;  //注解类型
        Class c6 = ElementType.class;  //枚举类型
        Class c7 = int.class;  //基本数据类型
        Class c8 = void.class;   //void
        Class c9 = Class.class;  //Class
        Class c10 = Integer.TYPE; //包装类  int
        Class c11 = Character.TYPE;   //int

        System.out.println(c1);
        System.out.println(c2);
        System.out.println(c3);
        System.out.println(c4);
        System.out.println(c5);
        System.out.println(c6);
        System.out.println(c7);
        System.out.println(c8);
        System.out.println(c9);
        System.out.println(c10);
        System.out.println(c11);

        //只要元素类型与维度一样,就是同一个class
        int[] a = new int[10];
        int[] b = new int[100];

        System.out.println(a.getClass().hashCode());
        System.out.println(b.getClass().hashCode());
    }
}

打印结果:

class java.lang.Object
interface java.lang.Comparable
class [Ljava.lang.String;
class [[I
interface java.lang.Override
class java.lang.annotation.ElementType
int
void
class java.lang.Class
int
char
1555009629
1555009629

类的加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化。

类的加载

反射机制是java实现动态语言的关键,也就是通过反射实现类动态加载

  • 静态加载:编译时加载相关的类,如果没有则报错,依赖性太强。
  • 动态加载:运行时加载需要的类,如果运行时不用该类,即使不存在该类,则不报错,降低了依赖性。

类加载时机:

  1. 当创建对象时(new) //静态加载
  2. 当子类被加载时,父类也加载 //静态加载
  3. 调用类中的静态成员时 //静态加载
  4. 通过反射 //动态加载
类加载的三个阶段

  1. 加载阶段:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象。
  2. 连接阶段:将java类的二进制代码合并到JVM的运行状态之中的过程。
    ①验证:确保加载的类信息符合JVM规范,没有安全方面的问题。
    ②准备:正式为类变量(static)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
    ③解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
  3. 初始化阶段:
    ①执行类构造器< clinit>()方法的过程。类构造器< clinit>() 方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
    ② 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
    ③虚拟机会保证一个类的< clinit>() 方法在多线程环境中被正确加锁和同步。
类加载器

ClassLoader能根据需要将class文件加载到JVM中,它使用双亲委托机制,在加载类的时候会判断类未被自己加载过,就优先让父加载器加载。另外在使用instanceof关键字、equals()方法、isAssignableFrom()方法、isInstance()方法时,就要判断是不是由同一个类加载器加载。

  1. 类加载器的种类
    1.1 启动类加载器(Bootstrap ClassLoader)
    负责加载JDK中的核心类库,即%JRE_HOME%/lib 目录下,这个类完全由JVM自己控制,外界无法访问这个类。不过在启动JVM时可以通过参数-Xbootclasspath来改变加载目录,有以下三种使用方式:
    ①-Xbootclasspath 完全取代系统默认目录;
    ②-Xbootclasspath/a 在系统加载默认目录前后,加载此目录;
    ③-Xbootclasspath/p 在系统加载默认目录里,加载此目录。
    1.2 扩展类加载器(ExtClassLoader)

继承自URLClassLoader类,默认加载%JRE_HOME%/lib/ext 目录下的jar包。可以用-D java.ext.dirs 来指定加载位置。-D是设置系统属性,即System.getProperty()的属性。
1.3 应用类加载器(AppClassLoader)

继承自URLClassLoader类,加载当前项目bin目录下的所有类。可以通过System.getProperty("java.class.path")获取到目录地址。
1.4自定义类加载器
如果我们想要自己实现类加载器,一般会继承URLClassLoader这个子类,因为这个类已经实现了大部分工作,只需要在适当的地方做些修改就好,就像我们要实现Servlet时通常会直接继承HttpServlet。
不管是直接实现抽象类ClassLoader,还是继承URLClassLoader类,或其他子类,它的父类加载器都是AppClassLoader,因为不管调用哪个父类构造器,创建对象都必须最终调用getSystemClassLoader()作为父类加载器,然后获取到AppClassLoader。
2. 类加载器的加载顺序
在JVM启动时,首先“启动类加载器”会去加载核心类,然后再由“扩展类加载器”去加载,最后让“应用类加载器”加载项目下的类。
另外,类加载器使用双亲委托模型,可以保证类只会被加载一次(当父类架加载了该类的时候,子类就不必再加载),避免重复加载。在加载类的时候,会判断如果类未被自己加载过,就让父加载器进行加载。这个父加载器并不是父类加载器,而是在构造方法中传入(如果不在构造方法中传入,默认的加载器是加载这个类的加载器),并且委派加载流程是在loadClass方法中实现的。当我们自定义类加载器的时候,一般不推荐覆盖loadClass方法。
ClassLoader抽象类中的loadClass方法如下:

在loadClass方法中,当该类没有被自己加载过时,就调用父加载器的loadClass方法(没有父加载器则使用“启动类加载器”)。如果父类加载器没有加载到该类,就使用自己的findClass方法查找该类进行加载。 如果没有找到这个类则会抛出ClassNotFoundException异常。得到这个类的Class对象后,调用resolveClass方法来链接这个类,最后返回这个类的Class对象。
loadClass 中使用的几个方法如下:
①findClass通常是和defineClass方法一起使用。首先要去查找所加载的类字节码文件(不同的类加载器可以通过重写这个方法来实现不同的加载规则,如ExtClassLoader和AppClassLoaderer加载不同的类),然后调用defineClass方法生成类的Class对象并返回。
②defineClass方法是将byte字节流解析成JVM能识别的Class对象,有了这个方法使我们不仅可以通过class文件实例化对象,还可以通过其他方式实例化对象,比如我们通过网络接收到一个类的字节码,可以利用这个字节码流直接创建类的Class对象形式实例化对象。
③resolveClass是对这个类进行连接。如果想在类被加载到JVM中时就被连接,那么可以调用resolveClass方法,也可以选择让JVM在什么时候才连接这个类。
3. 自定义类加载器的作用
Tomcat自己实现自定义类加载器,因为要解决如下功能:
- 隔离两个Web应用所使用的类库,因为两个应用程序可能会用到同一个类的不同版本。
- 共享两个Web应用程序所使用的类库,如果两个应用程序所使用的类完全相同。
- 支持热替换
因此,tomcat服务器自己实现了类加载器。

双亲委派模型就是在其中实现的,所以如果不想打破双亲委派模型,只需 重写findClass方法,如果想要打破双亲委派模型,那可以重写loadClass方法。

  1. 类加载器的使用
    使用当前类的类加载器。
    MyTest.class.getClassLoader().loadClass("");
    ClassLoader.getSystemClassLoader().loadClass("");
类加载内存分析
package com.kuang.reflection;


//类的加载
public class Test05 {
    public static void main(String[] args) {
        A a = new A ();
        System.out.println(A.m);
    }
}
class A{
    static {
        System.out.println("A类静态代码块初始化");
        m = 300;
    }

    static int m = 100;
    public A(){
        System.out.println("A类的无参构造初始化");
    }
}

程序运行结果:

A类静态代码块初始化
A类的无参构造初始化
100
一、类加载的步骤

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括七个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、 初始化(Initialization)、使用(Using)、卸载(Unloading),其中验证、准备、解析三个阶段统称为连接。

注:加载、验证、准备、初始化、卸载这五个顺序是一定的,而解析阶段在某些情况下可以在初始化之后再开始。

(一)加载阶段
首先会获取这个类的二进制字节流,将这个字节流中的数据存储到方法区中,然后生成一个代表该类的java.lang.Class对象(存放在堆中)(HotSpot是把Class对象放到方法区中),用来访问方法区中这些数据。
  1. 类会编译成class,加载到内存的方法区(特殊的堆)中,类加载时,它会把这些信息加进去:Test05类和A类的数据(1.静态变量 2.静态方法 3.常量方法 4.代码 …)
  2. 类加载时,会形成一个class对象,指向堆中的java.lang.Class对象(代表Test05类);产生另一个class对象,指向堆中的Java.lang.Class对象(代表A类)。这两个class对象包含了方法区中类的所有东西。

关于这个类的二进制字节流,我们可以利用自定义类加载器从以下渠道获取:

  • 从压缩包中读取:如JAR、WAR格式;
  • 从网络中获取:如Applet的应用;
  • 从数据库中读取:如有些中间件服务器
  • 运行时生成:如在java.lang.reflect.Proxy中为特定接口生成代理类;
  • 从其他文件中生成:如JSP生成对应的class类
  • … …

对于数组而言,加载情况有所不同,数组类本身不通过类加载器创建,是由JVM直接创建的。但是数组中的元素还是要靠类加载器去创建,如果数组去掉一个维度后是引用类型,就采用类加载器去加载,否则就交给启动类加载器去加载。
另外,加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成。

(二)连接阶段

(将类的二进制数据合并到JRE运行环境中。)

  1. 第一步,验证:是为了确保类中字节码的信息符合JVM的要求, 并且不会危害虚拟机自身的安全,有文件格式验证、元数据验证、字节码验证、符号引用验证。只有通过了文件格式验证,字节流中的数据才会被储存到方法区中,而后面的三种验证则是在方法区中进行的。符号引用验证发生在符号引用转化为直接引用的时候。
    对文件安全校验。
  2. 第二步,准备:为类变量(static)分配内存,并设置类变量默认值(如static int a = 123 ,此时a的值为0,在初始化阶段才会变成123),这些内存都将在方法区中进行分配。这一阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
  3. 第三步,解析:将class常量池内的符号引用,加载到运行时常量池内成为直接引用的过程。符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,但引用目标并不一定已经加载到内存中;直接引用在不同的虚拟机中有不同的实现方式,它可以是直接指向目标的指针、相对偏移量、或是一个能间接定位到目标的句柄,引用的目标必定已经在内存中(类变量、类方法的直接引用可能是直接指针或句柄,实例变量、实例方法的直接引用都是偏移量。实例变量的直接引用可能是从对象的映像开始算起到这个实例变量位置的偏移量,实例方法的直接引用可能是方法表的偏移量)。
    将符号引用转为直接引用。

java字面量和符号引用:
字面量就是比如说int a = 1; 这个1就是字面量。又比如String a = “abc”; 这个abc就是字面量。
在java中,一个java类将会编译成一个class文件。在编译中,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。比如org.simple.People类要引用org.simple.Tool类。在编译时People类并不知道Tool类的实际内存地址,因此只能使用符号org.simple.Tool(假设)来表示Tool类的地址。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,即直接引用地址。

(三)初始化阶段

JVM负责对类进行初始化,这里主要是指静态成员。

首先什么情况下类会初始化?什么情况下类不会初始化?
1. 类的“主动引用”(一定发生初始化)

  • 创建类的实例(如通过new、反射、克隆、反序列化)
  • 访问类的静态变量(除了常量)和静态方法
  • 利用反射调用方法时
  • 初始化类时发现其父类未初始化,则初始化其父类
  • 虚拟机启动时,包含main()方法的类

2.类的“被动引用”(一定不发生初始化)

  • 访问一个静态变量,但这个变量属于其父类,只会初始化其父类。
  • 创建类的数组不会发生初始化(A[ ] a = new A[10];)。
  • 引用常量不会发生初始化(常量在编译阶段就存入所属的常量池中了)

接口的加载过程与类的加载过程稍有不同,接口中不能使用static{ }块。当一个接口在初始化时,并不要求其父接口全部都完成初始化,只有在真正用到父接口时(如引用接口中定义的变量)才会初始化。

二、类的对象
  1. 对象的创建
    A a = new A();会在堆中产生一个新的对象(A类的对象)。该对象指向方法区中的A类,从而可以拿到A类的所有数据(静态变量、静态方法、常量池、代码 …),通过这些数据,给 A类显示赋值,进而初始化。
    详细分析:
    当JVM遇到new指令时,首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载过,如果没有就先执行类加载。如果类已经被加载过,则会为新生对象分配内存(所需内存大小在类加载后就可以确定),分配对象内存采取的方式是“指针碰撞”或“空闲列表”,前者是在内存比较规整的情况下,后者是在空闲内存和已使用内存相互交错的情况下,而内存是否规整又取决于垃圾回收器。
    对象的创建是很频繁的,即使是简单的指针位置的修改,在并发情况下可能会出现线程安全问题。解决这个问题的方式有两种,一种是进行同步处理——JVM采用了CAS方式失败重试来保证的原子性操作;另一种是把内存分配划分在不同空间中——即每个线程预先分配一小块内存,成为本地线程分配缓冲(TLAB),可以通过-XX:+/-UseTLAB参数来设定是否使用。
    内存分配完成后,设置对象的对象头中的信息,如这个对象是哪个类的实例,如何找到。类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,此时,对象已经产生,但是还没有初始化,所有字段都为0。
  2. 对象的内存布局
    对象在内存中存储的布局可以分为3块区域:对象头、实例数据和对齐填充。
  • 对象头:包括两部分信息。第一部分储存对象自身运行时的数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。另一部分是类型指针,指向它的类元数据的指针(不是所有的虚拟机都有)。如果是数组,那在对象头中还必须有一块记录数组长度的数据。
  • 实例数据:这部分是对象真正存储的有效信息,即在对象中定义的各种字段内容(无论是从父类中继承下来的,还是本身所定义的)。存储顺序受虚拟机的分配策略和定义顺序的影响,HotSpot默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops。
  • 对齐填充:不是必然存在的,也没有特别含义,仅仅起着占位符作用。因为HotSpot要求对象起始地址必须是8字节的整数倍。
  1. 对象的访问定位
    可以通过Java栈中对象的引用去访问这个对象,访问对象的主流方式有2种:使用句柄和直接指针。
  • 使用句柄访问:在Java堆中划分一块内存作为句柄池,引用中储存的内容就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
  • 直接指针访问:在对象的内存布局中就要放置访问类型数据的指针。
    这两种方式各有优势,使用句柄的好处是引用中存储的是稳定的句柄,对象被移动时(垃圾回收时对象被移动)只需改变句柄中的实例数据的指针,不需要改动引用本身。而使用直接指针的好处是速度更快,它节省了一次指针定位的开销。HotSpot使用的是第二种方式进行对象的访问。
三、内存溢出

除了程序计数器外,JVM中其他几个内存区域都有可能发生OutOfMemoryError异常。

  1. Java堆溢出
    如果不断创建对象,并且对象始终被强引用,则垃圾回收器无法回收这些对象。最终会产生内存溢出。通过-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机出现内存溢出时Dump出当前对储存为快照,以后事后分析。
    解决堆溢出,一般先通过内存映像分析工具对这个快照进行分析,弄清楚成出现了内存泄漏还是内存溢出。如果是内存泄漏,可以通过工具查看泄漏对象到GC Root的引用链,以此来判断泄漏原因;如果不存在泄漏,即内存中的对象都必须活着,可以调整堆的大小参数和对代码进行优化。
  2. Java栈溢出和本地方法栈溢出
    (在HotSpot中不区分Java栈和本地方法栈,虽然可以通过-Xoss参数设置本地方法栈的大小,但是并没有效果,栈容量只有由-Xss参数设定。)
    栈中发生的异常有两种:
  • 如果需要的深度超过最大深度时抛出StackOverflowError异常。
  • 如果栈无法申请到足够内存时抛出OutOfMemoryError异常。
  1. 方法区和运行时常量池溢出
    String 字符串的intern() 方法作用是,如果字符串常量池存在这个字符,则返回其对象的引用,否则将字符出拷贝到方法区中的字符串常量池。在Java7之后方法区被移入堆中,intern()方法也有所变化,不会将首次遇到的字符串对象本身放入常量池,只会在常量池中记录这个字符串对象的引用。
    在使用CGLib 动态的将类加载进内存时,很容易造成溢出。
  2. 本机内存溢出
    NIO
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/292309.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号