《剑指Java面试-Offer直通车》--Java底层知识JVM

目录

一、平台无关性如何实现?

二、JVM如何加载.class文件?

Java虚拟机

JVM架构

三、反射

四、ClassLoader类加载器

ClassLoader的作用

ClassLoader的种类

自定义ClassLoader的实现

五、类加载器的双亲委派机制

六、loadClass和forName的区别

类的加载方式

类的装载过程

loadClass和forName的区别

七、Java内存模型

内存简介

JVM内存模型--JDK8

从线程的角度

        程序计数器(Program Counter Register)

        Java虚拟机栈(Stack)

        本地方法栈(Native Method Stack)

        元空间(MetaSpace)

        Java堆(Heap)

从存储的角度

        JVM三大性能调优参数-Xms、-Xmx、-Xss的含义

        Java内存模型中堆和栈的区别--内存分配策略

推荐资料


《剑指Java面试-Offer直通车》--Java底层知识JVM_第1张图片

一、平台无关性如何实现?

compile once,run anywhere.

编译:javac指令,将源码编译成字节码,生成.class文件(字节码文件)

运行:java指令,JVM解析.class文件,加载进内存,转化成特定平台的执行指令

 

二、JVM如何加载.class文件?

  • Java虚拟机

虚拟机是一种抽象化的计算机,在实际计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM屏蔽底层操作系统平台的不同,并且减少基于原生语言开发的复杂性。只要虚拟机厂商在特定操作系统上实现了虚拟机,定义如何将字节码解析成本操作系统可执行的二进制码(机器码),Java便能实现跨越各种平台。JVM最值得我们学习的两点是JVM内存结构模型和GC。JVM是内存中的虚拟机,JVM的存储就是内存,类、变量、方法都在内存中。

参考:字节码和机器码的区别

  • JVM架构

《剑指Java面试-Offer直通车》--Java底层知识JVM_第2张图片

ClassLoader加载javac编译好的.class文件到内存,不是任意的.class文件都能加载,ClassLoader加载的文件有格式要求。ClassLoader只管加载,只要符合格式要求就能加载,能不能运行由Execution Engine负责;Execution Engine负责对命令进行解析,解析完之后提交到操作系统执行;Native Interface融合不同编程语言为Java所用。Java执行性能绝大多数情况下没有C或者C++高,主流JVM也是基于C++实现的,涉及到一些较高性能的运算时,需要在Java直接调用它们。另外,本着不重复调用的原则,某个库如果已经用到别的语言开发,就不需要再开发一套,而是Java直接对这些库进行调用,为了满足上述要求,JVM在内存中开辟了一块区域处理标记为Native的代码,具体做法是Native Method中登记Native方法,在Execution执行时加载Native方法;程序被加载到Runtime Data Area这里运行。

《剑指Java面试-Offer直通车》--Java底层知识JVM_第3张图片

 

三、反射

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任何一个对象,都能够调用它的方法和属性。这种动态获取信息和动态调用方法的功能称为Java语言的反射机制。

eg:

package com.reflect;

public class Person {
    private String name;
    public void sayHi(String str){
        System.out.println(str+name);
    }
    private String throwHello(String name){
        return "hello,"+name;
    }
}
package com.reflect;

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

public class ReflectSample {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        Class cls=Class.forName("com.reflect.Person");
        System.out.println("类名:"+cls.getName());
        Person person=(Person)cls.newInstance();
        /**private方法的调用*/
        //getDeclaredMethod方法的第一个参数是String类型的方法名,第二个参数是方法传入的参数类型.class
        //getDeclaredMethod能获取所有修饰符修饰的方法,不能获取继承的方法和实现的接口的方法
        Method  throwHello=cls.getDeclaredMethod("throwHello",String.class);
        //私有的需要设置为true
        throwHello.setAccessible(true);
        //invoke方法的第一个参数是对象的实例,第二个参数是上述方法传入的参数
        Object obj=throwHello.invoke(person,"Zhansan");
        System.out.println("throwHello的结果:"+obj);

        /**public方法的调用*/
        //getMethod只能获取public方法,还能获取继承的方法和实现的接口的方法
        Method sayHi=cls.getMethod("sayHi",String.class);
        sayHi.invoke(person,"hi,");

