目 录CONTENT

文章目录

JVM内存模型

Administrator
2022-11-02 / 0 评论 / 2 点赞 / 1502 阅读 / 7970 字

一、JVM内存划分

image-1668009298685
image-1667318276527

  • java堆:堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。
    在 Java6 版本中,永久代在非堆内存区;到了 Java7 版本,永久代的静态变量和运行时常量池被合并到了堆中;而到了 Java8,永久代被元空间取代了。 结构如下图所示:
    image-1667318306036

  • 方法区: 所有线程所共有,方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、运行时常量池、字符串常量池。类信息又包括了类的版本、字段、方法、接口和父类等信息。(永久代,jdk1.8之后为元空间)

  • 运行时常量池:常量池是方法区的一部分,存放字面量和符号引用。(String的intern()方法)

  • 虚拟机栈:线程私有,他的生命周期与线程相同。虚拟机栈描述的是java内存模型:每个方法执行的同时,都会创建一个栈帧,用于存储局部的变量表、惭怍舒展。动态链接、方法出口信息等。如果线程请求栈的深度情况大于虚拟机所允许的栈的深度,那么将抛出StackOverflowError;如果虚拟机可以动态扩展,扩展时无法申请到足够的内存,就会抛出OutOfMemoryError。

  • 本地方法栈:区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

  • 程序计数器:是一块较小的内存空间,它可以看作是当前线程的字节码的行号指示器。字节码解释器工作时就是通过改变计数器的值来选取吓一跳的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。此区域是java虚拟机规范中唯一没有规定OutOfMemoryErro的区域。

  • 直接内存:native函数分配的堆外内存,和jvm无关,是服务器的内存。

对象的访问:目前主流的访问方式有使用句柄和直接指针

二、类编译加载执行过程

Java 类从编译到运行的整个过程,如下图:
image-1667318392064

2.1类编译

在编写好代码之后,需要将 .java 文件编译成 .class 文件,才能在虚拟机上正常运行代码。可以通过 javac 命令来生成 .class 文件;通过 javap反编译来看看一个 class 文件结构中主要包含了哪些信息。

编译后的字节码文件主要包括常量池和方法表集合这两部分

2.2 类加载

不同的实现类由不同的类加载器加载,JDK 中的本地方法类一般由根加载器(Bootstrp loader)加载进来,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现加载,而程序中的类文件则由系统加载器(AppClassLoader )实现加载。

1)Bootstrap ClassLoader : 将存放于<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中(加载jar包或class 文件)。启动类加载器无法被Java程序直接引用(用c语言编写是最顶层的加载器,在java 代码中如果尝试获取,例如:String.class.getClassLoader(),会返回null)
2)Extension ClassLoader : 将<JAVA_HOME>\lib\ext目录下的,或者被java.ext.dirs系统变 量所指定的路径中的所有类库加载;开发者可以直接使用扩展类加载器;只能加载扩展目 录下的jar包,不能加载class文件
3)Application ClassLoader : 负责加载用户类路径(ClassPath)上所指定的类库,开发者可直 接使用;(利用ClassLoader.getSystemClassLoader()可以获得);可以在家jar包或class文 件
4)最后还有一个自定义类的加载器

2.3类链接

类在加载进来之后,会进行连接、初始化,最后才会被使用;在连接过程中,又包括验证、准备和解析三个部分。

  • 验证:验证类符合 Java 规范和 JVM 规范,在保证符合规范的前提下,避免危害虚拟机安全。
  • 准备:为类的静态变量分配内存,初始化为系统的初始值。对于 final static 修饰的变量,直接赋值为用户的定义值。例如,private final static int value=123,会在准备阶段分配内存,并初始化值为 123,而如果是 private static int value=123,这个阶段 value 的值仍然为 0。
  • 解析:将符号引用转为直接引用的过程。我们知道,在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。类结构文件的常量池中存储了符号引用,包括类和接口的全限定名、类引用、方法引用以及成员变量引用等。如果要使用这些类和方法,就需要把它们转化为 JVM 可以直接获取的内存地址或指针,即直接引用。

2.4 类链接

类初始化阶段是类加载过程的最后阶段,在这个阶段中,JVM 首先将执行构造器 方法,编译器会在将 .java 文件编译成 .class 文件时,收集所有类初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 () 方法。

三、编译优化技术

3.1 方法内联

方法内联的优化行为就是把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。
例如:

private int add1(int x1, int x2, int x3, int x4){
 return add2(x1, x2) + add2(x3, x4);
}

private int add2(int x1, int x2) {
 return x1 + x2;
}

最终会被优化为:

private int add1(int x1, int x2, int x3, int x4) { 
return x1 + x2+ x3 + x4;
}
  • -XX:CompileThreshold 来设置热点方法的阈值。但要强调一点,热点方法不一定会被 JVM 做内联优化,如果这个方法体太大了,JVM 将不执行内联操作。
  • 方法体的大小阈值,我们也可以通过参数设置来优化:经常执行的方法,默认情况下,方法体大小小于 325 字节的都会进行内联,可以通过 -XX:MaxFreqInlineSize=N 来设置大小值;
  • 不是经常执行的方法,默认情况下,方法大小小于 35 字节才会进行内联,可以通过 -XX:MaxInlineSize=N 来重置大小值。

3.2 逃逸分析

逃逸分析(Escape Analysis):是判断一个对象是否被外部方法引用或外部线程访问的分析技术,编译器会根据逃逸分析的结果对代码进行优化。

3.3 栈上分配:

在 Java 中默认创建一个对象是在堆中分配内存的,而当堆内存中的对象不再使用时,则需要通过垃圾回收机制回收,这个过程相对分配在栈中的对象的创建和销毁来说,更消耗时间和性能。这个时候,逃逸分析如果发现一个对象只在方法中使用,就会将对象分配在栈上。

3.3.1 锁消除

在非线程安全的情况下,尽量不要使用线程安全容器。
比如 StringBuffer。由于 StringBuffer 中的 append 方法被 Synchronized 关键字修饰,会使用到锁,从而导致性能下降;但实际上,在以下代码测试中,StringBuffer 和 StringBuilder 的性能基本没什么区别。这是因为在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除

3.3.2 锁消除

逃逸分析证明一个对象不会被外部访问,如果这个对象可以被拆分的话,当程序真正执行的时候可能不创建这个对象,而直接创建它的成员变量来代替。将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了。这种编译优化就叫做标量替换。

四、 双亲委派机制

image-1667318832842

4.1双亲委派机制得工作过程:

1)类加载器收到类加载的请求;
2)把这个请求委托给父加载器去完成,一直向上委托,直到启动类加器;
3)启动器加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,抛 出异常,通知子加载器进行加载

同一个类的标准:类的全路径名必须一致;类的加载器也必须是同一个

好处: java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,该类不会被加载。

class.forName(className) ,反射是对 className 的主动使用,会导致类的初始化;而 java.lang.ClassLoader.loadClass(className) 并不是对类对主动使用,所以不会导致 className 类对初始化

4.2如何破坏双亲委派机制

重写加载器的loadClass 方法

为什么要破坏双亲委派机制?

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

2

评论区