Java虚拟机(JVM)面试知识汇总

 JVM Class文件格式

https://blog.csdn.net/zhaohong_bo/article/details/89399715

JVM类加载过程

什么是JVM的类加载机制?

首先,在代码编译后,就会生成JVM(Java虚拟机)能够识别的二进制字节流文件(*.class)。而JVM把Class文件中的类描述数据从文件加载到内存,并对数据进行校验、转换解析、初始化,使这些数据最终成为可以被JVM直接使用的Java类型,这个说来简单但实际复杂的过程叫做JVM的类加载机制。

 

什么是类初始化?


Class文件中的“类”从加载到JVM内存中,到卸载出内存过程有七个生命周期阶段。类加载机制包括了前五个阶段。

如下图所示:

其中,加载、验证、准备、初始化、卸载的开始顺序是确定的,注意,只是按顺序开始,进行与结束的顺序并不一定。解析阶段可能在初始化之后开始。


另外,类加载无需等到程序中“首次使用”的时候才开始,JVM预先加载某些类也是被允许的。(类加载的时机)

一、类的加载 (loading

我们平常说的加载大多不是指的类加载机制,只是类加载机制中的第一步加载。在这个阶段,JVM主要完成三件事:
1、通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过jar包、war包、网络中获取、JSP文件生成等方式。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)

3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。


二、类的连接 (linking)

类的加载过程后生成了类的java.lang.Class对象,接着会进入连接阶段,连接阶段负责将类的二进制数据合并入JRE(Java运行时环境)中。类的连接大致分三个阶段。

1、验证:验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。

2、准备:为类的静态变量(static filed)在方法区分配内存,并赋默认初值(0值或null值)。如static int a = 100;

静态变量a就会在准备阶段被赋默认值0。

对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。

另外,静态常量(static final filed)会在准备阶段赋程序设定的初值,如static final int a = 666;  静态常量a就会在准备阶段被直接赋值为666,对于静态变量,这个操作是在初始化阶段进行的。

3、解析:将类的二进制数据中的符号引用换为直接引用。

 

三、类的初始化(init)

类初始化是类加载的最后一步,除了加载阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才真正执行Java代码。

类的初始化的主要工作是为静态变量赋程序设定的初值。

如static int a = 100;在准备阶段,a被赋默认值0,在初始化阶段就会被赋值为100。

什么时候进行类初始化?


Java虚拟机规范中严格规定了有且只有五种情况必须对类进行初始化:(主动引用)

1、使用new字节码指令创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。

2、通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。

3、当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。

4、当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。

5、使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。

主动引用和被动引用

注意,虚拟机规范使用了“有且只有”这个词描述,这五种情况被称为“主动引用”,除了这五种情况,所有其他的类引用方式都不会触发类初始化,被称为“被动引用”。

被动引用的例子一:

通过子类引用父类的静态字段,对于父类属于“主动引用”的第一种情况,对于子类,没有符合“主动引用”的情况,故子类不会进行初始化。代码如下:


//父类
public class SuperClass {
    //静态变量value
    public static int value = 666;
    //静态块,父类初始化时会调用
    static{
        System.out.println("父类初始化!");
    }
}
 
//子类
public class SubClass extends SuperClass{
    //静态块,子类初始化时会调用
    static{
        System.out.println("子类初始化!");
    }
}
 
//主类、测试类
public class NotInit {
    public static void main(String[] args){
        System.out.println(SubClass.value);
    }
}
输出结果:


被动引用的例子之二:

通过数组来引用类,不会触发类的初始化,因为是数组new,而类没有被new,所以没有触发任何“主动引用”条款,属于“被动引用”。代码如下:


//父类
public class SuperClass {
    //静态变量value
    public static int value = 666;
    //静态块,父类初始化时会调用
    static{
        System.out.println("父类初始化!");
    }
}
 
//主类、测试类
public class NotInit {
    public static void main(String[] args){
        SuperClass[] test = new SuperClass[10];
    }
}
没有任何结果输出!

被动引用的例子之三:

刚刚讲解时也提到,静态常量在编译阶段就会被存入调用类的常量池中,不会引用到定义常量的类,这是一个特例,需要特别记忆,不会触发类的初始化!


//常量类
public class ConstClass {
    static{
        System.out.println("常量类初始化!");
    }
    
    public static final String HELLOWORLD = "hello world!";
}
 
//主类、测试类
public class NotInit {
    public static void main(String[] args){
        System.out.println(ConstClass.HELLOWORLD);
    }
}

 