        /**获取private属性*/
        Field name=cls.getDeclaredField("name");
        name.setAccessible(true);
        name.set(person,"lisi");
        sayHi.invoke(person,"hi,");
    }
}

执行结果为:

类名:com.reflect.Person
throwHello的结果:hello,Zhansan
hi,null
hi,lisi

反射是将Java类中的各种成分映射成一个个Java对象:Field、Method、Class...

类从编译到执行的过程:1)编译器将Person.java源文件编译为Person.class字节码文件;2)ClassLoader将字节码转换为JVM中的Class对象(类对象、Class对象,不是new A();之类的对象);3)JVM利用Class对象实例化为Person对象。

参考:Java中的Class类和Java中的java.lang.Class API 详解

 

四、ClassLoader类加载器

ClassLoader是一个抽象类,里面最重要的方法是loadClass。

  • ClassLoader的作用

ClassLoader在Java中有着非常重要的作用,主要工作在Class装载的加载阶段,主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。

  • ClassLoader的种类

1)BootStrapClassLoader:C++编写,加载核心库java.*

2)ExtClassLoader:Java编写,加载扩展库javax.*  (在IDEA导航项Navigate下的Class中搜索ExtClassLoader可以看到ExtClassLoader类的源代码,AppClassLoader同理。BootStrapClassLoader是C++编写的,只能去JVM源码中看)

3)AppClassLoader:Java编写,加载程序所在目录(classpath)

4)自定义ClassLoader:Java编写,定制化加载

  • 自定义ClassLoader的实现

关键函数

1)findClass:寻找class文件,包括读进二进制流,处理,返回一个Class对象

    protected Class findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

2) defineClass:定义一个类,字节码以byte[]数组形式传递进来

   protected final Class defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(name, b, off, len, null);
    }

findClass根据名称加载字节码,调用 defineClass解析定义字节流,返回Class对象。

eg:

public class Hello{
    static{
        System.out.println("Hello World");
    }
}

javac将源码编译成字节码,生成Hello.class文件

自定义ClassLoader实现类:

package com.reflect;

import java.io.*;

public class MyClassLoader extends ClassLoader{
    private String path;

    public MyClassLoader(String path){
        this.path=path;
    }

    @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();
        }finally {
            try {
                out.close();
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return out.toByteArray();
    }
}

测试类:

package com.reflect;

public class MyClassLoaderCheck {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        //编译后的Hello.class文件放在D盘
        MyClassLoader m=new MyClassLoader("D:\\");
        Class cls=m.findClass("Hello");
        //打印该类的类加载器
        System.out.println(cls.getClassLoader());
        //触发static代码
        cls.newInstance();
    }
}

执行结果为:

com.reflect.MyClassLoader@677327b6
Hello World

 

五、类加载器的双亲委派机制

不同的ClassLoader加载类的方式和路径有所不同,为了实现分工,各自负责各自的区块,使得逻辑更加明确。加载类的时候按照各自管理的区域各司其职,所以存在一个机制,让它们之间相互协作形成一个整体,这个机制便是双亲委派机制。

