Java内存分析

Posted 有心有梦

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java内存分析相关的知识,希望对你有一定的参考价值。

Java内存主要分为三个部分:

    • 存放new的对象和数组

    • 可以被所有的线程共享,不会存放别的对象引用

    • 存放基本变量类型(会包含这个基本类型的具体数值)

    • 引用对象的变量(会存放这个引用在在堆里面的具体地址)

  • 方法区

    • 可以被所有的线程共享

    • 包含了所有的class和static变量

类加载到内存的流程

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

  1. 加载(Load):这是由类加载器(ClassLoader)执行的。通过一个类的全限定名来获取其定义的二进制字节流(class文件字节码),将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,根据字节码在Java堆中生成一个代表这个类的java.lang.Class对象。

  2. 链接(Link):将Java类的二进制代码合并到JVM的运行状态之中,包括三步:

    • 验证:确保加载的类信息(class文件中的字节流包含的信息)符合JVM规范,没有安全方面的问题;

    • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配;

    • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

  3. 初始化(Initalize):到了此阶段,才真正开始执行类中定义的Java程序代码。

    • 执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器);

    • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化;

    • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步;

用代码来分析一下这个过程:

public class Test05 {
    /*
        1.加载到内存,会产生一个类对应的Class对象
        2.链接,链接结束后 m = 0
        3.初始化
            <clinit>(){
                System.out.println("AA类静态代码块");
                m = 300;
                m = 100;
            }
            m = 100;
     */
    public static void main(String[] args) {
?
//        System.out.println(AA.m);
        /*
            输出结果:
                AA类静态代码块
                100
         */
?
        AA aa = new AA();
        System.out.println(aa.m);
        /*
            输出结果:
                AA类静态代码块
                AA类无参构造函数初始化
                100
         */
?
    }
}
class AA {
?
    static {
        System.out.println("AA类静态代码块");
        m = 300;
    }
    static int m = 100;
    public AA(){
        System.out.println("AA类无参构造函数初始化");
    }
}

所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此Java程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。这一点与许多传统语言都不同。动态加载使能的行为,在诸如C++这样的静态加载语言中是很难或者根本不可能复制的。

在进行类加载的时候,类加载器会先检查这个类的Class对象是否已经加载,如果还未加载,则默认的类加载器就会根据类名查找.class文件(这里不一定都是从文件系统中获取,可以从数据库读取字节码或者接受网络中传输过来的字节码)。这些字节码在被加载的时候,会检查验证保证它们没有被破坏。一旦某个类的Class对象被载入内存,它就会被用来创建这个类的所有对象。

通过上面的过程我们还可以直到,static变量在链接阶段就已经有默认值了,所以在类没有的被初始化的时候,就可以调用static变量了。

这里要解释一下三个概念,字面量、符号引用、直接引用:

  • 字面量:用于表达源代码中一个固定值的表示法,比如Java中的常数1,2,..等,或者说字符串"abcd" "java"等

  • 符号引用:以一组符号来描述所引用的目标, 符号可以是任何形式的字面量, 只要使用时能够无歧义的定位到目标即可。例如, 在Java中, 一个Java类将会编译成一个class文件. 在编译时, Java类并不知道所引用的类的实际地址, 因此只能使用符号引用来代替. 比如org.simple.People类引用了org.simple.Language类, 在编译时People类并不知道Language类的实际内存地址, 因此只能使用符号org.simple.Language来表示Language类的地址。

  • 直接引用可以是以下三种:

    • 直接指向目标的指针.( 指向方法区中类对象, 类变量和类方法的指针);

    • 相对偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到实例方法字节码或者实例变量字节码的起始位置;

    • 一个间接定位到对象的句柄;

符号引用通常是设计字符串的——用文本形式来表示引用关系。直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

通过上面的Object类的getClass方法我们可以取得该类已经被实例化了的(new 运算的)对象的类的引用,这个引用指向的是Class类的对象。比如,我们看如下代码:

// 创建User对象,并调用getClass方法获取类的引用
User user = new User();
System.out.println(user.getClass());
/*
    输出结果为:class 反射.User
*/
  // 通过反射获取类User的Class对象
Class c1 = Class.forName("反射.User");
System.out.println(c1); 
// 输出结果:class 反射.User

getClass方法返回的就是当前对象的运行时类,就是实际创建当前对象的那个类的引用。

类初始化

什么时候会发生类初始化
  • 类的主动引用(一定会发生类的初始化)

    • 当虚拟机启动,先初始化main方法所在的类

    • new一个类的对象

    • 调用的类的静态成员(除了final常量)和静态方法

    • 使用java.lang.reflect包的方法对类进行反射调用

    • 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类

  • 类的被动引用(不会发生类的初始化)

    • 当访问一个静态域的时候,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化;

    • 通过数组定义类引用,不会触发此类的初始化;

    • 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)

用代码展示一下:

public class Test06 {
    static {
        System.out.println("加载Main");
    }
?
    public static void main(String[] args) throws ClassNotFoundException {
        // 1.主动引用
        //Son son = new Son();
        /*
        输出结果:
            加载Main
            父类被加载
            子类被加载
         */
        
        // 2.反射也会产生主动引用
        //Class.forName("反射.Son");
        /*
        输出结果:
            加载Main
            父类被加载
            子类被加载
         */
        
        // 3.调用的类的静态成员(除了final常量)和静态方法会初始化类
        // 但是,当访问一个静态域的时候,只有真正声明这个域的类才会被初始化
        // 所以下面这个语句的输出结果为:
        /*
            加载Main
            父类被加载
            100
         */
        // System.out.println(Son.a);
?
        // 通过数组定义类引用,不会导致类的初始化
        //Son[] array = new Son[5];
        /*
            加载Main
         */
        
        // 引用常量也不会导致类的初始化
        //System.out.println(Son.c);
        /*
            加载Main
            100
         */
    }
}
?
class Father{
    static {
        System.out.println("父类被加载");
    }
    static int a = 100;
}
?
class Son extends Father{
    static {
        System.out.println("子类被加载");
    }
    static final int c = 100;
}

类加载器

  • 类加载器的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。

  • 类缓存:标准的Java SE加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

JVM规范定义了如下类型的类加载器:

  • 引导类加载器:用C++编写的,是JVM自带的类加载器,负责Java平台核心库(rt.jar包),用来装载核心类库。该加载类无法直接获取。

  • 扩展类加载器:负责jre/lib/ext目录下的jar包或-D java.ext.dirs指定目录下的jar包装入工作库;

  • 系统类加载器:负责java -classpath 或 -D java.class.path所指的目录下的类与jar包装入工作,是最常用的加载器。

实际用代码来调用一下加载器:

public static void main(String[] args) throws ClassNotFoundException {
    //获取系统类的加载器
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
    System.out.println(systemClassLoader);
    /*
        sun.misc.Launcher$AppClassLoader@18b4aac2
     */
?
    // 获取系统类加载器的父类加载器-->扩展类加载器
    ClassLoader parent = systemClassLoader.getParent();
    System.out.println(parent);
    /*
        sun.misc.Launcher$ExtClassLoader@677327b6
     */
?
    //获取扩展类加载器的父类加载器-->根加载器(C/C++)
    ClassLoader parent1 = parent.getParent();
    System.out.println(parent1);
    /*
        null
     */
?
    // 测试当前类是哪个加载器加载的
    ClassLoader classLoader = Class.forName("反射.Test07").getClassLoader();
    System.out.println(classLoader);
?
    // 测试JDK内置的类是谁加载的
    classLoader = Class.forName("java.lang.Object").getClassLoader();
    System.out.println(classLoader);
?
    // 如何获得系统类加载器可以加载的路径
    System.out.println(System.getProperty("java.class.path"));
    /*
    /usr/java/jdk1.8.0_231/jre/lib/charsets.jar:
    /usr/java/jdk1.8.0_231/jre/lib/deploy.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/cldrdata.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/dnsns.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/jaccess.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/jfxrt.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/localedata.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/nashorn.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/sunec.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/sunjce_provider.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/sunpkcs11.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/zipfs.jar:
    /usr/java/jdk1.8.0_231/jre/lib/javaws.jar:
    /usr/java/jdk1.8.0_231/jre/lib/jce.jar:
    /usr/java/jdk1.8.0_231/jre/lib/jfr.jar:
    /usr/java/jdk1.8.0_231/jre/lib/jfxswt.jar:
    /usr/java/jdk1.8.0_231/jre/lib/jsse.jar:
    /usr/java/jdk1.8.0_231/jre/lib/management-agent.jar:
    /usr/java/jdk1.8.0_231/jre/lib/plugin.jar:
    /usr/java/jdk1.8.0_231/jre/lib/resources.jar:
    /usr/java/jdk1.8.0_231/jre/lib/rt.jar:
    /home/fym/文档/JavaProject/production/learn_java:
    /home/fym/文档/JavaProject/learn_java/src/lib/commons-io-2.6.jar:
    /usr/local/idea-IU-193.5233.102/lib/idea_rt.jar
     */
?
    /*
        双亲委派机制:
            当我们自己定义一个类的时候,JVM会自己一层一层从用户类加载器到扩展类加载器
            再到引导类加载器的找有没有同名的包,如果有的话则不会调用我们自己写的类,
            而是去用JVM自己的。这是为了保证安全。
     */
}

可以看出系统类加载器加载的也有我们项目路径和IDE的Jar包。

以上是关于Java内存分析的主要内容,如果未能解决你的问题,请参考以下文章

变量的内存分析图

Android 插件化VirtualApp 源码分析 ( 目前的 API 现状 | 安装应用源码分析 | 安装按钮执行的操作 | 返回到 HomeActivity 执行的操作 )(代码片段

java 片段将重用以前膨胀的根视图,这可以节省内存。好可怜 ......

Java 垃圾回收 - 收集算法

任何可以找到内存泄漏java代码的java静态代码分析器?

大数据必学Java基础(三十四):面向对象内存分析