类加载器与双亲委派模型

类加载器是什么?

1、通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过jar包、war包、网络中获取、JSP文件生成等方式。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)

3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。

其中,实现第一个工作的代码块就被称为“类加载器”。

类加载器的作用不仅仅是实现类的加载,它还与类的的“相等”判定有关,关系着Java“相等”判定方法的返回结果,只有在满足如下三个类“相等”判定条件,才能判定两个类相等。

1、两个类来自同一个Class文件

2、两个类是由同一个虚拟机加载

3、两个类是由同一个类加载器加载
 

JVM类加载器分类详解:

1、Bootstrap ClassLoader:

         启动类加载器,也叫根类加载器,它负责加载Java的核心类库,加载如(%JAVA_HOME%/lib)目录下的rt.jar(包含System、String这样的核心类)这样的核心类库。根类加载器非常特殊,它不是java.lang.ClassLoader的子类,它是JVM自身内部由C/C++实现的,并不是Java实现的。

2、Extension ClassLoader:

        扩展类加载器,它负责加载扩展目录(%JAVA_HOME%/jre/lib/ext)下的jar包,用户可以把自己开发的类打包成jar包放在这个目录下即可扩展核心类以外的新功能。

3、System ClassLoader\APP ClassLoader:

       系统类加载器或称为应用程序类加载器,是加载CLASSPATH环境变量所指定的jar包与类路径。一般来说,用户自定义的类就是由APP ClassLoader加载的。

4.  用户自定义类加载器:

     通过继承 java.lang.ClassLoader类的方式实现。

 

各种类加载器间关系:以组合关系复用父类加载器的父子关系,注意,这里的父子关系并不是以继承关系实现的。

//验证类加载器与类加载器间的父子关系
    public static void main(String[] args) throws Exception{
        //获取系统/应用类加载器
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("系统/应用类加载器:" + appClassLoader);
        //获取系统/应用类加载器的父类加载器,得到扩展类加载器
        ClassLoader extcClassLoader = appClassLoader.getParent();
        System.out.println("扩展类加载器" + extcClassLoader);
        System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
        //获取扩展类加载器的父加载器,但因根类加载器并不是用Java实现的所以不能获取
        System.out.println("扩展类的父类加载器:" + extcClassLoader.getParent());
    }
}


类加载器的双亲委派加载机制(重点)

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

如何编写一个自定义的类加载器?

主要体现在ClassLoader的loadClass()方法中,思路很简单:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,调用自己的findClass()方法进行加载。


public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
protected synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
        if (parent != null) {
            c = parent.loadClass(name, false);
        } else {
            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.
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}


下面看一个简单的双亲委派模型代码实例验证:


public class ClassLoaderTest {
    public static void main(String[] args){
        //输出ClassLoaderText的类加载器名称
        System.out.println("ClassLoaderText类的加载器的名称:"+ClassLoaderTest.class.getClassLoader().getClass().getName());
        System.out.println("System类的加载器的名称:"+System.class.getClassLoader());
        System.out.println("List类的加载器的名称:"+List.class.getClassLoader());
 
        ClassLoader cl = ClassLoaderTest.class.getClassLoader();
        while(cl != null){
                System.out.print(cl.getClass().getName()+"->");
                cl = cl.getParent();
        }
        System.out.println(cl);
}


输出结果为:

解释一下:
1、ClassLoaderTest类是用户定义的类,位于CLASSPATH下,由系统/应用程序类加载器加载。

2、System类与List类都属于Java核心类,由祖先类启动类加载器加载,而启动类加载器是在JVM内部通过C/C++实现的,并不是Java,自然也就不能继承ClassLoader类,自然就不能输出其名称。

3、而箭头项代表的就是类加载的流程,层级委托,从祖先类加载器开始,直到系统/应用程序类加载器处才被加载
 

JIT

什么是 JIT ?

为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler),简称 JIT 编译器

什么是字节码、机器码、本地代码?

字节码是指平常所了解的 .class 文件,Java 代码通过 javac 命令编译成字节码

机器码和本地代码都是指机器可以直接识别运行的代码,也就是机器指令

什么是编译和解释?

编译器:把源程序的每一条语句都编译成机器语言,并保存成二进制文件,这样运行时计算机可以直接以机器语言来运行此程序,速度很快;

解释器:只在执行程序时,才一条一条的解释成机器语言给计算机来执行,所以运行速度是不如编译后的程序运行的快的;

热点代码