《剑指Java面试-Offer直通车》--Java底层知识JVM_第4张图片

 实现过程见ClassLoader的loadClass方法:

    //name类名
    public Class loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
 
    protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        //synchronized同步锁锁住class,多个线程调用同一个ClassLoader加载同一个类,为了避免冲突加一个同步锁
        synchronized (getClassLoadingLock(name)) {
            Class c = findLoadedClass(name);
            //本身的ClassLoader是否加载过类,加载过跳过下面两个if,返回Class
            //没有加载过进入if
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //ClassLoader的parent不为空,递归调用loadClass
                    //直到BootStrapClassLoader(C++编写),parent为null
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {//从BootstrapClassLoader查找有没有装载过对应的类,装载过返回;没有装载过,在BootstrapClassLoader目录里扫描有没有Class文件,有装载进来;没有返回上层走下一步if
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //ExClassLoader自己实现的findClass方法到对应的路径下找有没有类,有就返回;没有
                    //AppClassLoader自己实现的findClass方法到对应的路径下找有没有类文件,有装载进来返回;没有
                    //用户自定义的ClassLoader的findClass方法,有返回;没有抛出ClassNotFoundException
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

流程图:

《剑指Java面试-Offer直通车》--Java底层知识JVM_第5张图片

为什么使用双亲委派机制去加载类?

避免多份同样字节码的加载(以System.out.println()中的System为例,需要的是System这个class并且只需要一份,如果不用委托而是自己加载自己的,类A打印的时候就会加载一份System字节码,类B打印又会加载一份System字节码,内存中就会有两份System字节码。使用委托机制从自定义的ClassLoader或从当前的Class机制逐层向父类去查找,看哪一个ClassLoader装载过这个System类,System类位于BootStrapClassLoader管辖范围。假设A调用时是第一次装载这个System,BootStrapClassLoader会将System类装载进来,B调用时逐层查找,发现BootStrapClassLoader已经装载过System,直接返回同一个System,内存中只有一份System字节码。)

 

六、loadClass和forName的区别

  • 类的加载方式

隐式加载:new(new方式生成对象时隐式调用类加载器加载对应的类到JVM中,支持调用带参的构造器生成对象的实例)

显示加载:loadClass、forName等(调用Class对象的newInstance方法生成对象的实例,不支持传入参数,需要通过反射调用构造器对象的newInstance方法才能支持参数。)

  • 类的装载过程

1)加载:通过ClassLoader加载class文件字节码,生成Class对象(ClassLoader通过 loadClass方法把class文件字节码加载进内存中,把静态数据转换为运行时数据区方法区的类型数据,在运行时数据区堆中生成一个代表类的java.lang.class对象,作为方法区类数据的访问入口)

2)链接: a、校验:检查加载的class的正确性和安全性(检查class文件格式是否正确)

                 b、准备:为类变量分配存储空间并设置类变量初始值(类变量随类型信息存储在方法区中,生命周期很长,使用不当会造成内存泄漏。类变量即static变量。初始值是类变量类型的默认值,而不是实际要赋的值)

                 c、解析:JVM将常量池内的符号引用转换为直接引用(可选)

3)初始化:执行类变量赋值和静态代码块

参考:类变量和实例变量的区别

  • loadClass和forName的区别

ClassLoader.loadClass得到的class是还没有链接的

    //ClassLoader类里面的loadClass方法参数resolve为true时,调用resolveClass方法。该方法的作用是Links the specified class。即上述第二步链接
    protected final void resolveClass(Class c) {
        resolveClass0(c);
    }