理解

当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。

热点代码的分类

  • 被多次调用的方法

一个方法被调用得多了,方法体内代码执行的次数自然就多,成为“热点代码”是理所当然的。

  • 被多次执行的循环体

一个方法只被调用过一次或少量的几次,但是方法体内部存在循环次数较多的循环体,这样循环体的代码也被重复执行多次,因此这些代码也应该认为是“热点代码”。

上面提到的多次是一个不具体的词语,那到底是多少次才能成为热点代码呢?

如何检测热点代码

判断一段代码是否是热点代码,是否需要触发即使编译,这样的行为称为热点探测,热点探测并不一定知道方法具体被调用了多少次,目前主要的热点探测判定方式有两种:

  • 基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程的栈顶如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”

优点:实现简单高效,容易获取方法调用关系(将调用堆栈展开即可)

缺点:不精确,容易因为因为受到线程阻塞或别的外界因素的影响而扰乱热点探测

  • 基于计数器的热点探测:采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果次数超过一定的阈值就认为它是“热点方法”

优点:统计结果精确严谨

缺点:实现麻烦,需要为每个方法建立并维护计数器,不能直接获取到方法的调用关系

HotSpot使用第二种 - 基于计数器的热点探测方法。

确定了检测热点代码的方式,如何计算具体的次数呢?

计数器的种类(两种共同协作)

  • 方法调用计数器:这个计数器用于统计方法被调用的次数。默认阈值在 Client 模式下是 1500 次,在 Server 模式下是 10000 次

  • 回边计数器:统计一个方法中循环体代码执行的次数

了解了热点代码和计数器有什么用呢?达到计数器的阈值会触发后文讲解的即时编译,也就是说即时编译是需要达到某种条件才会触发的,先写结论,后文讲解什么是即时编译器。

两个计数器的协作(这里讨论的是方法调用计数器的情况):当一个方法被调用时,会先检查该方法是否存在被 JIT(后文讲解) 编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器加 1,然后判断方法调用计数器与回边计数器之和是否超过方法调用计数器的阈值。如果已经超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。

当编译工作完成之后,这个方法的调用入口地址就会被系统自动改成新的,下一次调用该方法时就会使用已编译的版本。(注:1.8后,方法区没有永久代,变成了meta space)

为何 HotSpot 虚拟机要使用解释器与编译器并存的架构?

解释器与编译器两者各有优势

解释器:一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的。


即时(Just-In-Time)编译器:即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。

两者的协作:在程序运行环境中内存资源限制较大时,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。当通过编译器优化时,发现并没有起到优化作用,,可以通过逆优化退回到解释状态继续执行。



初级调优:客户模式(Client Compiler - C1编译器)或服务器模式 (Server Compiler - C2编译器)【后端编译】

 C1 编译,将字节码编译为本地代码,进行简单,可靠的优化,如有必要将加入性能监控的逻辑

 C2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

有两种编译模式可以选择,并且其会在运行时决定使用哪一种以达到最优性能。这两种编译模式的命名源自于命令行参数(eg: -client 或者 -server)。JVM Server 模式与 client 模式启动,最主要的差别在于:-server 模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。原因是:当虚拟机运行在-client 模式的时候,使用的是一个代号为 C1 的轻量级编译器,而-server 模式启动的虚拟机采用相对重量级代号为 C2 的编译器。C2 比 C1 编译器编译的相对彻底,服务起来之后,性能更高。 
通过 java -version 命令行可以直接查看当前系统使用的是 client 还是 server 模式。例如: 

HOT SPOT 默认是混合模式,两者都有

中级编译器调优

   大多数情况下,优化编译器其实只是选择合适的 JVM 以及为目标主机选择合适的编译器(-cient,-server 或是-xx:+TieredCompilation)。多层编译经常是长时运行应用程序的最佳选择,短暂应用程序则选择毫秒级性能的client 编译器。

优化代码缓存

     当JVM编译代码时,它会将汇编指令集保存在代码缓存。代码缓存具有固定的大小,并且一旦它被填满,JVM 则不能再编译更多的代码。如何确定到底需要多大的代码缓存,通常的做法是将代码缓存变成默认大小的两倍或四倍。