Class.forName得到的class是已经初始化完成的

    //Class.forname方法return forName0方法,forName0的第二个参数initialize为true,初始化类,即上述第三步
    public static Class forName(String className)
                throws ClassNotFoundException {
        Class caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

eg:

//被加载的类
public class Person {
    private String name;
    public void sayHi(String str){
        System.out.println(str+name);
    }
    private String throwHello(String name){
        return "hello,"+name;
    }
    static{
        System.out.println("test");
    }
}
//测试类
public class LoadDifference {
    public static void main(String[] args) throws ClassNotFoundException {
        //ClassLoader什么都没打印        
        ClassLoader cl=Person.class.getClassLoader();
        cl.loadClass("com.reflect.Person");
        //Class.forName打印test
        Class.forName("com.reflect.Person");
    }
}

 这种区别产生的作用:

Class.forName

//连接数据库,加载驱动,用Class.forName
Class.forName("com.mysql.jdbc.Driver");

//Driver类中有static代码段
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

 loadClass

SpringIOC中,资源加载器获取要读入的资源时,即读取Bean的配置文件时(以ClassPath的方式),需要使用ClassLoader.loadClass加载。这和SpringIOC的懒加载有关,SpringIOC为了加快初始速度,大量使用延时加载技术,ClassLoader不需要执行类中的初始化代码和链接的步骤,可以加快加载速度,把类的初始化工作放在实际使用的时候。

参考:类的初始化和实例化的区别、类加载过程

 

七、Java内存模型

  • 内存简介

计算机所有程序都是在内存中运行的,这个内存可能包括虚拟内存,同时也离不开硬盘这种外存支持。

在程序运行过程中需要不断将内存的逻辑地址和物理地址进行映射,找到相关的指令和数据去执行。作为操作系统进程,Java运行时面临着与其他进程完全相同的内存限制,受限于操作系统架构提供的可寻址地址空间,它由处理器的位数决定。常见处理器有32位和64位。32位处理器2^32的可寻址范围,即4GB;64位处理器2^64的可寻址范围。

地址空间被划分为内核空间和用户空间。内核是主要的操作系统程序和C运行时的空间,包含用于连接计算机硬件、调度程序以及提供联网和虚拟内存等服务的逻辑和基于C的进程。除去内核空间就是用户空间,用户空间才是 Java 进程实际运行时使用的内存。

32位系统用户进程最大可以访问3GB,内核代码可以访问所有物理内存。64位用户进程最大可以访问超过512GB,内核代码可以访问所有物理内存。内存寻址空间略。

  • JVM内存模型--JDK8

Java内存指的是RunTime Data Area。Java程序运行在虚拟机之上,运行时需要内存空间,虚拟机执行Java程序的过程中会把管理的内存划分为不同的数据区方便管理。C编译器在划分内存区域的时候把管理的区域划分为数据段和代码段,数据段包括堆、栈、静态数据区。以下以JDK8为标杆讲解Java语言的内存划分,从线程和存储的角度看模型结构。

《剑指Java面试-Offer直通车》--Java底层知识JVM_第6张图片

  • 从线程的角度

《剑指Java面试-Offer直通车》--Java底层知识JVM_第7张图片

线程私有:程序计数器、虚拟机栈、本地方法栈

线程共享:MetaSpace、Java堆

  • 程序计数器(Program Counter Register)

是一块较小的内存空间。当前线程所执行字节码的行号指示器。在虚拟机的概念模型里,字节码解析工作时改变计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于jvm的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,也就是任何时刻,一个处理器(或者说一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每个线程都有独立的程序计数器。各个线程之间的计数器互不影响,独立存储,我们称这块内存区域为线程私有的内存。

如果线程正在执行Java中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址;如果是Native方法,这个计数器就为空(undefined),由于只是记录行号,程序计数器不必担心内存泄漏的问题(该内存区域是唯一一个在Java虚拟机规范中没有规定OutOfMemoryError的区域)。

总之,程序计数器是逻辑计数器而非物理计数器。为了线程切换后能恢复正确的执行位置,每个线程都有独立的程序计数器。只为Java方法计数,Native方法对应的程序计数器为undefined。使用程序计数器不用担心发生内存泄漏的问题。

  • Java虚拟机栈(Stack)

Java虚拟机栈也是线程私有的,是Java方法执行的内存模型。每个方法在执行时都会创建一个栈帧(Stack Frame),即方法运行期间的基础数据结构。栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等,每个方法从调用直至执行完成的过程,都对应着栈帧在虚拟机栈中入栈到出栈的过程。Java虚拟机栈用于存储帧,栈帧持有局部变量和部分结果、方法的调用和返回,方法调用结束时帧才会被销毁。

总之,Java虚拟机栈包含单个线程每个方法执行的栈帧,栈帧存储局部变量表、操作数栈、动态链接、方法出口等信息。

《剑指Java面试-Offer直通车》--Java底层知识JVM_第8张图片

局部变量表和操作数栈区别?

局部变量数组包含方法执行过程中的所有变量,所有方法参数其他局部变量包括布尔值、byte、char、long、short、int、float、double等等类型;操作数栈在执行字节码指令过程中会用到,这种方式类似于原生CPU寄存器,大部分JVM字节码把时间花费在操作数栈的操作上,包括入栈、出栈、复制、交换、产生消费变量的操作。因此局部变量数组和操作数栈的交换变量指令操作通过字节码频繁执行。当前执行的方法在栈的底部,每次方法调用时一个新的栈帧创建并压栈到栈顶,当方法正常返回或抛出未捕获的异常时,栈帧就会出栈,除了栈帧的压栈和出栈,栈不能被直接操作。

递归为什么会引发java.lang.StackOverflowError异常?

eg:fibonacci(1000000)会引发StackOverFlowError异常。递归层数过多,当线程执行一个方法时会随之创建一个对应的栈帧,并将建立的栈帧压入虚拟机栈中,方法执行完毕将栈帧出栈。线程目前执行的方法所对应的栈帧必定位于Java栈的顶部,递归函数不断调用自身,每一次调用会涉及1)每新调用一次方法会生成一个栈帧;2)会保存当前方法的栈帧状态,将它放到虚拟机栈中;3)栈帧上下文切换时会切换到最新的方法栈帧当中。每个线程的虚拟机栈深度是固定的,递归实现将导致栈深度的增加,每次递归都会往栈压一个栈帧,如果这个消耗超出了最大允许的深度,就会报StackOverFlowError异常。解决这个问题的思路就是限制递归的次数,甚至使用循环的方法替换递归。