编译阈值

     在 JVM 中,编译是基于两个计数器的:一个是方法被调用的次数,另一个是方法中循环被回弹执行的次数。回弹可以有效的被认为是循环被执行完成的次数,不仅因为它是循环的结尾,也可能是因为它执行到了一个分支语句,例如 continue。当 JVM 执行一个 Java 方法,它会检查这两个计数器的总和以决定这个方法是否有资格被编译。如果有,则这个方法将排队等待编译。这种编译形式一般被叫做标准编译。但是如果方法里有一个很长的循环或者是一个永远都不会退出并提供了所有逻辑的程序会怎么样呢?这种情况下,JVM 需要编译循环而并不等待方法被调用。所以每执行完一次循环,分支计数器都会自增和自检。如果分支计数器计数超出其自身阈值,那么这个循环(并不是整个方法)将具有被编译资格。这种编译叫做栈上替换(OSR),因为即使循环被编译了,这也是不够的:JVM 必须有能力当循环正在运行时,开始执行此循环已被编译的版本。标准编译是被-XX:CompileThreshold=Nflag 的值所触发。Client 编译器模式下,N 默认的值 1500,而 Server 编译器模式下,N 默认的值则是 10000。改变 CompileThreshold 标志的值将会使编译器相对正常情况下提前(或推迟)编译代码。在性能领域,改变 CompileThreshold 标志是很被推荐且流行的方法。事实上,您可能知道 Java 基准经常使用此标志(比如:对于很多 server 编译器来说,经常在经过 8000 次迭代后改变次标志)。 
     client 编译器和 server 编译器在最终的性能上有很大的差别,很大程度上是因为编译器在编译一个特定的方法时,对于两种编译器可用的信息并不一样。降低编译阈值,尤其是对于 server 编译器,承担着不能使应用程序运行达到最佳性能的风险,但是经过测试应用程序我们也发现,将阈值从 8000 变成 10000,其实有着非常小的区别和影响。

检查编译过程

     中级优化的最后一点其实并不是优化本身,而是它们并不能提高应用程序的性能。它们是 JVM(以及其他工具)的各个标志,并可以给出编译工作的可见性。它们中最重要的就是--XX:+PrintCompilation(默认状态下是 false)。 
如果 PrintCompilation 被启用,每次一个方法(或循环)被编译,JVM 都会打印出刚刚编译过的相关信息。不同的 Java 版本输出形式不一样,我们这里所说的是基于 Java 7 版本的。 
编译日志中大部分的行信息都是下面的形式: 
 timestamp compilation_id attributes (tiered_level) method_name size depot

   timestamp :编译完成时的时间戳,compilation_id :内部任务 ID
 

把源代码翻译成机器指令,一般要经过以下几个重要步骤:

我们可以把将.java文件编译成.class的编译过程称之为前端编译。把将.class文件翻译成机器指令的编译过程称之为后端编译。

Java中的前端编译

      前端编译主要指与源语言有关但与目标机无关的部分,包括词法分析、语法分析、语义分析与中间代码生成。 
      我们所熟知的javac的编译就是前端编译。除了这种以外,我们使用的很多IDE,如eclipse,idea等,都内置了前端编译器。主要功能就是把.java代码转换成.class代码。 
词法分析 
      词法分析阶段是编译过程的第一个阶段。这个阶段的任务是从左到右一个字符一个字符地读入源程序,将字符序列转换为标记(token)序列的过程。这里的标记是一个字符串,是构成源代码的最小单位。在这个过程中,词法分析器还会对标记进行分类。词法分析器通常不会关心标记之间的关系(属于语法分析的范畴),举例来说:词法分析器能够将括号识别为标记,但并不保证括号是否匹配。


语法分析 
     语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等.语法分析程序判断源程序在结构上是否正确.源程序的结构由上下文无关文法描述。 


语义分析 
      语义分析是编译过程的一个逻辑阶段, 语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。 
语义分析的一个重要部分就是类型检查。比如很多语言要求数组下标必须为整数,如果使用浮点数作为下标,编译器就必须报错。再比如,很多语言允许某些类型转换,称为自动类型转换。 


中间代码生成 
      在源程序的语法分析和语义分析完成之后,很多编译器生成一个明确的低级的或类机器语言的中间表示。该中间表示有两个重要的性质: 1.易于生成; 2.能够轻松地翻译为目标机器上的语言。 
在Java中,javac执行的结果就是得到一个字节码,而这个字节码其实就是一种中间代码。 
PS:著名的解语法糖操作,也是在javac中完成的。
 

如何打破双亲委派?

https://blog.csdn.net/xiaobao5214/article/details/81674215

 

JVM内存屏障?

https://blog.csdn.net/ITer_ZC/article/details/42006811

 

JMM&volatile?

https://blog.csdn.net/javazejian/article/details/72772461

对象的创建过程?

简洁版:https://www.cnblogs.com/niejunlei/p/8651755.html  

简洁版:https://blog.csdn.net/fly_rice/article/details/82354188

详细版:https://blog.csdn.net/justloveyou_/article/details/72466416

结构版:https://artisan.blog.csdn.net/article/details/106912264

关于指针压缩 (hotsport 在内存大于32g的时候才默认无压缩)

一个对象占多少字节?

 

64位的markword

synchronized锁升级过程以及64位jvm的Mark Word_第2张图片

https://blog.csdn.net/qq_36434742/article/details/106854061

Java内存区域

说一下 JVM 的主要组成部分及其作用?

JVM包含两个子系统和两个组件

两个子系统为Class loader(类装载)、Execution engine(执行引擎);

        Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。

        Execution engine(执行引擎):执行classes中的指令。

两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。

        Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。

       Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

java类执行图

å¨è¿éæå¥å¾çæè¿°

说一下 JVM 运行时数据区

不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分:

程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。(方法区只是个抽象的概念,他位于堆中,java8后就没有方法区这个抽象概念,变成元数据区了)
 

方法区:

https://blog.csdn.net/qq_43455790/article/details/107082930(这篇文章介绍的很详细了)

在《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或进行压缩”,但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是和堆分开

所以,方法区看做是一块独立于Java堆的内存空间

å¨è¿éæå¥å¾çæè¿°

方法区的基本理解

方法区(Method Area)与Heap一样,是各个线程共享的内存区域
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样都可以是不连续的
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,就会抛出内存溢出错误:Java.lang.OutOfMemoryErro:MetaSpace 或Java.lang.OutOfMemoryErro:PermGen space
关闭JVM就会释放整个方法区内存
 

HotSpot中方法区的演进

在JDK7之前,习惯上把方法区称为永久代,JDK8开始,使用元空间替换了永久代

本质上,方法区和永久代并不等价,仅是对Hotspot而言。

到了JDK8,完全废弃了永久代的概念(大小启动时候指定,不能变),改用为在本地内存中实现的元空间代替(不设定的话最大就是物理内存)


元空间的本质和永久代类似,都是对JVM规范中的方法进行实现,不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用了本地内存

永久代和元空间二者不仅仅是名字变了,内部结构也调整了

根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常

设置方法区大小与OOM


方法区的大小是不必固定的,jvm可以根据应用的需要动态调整
JDK7及之前:
通过-XX:PermSize来设置永久代初始分配空间。默认值是20.75m
-XX:MaxPermSize来设定永久代最大可分配空间。32位机器默认为64m,64位机器是82m
当jvm加载的类信息容量超过这个值,会报Java.lang.OutOfMemoryErro:PermGen space
JDK8及之后:
通过-XX:MetaspaceSize=N来设置元空间初始分配空间
通过-XX:MaxMetaspaceSize=N来设置元空间最大分配空间
默认值依赖于操作系统。Windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存,如果元数据区发生溢出,虚拟机一样会抛出OutOfMemoryError:Metaspace
-XX:MetaspaceSize=N来设置元空间初始分配空间,Windows下,-XX:MetaspaceSize是21M,这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类,然后这个高水位线将会重置,新的高水位线的值取决于GC后释放了多少元空间,如果释放的空间不同,那么在不超过MaxMetaspace时,适当提高该值。如果释放空间过多,则适当降低该值
如果初始化的高水平线设置过低,上述高水平线调整情况会发生很多次,通过垃圾回收器的日志可以观察到Full GC多次调用,为了避免频繁GC,建议将-XX:MetaspaceSize设置为一个相对较高的值
如何解决OOM

要解决00M异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer) 对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。
如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms) ,与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
 

方法区中存储什么?

类型信息

对每个加载的类型(class,inteface,enum,annoatation),JVM必须在方法区中存储以下类信息

这个类型的完整有效名称(包名.类名)
这个类型的直接父类的完整有效名
这个类型的修饰符
这个类型直接接口的一个有序列表


域信息

JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
域的相关信息包括:域名称,域类型,域修饰符


方法信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

方法名称
方法的返回类型(或void)
方法参数的数量和类型(按顺序)
方法的修饰符(public, private, protected, static,final,synchronized, native, abstract的一个子集)
方法的字节码(bytecodes)、操作数栈、局部变量表及大小 (abstract和native方法除外)
异常表( abstract和native方法除外)
每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引