虚拟机栈过多会引发java.lang.OutOfMemoryError异常。当虚拟机栈可以动态扩展时,如果无法申请足够多的内存,就会抛出这个异常。

虚拟机栈也是Java虚拟机自动管理的,栈类似一个集合,但它有固定的容量,是由多个栈帧合起来的。每调用一个方法运行时Java虚拟机就会自动在内存中分配对应的一块空间,这块空间即一个栈帧。方法调用之后对应的栈帧就会自动释放,所以栈的内存不需要通过GC回收,而会自动释放的原因。JDK自带工具jstack可以分析线程卡顿问题,可以列出当前进程号(pid)对应JVM的所有虚拟机栈的描述,包括每个线程的状态以及堆栈内各栈帧的方法全限定性类名、代码位置。

  • 本地方法栈(Native Method Stack)

与虚拟机栈类似,只不过虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用Native方法服务的。

  • 元空间(MetaSpace)

元空间(MetaSpeace)与永久代(PermGen)的区别?

JDK8以后把类的元数据放在本地堆内存中,这一块区域叫元空间MetaSpace,该区域在JDK7及以前属于永久代。元空间和永久代都是存储class的相关信息,包括class对象的Method和Feild等。元空间和永久代均是方法区的实现。方法区只是JVM的规范,在Java7之后原先位于方法区中的字符串常量池移动到了Java堆中,并且在JDK8以后使用元空间替代了永久代。元空间使用本地内存,永久代使用的是JVM中的内存。

替换的优势:1)字符串常量池存在永久代中,容易出现性能问题和内存溢出。2)类和方法的信息大小难以确定,给永久代的大小指定带来困难。太小容易出现永久代溢出,太大容易导致老年代溢出。3)永久代会为GC带来不必要的复杂度,并且回收效率偏低。永久代中的元数据可能会随着每次Full GC发生而进行移动,HotSpot虚拟机的每种垃圾回收器都需要特殊处理永久代中的元数据。分离出来后可以简化Full GC以及对以后并发隔离元数据等方面进行优化。4)Oracle 可能会将HotSpot 与其他JVM如 JRockit 合二为一。HotSpot是永久代实现特有的,别的VM没有永久代。

总之,元空间与永久代的区别是它直接使用本机内存,没有字符串常量池,在JDK7被移动到了堆中,其他储存的东西在包括类文件JVM运行时数据结构以及class相关的内容如Method和Feild都与永久代一样,只是划分更趋于合理。比如类及相关的元数据生命周期与类加载器一致,每个加载器ClassLoader都会分配一个单独的存储空间。

  • Java堆(Heap)

Java虚拟机所管理的内存中最大的一块,是被所有线程共享的,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,所有的对象实例都在这里分配内存。Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。就像磁盘空间一样,在实现时即可以是固定大小的,也可以是可扩展的。当前主流的虚拟机都是按照可扩展实现的,即通过-Xmx和-Xms控制。若在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为GC堆。从内存回收的角度看,现在收集器都是采用分代收集算法,所以Java堆中还可以细分为新生代和老年代。

  • 从存储的角度

  • JVM三大性能调优参数-Xms、-Xmx、-Xss的含义

在调用Java指令执行程序的时候,可以通过传入以上三个参数分别调整Java堆以及线程所占内存的大小。

-Xss规定了每个线程虚拟机栈(堆栈)的大小,一般情况下256K足够,此配置将会影响此进程中并发线程数的大小。

-Xms初始的Java堆的大小,该进程刚创建出来时它的专属Java堆大小。一旦对象容量超过Java堆的初始容量,Java堆将会自动扩容,扩容到-Xmx大小。

-Xmx表示Java堆可以扩展到的最大值。很多情况下通常将-Xms和-Xmx设置成一样的,因为当heap不够用发生扩容时会发生内存抖动,影响程序运行时的稳定性。

  • Java内存模型中堆和栈的区别--内存分配策略

程序运行时有3种内存分配策略,静态的、栈式的、堆式的

1)静态存储:是指在编译时就能够确定每个数据目标在运行时的存储空间需求,因而在编译时就可以给它们分配固定的内存空间。 这种分配策略要求程序代码中不允许有可变数据结构的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间。

2)栈式存储:栈式存储分配是动态存储分配,是由一个类似于堆栈的运行栈来实现的,和静态存储的分配方式相反。 在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到了运行的时候才能知道,但是规定在运行中进入一个程序模块的时候,必须知道该程序模块所需要的数据区的大小才能分配其内存。和我们在数据结构中所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。

3)堆式存储:堆式存储分配专门负责在编译时或运行时,无法确定存储要求的数据结构的内存分配。 比如可变长度串和对象实例,堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。

堆和栈的联系:

创建好的数组和对象实例都会被保存在堆中,想要引用堆中的某个对象或数组可以在栈中定义一个特殊的变量,栈中这个变量的取值等于数组或者对象在堆内存中的首地址。栈中的这个变量就成了数组或者对象的引用变量,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或者对象。引用变量就相当于是为数组或者对象起的一个名称。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后就会被释放掉了,而数组和对象本身在堆中分配,即使程序运行到使用new产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,在没有引用变量指向的时候,才会变为垃圾,需要等待随后的一个不确定的时间被垃圾回收器释放掉。

《剑指Java面试-Offer直通车》--Java底层知识JVM_第9张图片

堆和栈的区别:

1)管理方式:栈自动释放,堆需要GC。JVM可以针对内存栈进行管理操作,而且该内存空间的释放是编译器就可以操作的内容;而堆空间在Java中,JVM执行引擎不会对其进行释放,而是让垃圾回收器进行自动回收。
2)空间大小:栈比堆小。这是栈空间里面存储的数据以及本身需要的数据特性来决定的;而堆空间在JVM堆实例进行分配的时候,一般大小都比较大,因为堆空间在一个Java程序中需要存储比较多的Java对象数据。
3)碎片相关:栈产生的碎片远小于堆。针对堆空间而言,即使垃圾回收器能够进行自动堆内存回收,但是堆空间的活动量相对栈空间而言比较大,很有可能存在长期的堆空间分配和释放操作,而且垃圾回收器不是实时的,它有可能使得堆空间的内存碎片逐渐累积起来;针对栈空间而言,因为它本身就是栈的数据结构,操作都是一一对应的,而且每一个最小单位的结构栈帧和堆空间内复杂的内存结构不一样,所以它一般在使用过程中很少出现内存碎片。
4)分配方式:栈支持静态和动态分配,而堆仅支持动态分配。栈的静态分配是本身由编译器分配好了,动态分配可能根据情况有所不同;堆空间是一个运行时级别的内存。栈空间分配的内存不需要考虑释放的问题;堆空间在有垃圾回收器的前提下,还是需要考虑其释放的问题的。
5)效率:栈的效率比堆高。因为内存块的排列本身就是一个堆栈结构,所以栈空间的效率比堆空间的效率高很多。计算机底层内存空间本身就是使用了堆栈结构,使得栈空间和底层结构更加符合。栈的操作简单,只设计到了入栈和出栈。栈空间相对堆空间是灵活程度不够,特别是在动态管理的时候。堆空间最大的优点是动态分配,因为它在计算机底层实现可能是一个双向链表的结构。

《剑指Java面试-Offer直通车》--Java底层知识JVM_第10张图片

《剑指Java面试-Offer直通车》--Java底层知识JVM_第11张图片

参考:Java虚拟机的堆、栈、堆栈如何去理解?

 

推荐资料

B站 【黑马程序员-Java语言高级部分】Java 反射(反射机制笔记)、《深入理解Java虚拟机》

你可能感兴趣的:(Java)