non-final的类变量

静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
类变量被类的所有实例共享,即使没有类实例时你也可以访问它


补充说明:全局常量:static final

被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了


运行时常量池和常量池

方法区:内部包含了运行时常量池
字节码文件:包含了常量池


一个有效的字节码文件中除了包含类的版本信息,字段,方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包含各种字面量和对类型,域和方法的符号引用
 

为什么需要常量池

一个java源文件中的类,接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存储到字节码中,换另外一种方式,可以存放到常量池里,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池

几种在常量池内存储的数据类型包括:
数量值 字符串值 类引用 字段引用 方法引用
 

运行时常量池

运行时常量池( Runtime Constant Pool) 是方法区的一部分。
常量池表(Constant Pool Table) 是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
JVM为每个已加载的类型( 类或接口)都维护-一个常量池。池中的数据项像数组项一样,是通过索引访问的。
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
运行时常量池类似于传统编程语言中的符号表(symboltable),但是它所包含的数据却比符号表要更加丰富一些。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OutOfMemoryError异常。
 

永久代为什么要被元空间替换

随着Java8的到来,Hotspot Vm中再也见不到永久代了。但这并不意味着类的元数据信息也消失了,这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间

由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间

这项改动是很有必要的,因为:

为永久代设置空间大小是很难确定的
对永久代进行调优是很困难的

 

字符串常量池

https://www.cnblogs.com/tiancai/p/9321338.html

https://blog.csdn.net/qq_41376740/article/details/80338158

https://blog.csdn.net/qq_34115899/article/details/86583262


StringTable为什么要调整位置

JDK7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候会触发,而full gc是老年代的空间不足,永久代的空间不足时才会触发

这就导致了StringTable的回收效率不高,因为开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆中能够及时回收


 

方法区的垃圾回收
有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虛 拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在
(如JDK11时期的ZGC收集器就不支持类卸载)。
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug;就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。


方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型

先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。

字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:

类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用I就可以被回收。
回收废弃常量与回收Java堆中的对象非常类似。

判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
该类对应的java.lang. Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
Java虚拟机被允许对满足,上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象-样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及XX:+TraceClass-Loading、-XX: +TraceClassUnLoading查看类加载和卸载信息

在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
 

Java 运行时常量池 字符串常量池 类的常量池 局部变量表 区别?

https://blog.csdn.net/littlehaes/article/details/105144148

https://blog.csdn.net/zm13007310400/article/details/77534349

 

Java 虚拟机栈:

详情请看:https://www.cnblogs.com/yanl55555/p/12616356.html

栈内存是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、返回出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

invoke相关指令

请看:https://kang-kang.blog.csdn.net/article/details/79422520

 

jvm的半初始化

new是半初始化 invoke发init方法才是初始化对象 对象的构造创建完成

 

Java 堆:

 

一个对象占多少字节?

说到占比多少字节,肯定得先知道对象的组成结构;

https://artisan.blog.csdn.net/article/details/106963927

JVM垃圾回收基础

https://blog.csdn.net/tjiyu/article/details/53982412

https://blog.csdn.net/tjiyu/article/details/53983064

https://blog.csdn.net/tjiyu/article/details/53983650

https://blog.csdn.net/tjiyu/article/details/54588494

关于二次标记的一个小解析

常见垃圾回收机简述

https://blog.csdn.net/weixin_43228814/article/details/88934939

CMS详解

https://blog.csdn.net/hongxingxiaonan/article/details/105019325

https://blog.csdn.net/lz710117239/article/details/78565926?utm_source=blogxgwz0

https://zhuanlan.zhihu.com/p/54286173

https://blog.csdn.net/liang9338/article/details/86547403

cms三色标记详解

https://www.jianshu.com/p/12544c0ad5c1

 

G1详解:

https://blog.csdn.net/u022812849/article/details/113822908

https://www.jianshu.com/p/aef0f4765098

https://blog.csdn.net/fedorafrog/article/details/104503829

 

程序计数器:

 

本地方法栈:

 

 

 

 

原文链接:https://blog.csdn.net/qq_43455790/article/details/107082930
原文链接:https://blog.csdn.net/ThinkWon/article/details/104390752
原文链接:https://blog.csdn.net/u011972171/article/details/80905564
链接:https://www.jianshu.com/p/fbced5b34eff
原文链接:https://blog.csdn.net/zhangliangzi/article/details/51319033

原文链接:https://blog.csdn.net/zhangliangzi/article/details/51338291

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值