完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
JVM内存结构
1、类的加载过程 1.1加载(Loading) 1.通过编译生成的class文件 获取类的二进制字节流 2.将这个字节流所代表的静态存储结构 转换为方法区的运行时数据结构 3.在内存中生成一个代表这个类的Class对象(Class模板) 为方法区的这个类的各种数据的访问入口(大Class对象在方法区 1.2连接(Linking) 1.验证确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。cafebabe 2.准备:为类的静态变量分配内存 并将其赋 默认值1.为静态变量分配内存 这些内存都在方法区中 只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等) 2、 对final修饰的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值。 3.这里不会为实例变量分配初始化 ,类变量(静态变量)会在方法区中 而实例变量是会随着对象一起分配到Java堆中 3.解析 将常量池中的符号引用替换为直接引用(内存地址)的过程在类型的常量池中寻找类,接口,字段和方法的符号引用,把这些符号引用替换成直接引用的过程。 常量池:存在于方法区 1.3初始化(Initialization) 为类的静态变量赋初值 并执行静态代码块 就是执行类构造器方法() 的过程 此方法不需要定义 是java编译器自动收集类中的所有(static)类变量的赋值动作和静态代码块的语句合并而来 init方法init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行。
在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法, 另一个是实例的初始化方法。 clinitclinit指的是类构造器,主要作用是在类加载过程中的初始化阶段进行执行,执行内容包括静态变量初始化和静态块的执行。类比init方法是实例化构造器 也就是给创建的对象进行属性初始化 cinit(class init)是类的构造器 那么也就可以想到是给类中的静态变量初始化 和调用静态代码块 注意事项:
initinit指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行。 注意事项:
1.加载: 将class文件变为二进制流存进方法区 生成java.lang.Class 对象 2.连接: 为静态变量 静态代码块中的值赋默认值 对final的静态字面值常量直接赋初值 3.初始化:为静态变量 静态代码块中的值 赋初值 执行静态代码块 只加载一次 何时会触发类加载
关于静态代码块何时会被执行静态代码块的执行时间 静态代码块会在类加载的最后一个过程即初始化阶段被调用,因为在初始化阶段,回调用类的方法,收集各种赋值语句 详情见1.8静态变量与成员变量的初始化过程 https://www.cnblogs.com/ivanfu/archive/2012/02/12/2347817.html 父子类代码块 构造器执行顺序 总结就是当创建一个对象时 (先找父类)父类的静态代码块——> 子类的静态代码块——>父类的非静态代码块——>父类的构造器——>子类的非静态代码块——>子类的构造器 对象的初始化顺序:首先执行父类静态的内容,父类静态的内容执行完毕后,接着去执行子类的静态的内容,当子类的静态内容执行完毕之后,再去看父类有没有非静态代码块,如果有就执行父类的非静态代码块,父类的非静态代码块执行完毕,接着执行父类的构造方法;父类的构造方法执行完毕之后,它接着去看子类有没有非静态代码块,如果有就执行子类的非静态代码块。子类的非静态代码块执行完毕再去执行子类的构造方法。总之一句话,静态代码块内容先执行,接着执行父类非静态代码块和构造方法,然后执行子类非静态代码块和构造方法。 注意:子类的构造方法,不管这个构造方法带不带参数,默认的它都会先去寻找父类的不带参数的构造方法。如果父类没有不带参数的构造方法,那么子类必须用super关键子来调用父类带参数的构造方法,否则编译不能通过 1.6类加载器 类加载器的作用:1.通过类加载器将对应类的字节码文件加载到JVM中 2.通过类加载器将字节码文件转换为方法区的Class对象 类加载器的分类
1.7双亲委派 双亲委派 1.如果一个类加载器收到了类加载请求 他并不会自己先去加载 而是把这个请求委托给父亲的加载器去执行 2.如果父类加载器还存在其父类加载器 则进一步向上委托 请求最终达到顶层的引导类(启动类)加载器 3.如果父类加载器可以完成类加载任务 就成功返回 倘若父类加载器无法完成此加载任务 子加载器才会尝试自己去加载 这就是双亲委派机制 例子: 如果你自己定义一个java.lang.String 类 里面定义了一个main方法 那么无法运行 因为当这个String类在加载的时候 类加载器会向上转换 引导类加载器一看这是 java.lang包下的 就会自己去加载 而真实情况是:我们自己写的类都是由系统类加载器进行加载的 此时引导类加载的实际上还是java真实的Stirng类 而真实地String类中没有main方法 就会报错 双亲委派优势1、首先,保证了java核心库的安全性。如果你也写了一个java.lang.String类,那么JVM只会按照上面的顺序加载jdk自带的String类,而不是你写的String类。 2、保证同一个类不会被加载多次 1.8类的主动使用与被动使用 举例:当一个类中 有静态代码块只会在初始化的时候才会执行 所以在创建类的实例 或者调用静态方法 对静态属性赋值的时候导致类被初始化 这时静态代码块才会执行 重点:静态变量的初始化过程 与 成员变量的初始化过程(详细) 博客:https://blog.csdn.net/a940902940902/article/details/56684669 >1.静态变量与成员变量的初始化过程 类加载过程分为 加载 ——> 连接——>初始化 1.加载: 将class文件变为二进制流存进方法区 生成java.lang.Class 对象 作为各个数据的访问入口 2.连接 又分为
执行====方法 自动收集所有(static)类变量的赋值动作和静态代码块的语句合并而来 在java类的初始化中,一边只初始化一次,类的初始化主要是用以初始化类变量,即静态变量 在初始化的过程中存在着静态变量与静态块两部分,初始化的顺序为先加载静态变量,后加载静态块的内容,静态代码块只在类加载的时候执行且只执行一次 public class StaticTest { //静态变量的直接赋值 public static String name = "张三1"; public static int age; //静态代码块1 static { name = "张三2"; age = 1; } public static char sex; //静态代码块2 static { name = "张三3"; age = 2; sex = '男'; } /* 问题一? 我们都知道 一个变量必须是先声明 才能为其赋值(抛出在声明的时候就赋值)JVM在加载这些代码块和声明变量的时候 都会先加载变量的声明 后加载代码块 不过不同的是 1.静态的变量 在类加载的 连接-准备阶段加载先这些静态变量 为其分配内存空间 然后赋默认值(一定是默认值 除了使用 static final声明的变量 会直接赋初始值) 然后在类加载的第三个过程 初始化时 调用方法 这个方法 收集了静态变量的赋值动作 声明时的直接赋值语句+静态代码块的赋值语句 这两处的赋值语句的加载顺序——>所有的赋值语句按照由上带下的赋值顺序依次执行 如上面代码例子所示 **所以说静态代码块的赋值可以写在声明变量之前 因为先加载的是变量的声明 后加载变量的赋值 如果静态代码块写在了声明属性前面 只是赋值顺序改变了 但是如果静态代码块(有赋值操作)写在了声明变量的前面 那么在静态代码块不能使用 ** 所以说 静态代码块的内容只在类加载的时候执行一次 这一点区别普通的代码块 因为静态代码只能在类加载的时候执行 而普通的代码块是在创建对象的时候才加载 我们都知道在创建对象的时候 第一步就是判断这个类是否被加载 如果没有被加载 就先加载类 在方法区生成一个Class对象 如果加载过了 就不加载了 也就不执行静态代码块 1 public class Person{ 2 { 3 name="李四"; 4 age=56; 5 System.out.println("初始化age"); 6 address="上海"; 7 } 8 public String name="张三"; 9 public int age=29;10 public String address="北京市";11 public Person(){ //构造器也可以写在定义变量之前 因为他的作用就是收集各种赋值语句 但是在创建对象的时候构造器里的赋值语句 会放在收集的所有赋值语句之后运行 代码块的赋值和显示的赋值代码顺序按照从上到下执行 12 name="赵六";13 age=23;14 address="上海市";15 }16 }只是在创建对象阶段 没有cinit方法收集 那两种赋值语句 这里用 init方法代替 也就是类的构造器 它去收集各种赋值语句 按照顺序一一 赋值 这里是 声明是赋值语句与代码块的赋值语句进行顺序执行 构造器本身的赋值语句最后才会执行 这样就顺承了Java开发人员的意愿 即不管是代码块 还是 声明时的直接赋值 都是一个类内部的赋值方式 即每个类刚造出来属性都是那一个值 但是我们想要使用不同属性的对象 那么我们在创建对象的时候就可以在构造器里重新的为我们的对象的属性赋值 保证创建出我们所需要的对象 如上面的代码 public class Person{ public String name; public int age; public String address; public Person(){ name="李四"; //代码块 age=56; //代码块 System.out.println("初始化age"); //代码块 address="上海"; name = "张三"; age = "29"; address = "北京"; name="赵六"; age=23; address="上海市"; } } 特殊的代码块声明在变量之前可以使用变量的 public class Test2 { { a = 4; a += 1; // 这里可以使用a 因为a是静态的 类加载的时候就已经声明并且已经初始化了 而代码块是在创建对象的时候才会执行的 System.out.println("代码块"+a); } private static int a; // 必须是静态变量 如果a不是静态报错 2.如果a不是静态变量 那么在静态代码块里使用a也报错 public static void main(String[] args){ Test2 test2 = new Test2(); System.out.println(test2.a); }} 父类的 静态代码块 非静态代码块 构造器 子类的静态代码块 非静态代码块 构造器 在创建子类对象的时候的执行顺序1.父类的 静态代码块 2.子类的静态代码块 3.父类的代码块 4.父类的构造器 5.子类的代码块 6.子类的构造器 解答:先把这六项分为两部分 一部分是父类的 一部分是子类的 然后分析 静态代码块是在类加载的时候就执行的 每个类的方法收集静态变量的直接赋值语句和静态代码块的赋值语句 我们创建一个子类对象 第一步就是去检查是否加载过子类和父类 如果没有会先加载父类所以父类的静态代码块会第一个执行,然后去加载子类即子类的静态代码块第二个执行,然后分析 代码块和构造器的执行顺序,由上述的知识,我们知道在创建对象的时候 方法会收集所有的非静态成员变量的赋值语句和非静态代码块的所有语句以及构造器内容 且构造器的语句会最后执行,所以在创建对象的时候代码块会先于构造器执行,而我们又知道子类的构造器会默认先调用父类的构造器 所以说在创建子类的对象的阶段 父类的非静态代码块先于父类的构造器执行,然后最后执行子类的代码块,子类的构造器 由上述我们可以知道 一个变量在声明时直接赋值跟在代码块中为其赋值是没有什么区别的 因为最后不管是或是 都会收集两种赋值语句统一赋值 在默认初始化的时候都只会赋类型的默认值(除了final) 所以我们下面的代码 class Test{public final int a; //这里如果没有下面的代码块 就报错 因为final类型的变量要求对象在创建出来的时候就为其赋值 //且只能赋一次值 所以这里的a我们在声明时赋值 在代码块里也赋值 就报错 谁的顺序在后谁报错 { a=2; }} 2.运行时数据区(Running Data Area) 2.1程序计数器(Program Counter Register) 每个线程独有 无GC不会报异常程序计数器用来存储指向下一条指令的地址 即将要执行的指令代码 由执行引擎读取下一条指令 他是程序控制流的的指示器 分支循环 跳转 异常处理 线程恢复等基础功能都需要依赖这个计数器来完成 字节码解释器工作时就是通过改变这个威廉希尔官方网站 器的值来选取下一条需要执行的字节码指令 他是唯一一个在Java虚拟机规范中没有规定任何OutOtMemoryError(内存溢出)情况的区域 2.1.1两个常见问题 使用PC寄存器存储字节码文件指令地址有什么用呢 ?因为CPU需要不停地切换各个线程 这时候切换回来后 就得知道从哪开始继续执行 JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令 PC寄存器为什么会被设定为线程私有的?我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个Pc寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互千扰的情况。 由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。 这样必然导致经常中断或恢复, 如何保证分毫无差呢 每个线程在创建后 都会产生自己的程序计数器和栈帧 程序计数器在各个线程之间互不影响。 2.2虚拟机(Java)栈 每个线程独有 栈生命周期跟线程一致 不存在GC 存在OOM2.2.1 虚拟机栈主要特点 每个线程在创建的时候都会创建一个虚拟机栈 其内部保存一个个的栈帧 (Stack Frame) 对应着一次次的方法调用 作用:主管Java程序的运行 它保存方法的局部变量 部分结果 并参与方法的调用和返回 栈的特点:JVM直接对Java栈的操作只有两个
2.2.2虚拟机栈的常见异常与如何设置栈的大小 2.2.3栈的存储结构和运行原理 栈中存储什么
2.2.4栈帧的内部结构 2.2.4.1栈帧组成–局部变量表(Local Variables) 定义:一个数字数组:主要用于存储方法参数和定义在方法体内的局部变量 这些数据类型包括各类的基本数据类型 对象引用(reference) 以及returnAddress类型 使用索引记录
public class Maintest { public void method1() { int a = 1; int b = 23; int c = 2; String str = "ss"; double d = 2.3; Test test = new Test(); }} 点击method1 点开method1 点击code 点击LocalVariableTable 这个就是局部变量表 只存变量名 Name 绿色的链接指向了class常量池中的变量名 局部变量表举例静态方法举例 public static void method1() { int a = 1; int b = 23; int c = 2; String str = "ss"; double d = 2.3; Test test = new Test(); } 非静态方法举例 public void method1() { int a = 1; int b = 23; int c = 2; String str = "ss"; double d = 2.3; Test test = new Test(); } 由上面分析可以得出 在每个非静态方法中的局部变量表中 都有一个this变量 而静态方法的局变量表中没有this 所以可以明白 为什么静态方法中不允许使用类中的非静态属性 没有this无法引用非静态属性 变量的分类及变量的初始化过程 按照数据类型分类 1.基本数据类型 2.引用数据类型 按照在类中声明的位置分 1.成员变量:在使用前 都经历过默认初始化赋值 类变量:Linking的Prepare阶段 给类变量默认赋值 -->initial(初始化)阶段 给类变量显示赋值 即静态代码块赋值 实例变量:随着对象的创建 会在堆空间中分配实例变量空间 并进行默认赋值 2.局部变量:没有默认赋值的过程 在使用前必须要进行显示赋值 否则编译不能通过 >slot槽的理解 2.2.4.2栈帧组成–操作数栈(Operand Stack数组实现)
https://blog.csdn.net/hudashi/article/details/7062675 push const 是把数字放入操作数栈中 store是把操作数栈中的数字放到 局部变量表中 load是把局部变量表中的数据放到操作数栈中 add数字相加(此时处理后的数据还在操作数栈中) ldc 该系列命令负责把数值常量或String常量值从常量池中推送至栈顶 2.2.4.2.2栈顶缓存威廉希尔官方网站 2.2.4.3栈帧组成–动态链接(Dynamic Linking)
类的加载过程中 解析这一步 是将常量池中的符号引用变为直接引用 动态链接是将栈帧中的指向常量池的符号引用变为直接引用 2.2.4.3.1方法的调用_虚方法_虚方法表(方法区) PPT162 方法重写的本质 PPT_171 虚方法表 PPT_172
虚方法表会在类加载的链接阶段被创建并开始初始化 类的变量初始值准备完成之后 JVM会把该类的方法表也初始化完成 2.2.4.4栈帧组成–方法返回地址(return Address) PPT_181 2.2.4.5虚拟机栈面试题 2.3堆Heap(线程共享) 2.3.1堆的核心概述
堆区:
2.3.3OOM(OutOfMemoryError)举例 代码 2.3.4新生代与老年代 相关参数设置视频P71 默认
为新对象分配内存是一件非常严谨和复杂的任务 JVM的设计者们不仅需要考虑内存如何分配 在哪里分配的问题主要过程 1.new的对象先放在堆中的新生代中的Eden区中 (有特殊情况 当对象过大时 直接放进老年区) 此区有大小限制 2.当Eden区的内存满了 程序又创建了对象 JVM的垃圾回收器将对Eden区进行垃圾回收(Minor GC) 将Eden区中的不再被其他对象所引用的对象进行销毁 然后将剩余的对象移动到一个空的Survivor1区中 (这时给幸存下来的对象都给一个年龄为1 如果下次再次垃圾回收 这个对象还没有被销毁被放进了另一个幸存者区 这个年龄会继续增加) 然后就把这个新创建的对象放到Eden区中 3.如果再次触发垃圾回收 此时幸存下来的放到Survivor1区的对象放到Survivor0区中 4.如果再次经历垃圾回收 此时会重新放到Survivor1区 5.当这些对象一直没有被回收 年龄等于15的时候 这些对象就会去老年区 6.老年区发生的GC的次数一般会少 当养老区内存不足的时候 就会触发Maior GC 进行老年区的内存清理 7.在老年区执行了Maior GC之后发现依然无法进行对象的放入 就会产生OOM异常 注:新生代的三个区 Eden区 S0 S1区 只有Eden区满的时候才会触发Minor GC S0和S11区满的时候不会触发Minor GC 2.3.5.1GC的简单概述 JVM在进行GC时 并非每次都对 新生区(包括Eden区 S0区 S1区) 老年区 方法区一起回收 大部分时候回收都是指新生代 针对HotSpot VM的实现 它里面的GC按照回收区域又分为两大中类型:
年轻代GC(Minor GC)触发机制:当年轻代空间不足时 就会触发Minor GC 这里的年轻代满 值得是Eden区满 Survivor区满不会引发GC 每次MinorGC会清理年轻代的内存 Minor GC非常频繁 一般回收速度也比较快 但是 Minor GC会引发STM 暂停其他用户的线程 等垃圾回收结束 用户线程回复运行 老年代GC(Major GC)触发机制
Full GC触发机制调用System.gc()时 系统建议执行Full GC 但是不必然执行 老年代空间不足 方法区空间不足 通过Minor GC 后进入老年代的平均大小小于老年代的可用内存 Full GC是开发或调优尽量避免的 这样暂停时间会短一些 2.3.6为对象分配内存TLAB(Thread Local Allocation Buffer) 堆区是线程共享区域 如何线程(一个进程中的)都可以访问到堆区的共享区域 由于对象实例的创建在JVM中非常频繁 因此在并发环境下从堆区中划分内存空间是线程不安全的 这里的不安全指的是不同线程占用相同的内存地址 为避免多个线程操作同一地址 需要使用加锁等机制 进而影响分配速度 什么是TLAB?从内存模型而不是垃圾收集的角度 对Eden区继续进行划分 JVM为每个线程在Eden中分配一个私有的缓存区域 (注意:私有的意思是在这个线程创建对象的时候这块区域这个线程专属 其他线程不能占用 但是创建好的对象其他线程可以使用) 多线程同时分配内存时 使用TLAB可以避免这一犀利的非线程问题 同时还能够提升内存分配的吞吐量 因此我们可以将这种内存分配方式称为快速分配策略
2.3.8拓展-堆是分配对象的存储的唯一选择吗_逃逸分析 在《深入理解Java虚拟机》 中关于Java堆内存有这样一段描述:随着JIT编译器的发展与逃逸分析威廉希尔官方网站 的逐渐成熟, 栈上分配 标量替换优化威廉希尔官方网站 将会导致一些微妙的变化 所有的对象都分配到堆上 也渐渐变得不那么绝对了 在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识,但是有一些特殊情况,—那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就有可能被优化成栈上分配,这样就无需在堆上分配内存,也无需进行垃圾回收 这也就是常见的堆外存储威廉希尔官方网站 如何将堆上的对象分配到栈 需要用到逃逸分析手段 什么是逃逸分析:
结论: 开发中能使用局部变量的 就不要使用在方法外定义 2.3.8.1代码优化 1.栈上分配实际上 Hotspot还没有实现栈上分配 之所以优化是因为标量替换+逃逸分析都开启了 主要原因是JVM默认开启标量替换 优化的原因也是因为标量替换 代码分析: public class Test { public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); for (int i = 0; i < 10000000; i++) { create(); } long end = System.currentTimeMillis(); System.out.println("花费的时间为: " + (end - start) + " MS"); TimeUnit.SECONDS.sleep(1000); } //此方法不会发生逃逸 static void create() { A a = new A(); }}class A { } **测试一:**关闭逃逸分析(JVM是默认开启的)使用 -XX:-DoEscapeAnalysis 开启GC的打印细节 经过测试发现 最后一波还没有进行GC堆中A的对象存在 1,885,671 达100多万 并且发生了两次GC 执行时间为70ms 过程关闭了逃逸分析 即所有的Java对象都会在堆上创建 每个方法都是一个栈帧 在循环一千万次创建对象时 每一次都会在堆中创建一个对象 对象的引用在栈的栈帧中 方法调用完栈帧出栈 即创建的对象的引用出栈 即这个对象没有任何一个引用去指向他 当Eden区满了 就会发生Minor GC 那些在栈中没有引用的对象就被回收 只要Eden区满了就会进行GC 当方法调用完了 对象的引用也就没了 所以到最后A的对象都会被回收 测试二: 开启逃逸分析 -XX:+DoEscapeAnalysis 经过测试发现 最后一波还没有进行GC堆中A的对象存在 100,843 只有10万 过程中没有发生过GC 过程由于开启了逃逸分析(JVM默认是开启的) 也就是说不是所有的对象都是在堆中分配的 有的分配在栈中 随着方法的调用完成就出栈了 那些在堆中的对象放入堆中的Eden区的时候空间是够用的 没有发生GC 但是随着线程一直在执行 最终线程产生的其他垃圾会挤满Eden区 就会发生Minor GC 而这些对象都是没有被引用的 所以到最后A的对象会被全部回收 2.同步省略视频_P84 3.标量替换标量(Scalar)就是指一个无法再分解成更小的数据的数据 Java的原始数据类型就是标量 相对的那些还可以分解的数据叫做聚合量(Aggregate) 例如我们写的类 在JIT阶段 如果经过逃逸分析 发现一个对象不会被外界访问的话 那么经过JIT优化 就会把这个对象拆解成若干个其中包含的若干个成员变量来替代 这个过程就是标量替换 标量替换是基于逃逸分析的 也就是说经过逃逸分析如果是一个为逃逸的方法 才会使用标量分析 本次的实验逃逸分析一直开着作为不变 一次开启标量替换**(-XX:+EliminateAllocations)**一次关闭 发现结果也是 相差十倍 但是由上面的栈上分配不是主要开启了逃逸分析就有栈上分配吗 那么为何在我们开启逃逸关闭标量替换的情况下我们还是会相差那么多呢原因就是JVM根本没有使用栈上分配威廉希尔官方网站 我们的栈上分配实验实际上是标量替换引起的!!! 如下图: 2.3.9堆总结 2.4方法区(Method Area) 2.4.1栈 堆 方法区的交互关系 Person person = new Person() Metaspace元空间也叫方法区 2.4.2方法区的理解 《Java虚拟机规范》中明确说明:“尽管方法区在逻辑上是属于堆的一部分 但一些简单的实现可能不会选择去进行垃圾回收或者进行压缩” 但对于HotSpotJVM而言 方法区还有一个别名叫做 Non-Heap(非堆) 目的就是要和堆分开 所以 方法区看作是一块独立于堆的内存空间
(JPS) 查看正在运行中的Java进程号方法区的大小不必是固定的 JVM可以根据应用的需要动态调整 **JDK7及以前** 通过: -XX:PermSize来设置永久代初始分配空间 默认值是20.75M -XX:MaxPermSize来设置永久代最大可分配空间 32位机器默认是64M 64位机器默认是82M 当JVM加载的类的信息容量超过了这个值 会报异常OutOfMemoryError:PermGen space **JDK8及以后**
2.4.4方法区的内部结构 java代码经过编译形成class文件 方法区存储内容描述如下 它用于存储已被虚拟机加载的类型信息 常量 静态变量 即使编译器编译后的代码缓存等 对每个加载的类型(类class 接口interface 枚举enum 注解annotation) JVM必须在方法区中存储一下类型信息 类型信息1.这个类型的完整有效名称 (全名=包名.类名) 2.这个类型直接父类的完整有效名(interface或者是Object类 都没有父类) 3.这个类型的修饰符 4.这个类型直接接口的一个有序列表 属性(Field)信息1.JVM必须在方法区保存类型的所有属性的相关信息以及属性的声明顺序 2.属性的相关信息包括:属性名称 属性类型 属性修饰符 方法信息
non-final的类变量静态变量和类关联在一起 随着类的加载而加载 他们成为类数据在逻辑上的一部分 类变量被类的所有实例共享 即使没有类实例也可以访问 全局常量:static final被声明为final的静态变量的处理方法则不同 每个全局变量在编译的时候就会被分配 2.4.5常量池与运行时常量池 PPT_298 方法区内部包含运行时常量池 2.4.6方法区在JDK1.6 1.7 1.8中的演变细节1.6之前及之前 有永久代(Permanent generation) 静态变量存放在 永久代 1.7有永久代 但已经逐步“去永久代” 字符串常量池 静态变量从永久代转为保存在堆里 1.8永久代移除 替换为**元空间(Metaspace)**类型信息 字段 方法 (static final)常量保存在本地内存的元空间 但==字符串常量池 静态变量==仍在堆 2.4.7永久代为什么要被元空间替换随着Java8的到来 HotSpot VM中再也见不到永久代了 替换为元空间(Metaspace) 内存也有虚拟机内存变为了与堆不相连的本地内存 这项改动很有必要 原因是: 1.为永久代设置空间大小是很有必要的 在某些场景下 如果动态加载的类过多 容易产生永久代的OOM 比如某个实际的Web工程中 因为功能点比较多 在运行过程中 要不断动态加载很多类 经常出现致命错误 而元空间与永久代之间最大的区别在于:元空间并不在虚拟机中 而是使用的本地内存 因此默认情况下 元空间的大小仅受本地内存限制 2.对永久代进行调优是很困难的 2.4.8StringTable(字符串常量池为什么要调整)由方法区的运行时常量池直接放进了堆中JDK7以前字符串常量池在方法区的运行时常量池中 JDK7将字符串常量池放在了堆空间中 因为**永久代(以前的永久代与堆中连接)**的回收率很低 在full GC 的时候才会触发 而full gc是老年代的空间不足 永久代不足时才会触发 这就导致StringTable 回收效率不高 而我们开发中会有大量的字符串被创建 回收率低 导致永久代内存不足啊 能及时回收 简单来说就是:放在方法区中 发生GC的频率低 但是Stirng字符串的回收是很频繁的 所以就直接放进了堆中 2.4.6静态变量存在于堆中及各种变量的存放位置 Object obj = new Object 对于对象本身来说只只看 public class Test { //这个person引用会跟着Perosn对象的实例存在堆中 因为类的成员变量都在堆中 Person person = new Person(); //jdk8后静态的引用也会存在于堆中 jdk6在之前在方法区 jdk7之后和其他的成员变量一样分配在堆空间 就是这个personStatic static Person personStatic = new Person(); //person引用存在say方法栈帧的局部变量表中 局部变量都在栈中 void say() { Person person = new Person(); } } 2.4.7方法区的垃圾收集 方法区存在垃圾回收 但是回收效果比较令人难以满意 尤其是类型的卸载 条件相当苛刻 方法区的垃圾回收主要回收两部分内容:常量池中废弃的常量和不再使用的类型 方法区的常量池中主要存放的两大类常量 :字面量和符号引用 字面量如文本字符串 被声明为final的常量值等 2.4.8面试题及总结 视频_P101 3.本地方法接口 本地方法栈(线程私有) 本地(Native)方法是用C语言实现的 它的具体做法是Native Method Stack(本地方法栈)中登记native方法 在Execution Engine(执行引擎)执行时加载本地方法库 4.对象的实例化内存布局与访问定位 4.1对象的实例化 对象的创建过程
内存布局: 1.对象头: 1.运行时数据区(一些对象本身的信息)
2.实例数据 3.对齐填充 public class Customer { int id = 1001; String name = "xxx"; Account acct = new Account(); public static void main(String[] args){ Customer cust = new Customer(); } }class Account{ } 当在main方法中new了一个Customer对象时 首先在main方法的栈帧的局部变量表中 有两个变量 一个是args 一个是cust这个引用 指向了堆空间中的对象 在堆空间中 这个对象包含了运行时元数据存储哈希值 锁等信息 类型指针指向了这个对象在方法区的类的元信息 还有这个类的实例数据 (id:1001 name:(因为字符串常量池的原因)name指向了字符串常量池中的值 acct:指向了Account的实例对象) Account的实例对象结构都一样 它也有一个类型指针指向了方法区的Account的元信息 根据代码表示类中各个信息的占用位置 4.3对象的访问定位 JVM是如何通过栈帧中的对象引用访问到其内部的对象实例的呢? 通过栈帧中的引用存的是对象的地址指向了这个对象 对象的对象头里有元数据指针指向了方法的类信息 访问对象的方式主要有两种:句柄访问: 在Java堆里面维护一个句柄池 引用指向这个句柄池 句柄池中有两个指针一个指向对象 一个指向方法区中对象类型数据 直接指针: Hospot虚拟机默认是使用的直接指针 栈帧中的引用直接指向对象 在对象的对象头中有一个类型指针指向了方法区的对象的类的信息 4.4直接内存
- Java的NIO库允许Java程序使用直接内存 用于数据缓冲区
使用NIO时 如下图 操作系统划出的直接缓冲区可以被Java代码访问 只有一份 NIO适合对大文件操作 5.执行引擎 5.1执行引擎概述
工作过程 5.2Java代码编译和执行的过程 问题:什么是解释器(Interpreter) 什么是JIT(Just In Time)编译器(即时编译器)解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行 将每条字节码文件中的内容翻译为对应平台的本地机器指令执行 JIT编译器:就是Java虚拟机将class文件直接编译成和本地机器平台相关的机器语言 为什么说Java是半编译半解释型语言JDK1.0时代 将Java语言定位为解释执行还是比较准确的 在后来 Java也发展出可以直接生成本地代码的编译器 现在JVM在执行Java代码的时候 通常都会讲解释执行与编译执行二者结合起来 5.3机器码 指令 汇编语言 机器码
5.4解释器 解释器就是逐条解释字节码变为机器指令然后执行程序 解释器真正意义上所承担的角色就是一个运行时的翻译者 将字节码文件中的内容翻译为对应平台的本地机器指令执行 当下一条字节码指令被解释执行完成后 接着在根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作 由于解释器在设计和实现上非常简单,因此除了Java语言之外,还有许多高级语言同样也是基于解释器执行的,比如Python、Perl、Ruby等。但是在今天,基于解释器执行已经沦落为低效的代名词,并且时常被一些C/C++程序员所调侃 为了解决这个问题,JVM平台支持一种叫作即时编译的威廉希尔官方网站 。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。 不过无论如何,基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献。 5.5JIT编译器 Java代码的执行分类 第一种是将源代码编译成字节码文件 然后在运行时通过解释器将字节码文件转换为机器码执行 第二种是编译执行(直接编译成机器码) 现代虚拟机为了提高执行效率 会采用即时编译威廉希尔官方网站 将方法编译成机器码后字后执行 简单来说就是 解释器时逐条编译逐条执行 编译器是把找到所有代码热点都编译后在执行 热点代码探测 PPT_398 方法调用计数器 回边计数器 6.StringTable(字符串常量池) 美团文章关于intern()JDK6 7 的不同 https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html Java8字符串常量池 https://blog.csdn.net/qq_45737068/article/details/107149922 1.8字符串底层是char数组 1.9底层是byte数组 1.final /* 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改; 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。*/ final String str = "a"; str = "x"; //报错 因为这个str用了final修饰 就不能在去引用其他的字符串 2.字面量创建 与 new的区别 //使用字面量创建 会在字符串常量池中创建 String str = "字符串"; //使用new String("xx")来创建字符串 实际创建了两个对象 一个在堆中 一个在常量池 String str1 = new String("字符"); 2.1String的两种构造器的不同 //这种创建会在常量池中生成一个对象 String str1 = new String("字符"); //这种不会 因为在编译期对象不确定 只会在堆空间中创建一个对象 char[] c = new char[]{'中','国','人'}; String str2 = new String(c); 3.连接符的使用 //4.字符串拼接 + 的底层 String str1 = "aa"; //常量池中存一个aa 返回引用 String str2 = "bb"; //常量池中存一个bb 返回引用 String str7 = "aabb"; //常量池中存一个 aabb 返回引用 //这个在前端编译的时候会优化 直接在常量池中就是 aabb 但是常量池中没有aa 和 bb String str3 = "aa" + "bb"; //只要带上了变量进行+的操作 底层都是使用了StringBuilder() /*StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(1); stringBuilder.toString()*/注意这里的toString底层是使用了new String(char[] c)的这个构造器所以说字符串拼接完之后不会放进常量池中 因为最终使用了new String(char[] c) 所以说这这种类型的String在堆中 不指向常量池 即下面三个都是false String str4 = "aa" + str2; String str5 = str1 + "bb"; String str6 = str1 + str2; System.out.println(str7 == str3); //true System.out.println(str7 == str4); //false System.out.println(str7 == str5); //false System.out.println(str7 == str6); //false 4.intern的使用 String 参考代码 JVM**/StringTable/**zongjie String去重 视频P133 7.内存的分配与垃圾回收概述 面试题:视频135 垃圾是什么? 垃圾是指在运行程序中没有任何指针指向的对象 这个对象就是需要被回收的垃圾 内存泄露: 本身不用了 但是还没办法进行垃圾的回收 应该关心哪些区域的回收 [tr]运行时数据区是否有GC是否有OOM是否有栈溢出[/tr]
从次数上讲
8.1垃圾标记阶段 8.1.1引用计数算法 什么是引用计数算法引用计数算法**(Reference Counting)**比较简单 对每个对象保存一个整型的引用计数器属性 用于记录对象被引用的情况 对于一个对象A 只要有任何一个对象引用了A 则A的引用计数器及加1 当引用失效时 引用计数器就减1 只要对象的引用计数器的值为0 即表示对象A不可能再被使用 可进行回回收 优点:实现简单 垃圾对象便于辨别 判定效率高 回收没有延迟性 缺点:
循环引用一个引用p指向了一个对象next1 next1对象里有一个引用指向了next2 next2对象里有next3的引用指向了next3 next3中的有一个引用又指向了next1 这时next1的引用计数器为2 next2和next3的引用计数器都为1 这时如果栈中的引用p指向了null 这时next1的计数器减1变为1 但是这三个对象已经没有引用指向了 但是引用计数器没有变为0 这时就会出现无法被回收 导致内存泄露 总结引用计数算法 是很多语言的资源回收选择 例如Python 它更是同时支持引用计数和垃圾收集机制 集体使用哪种最优要看场景 业界有大规模实践中仅保留引用计数机制 以提高吞吐量的尝试 Java并没有选择引用计数 是因为他存在一个基本的难题 就是就是难以处理循环引用的问题 Python如何解决循环引用?
PPT_77 视频P_141 8.2对象的finalization机制
永远不要主动调用某个对象的finalize()方法 应该交给垃圾回收机制调用 理由包括下面三点
由于finalize()方法的存在 虚拟机中的对象一般处于三种可能的状态如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:
8.3垃圾清除阶段 PPT_2.95 当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。 8.3.1标记-清除(Mark-Sweep)算法 当堆中的有效空间(available memory)被耗尽的时候 就会停止整个线程(Stop The World) 然后进行两项工作 第一项是标记 第二项则是清除
这里所谓的清除并不是真的置空 而是把需要清除的对象地址保存在空闲的地址列表里 下次有新对象需要加载时判断垃圾的位置空间是否足够 如果够 就直接存放 等于是覆盖 8.3.2复制(copying)算法 核心思想:将内存空间一分为二A B 每次只使用其中的一块A 在垃圾回收时将正在使用A的内存中的存活对象复制到另一块空间中到B去 之后清除A的空间 交换两个内存的角色 最后完成垃圾回收 适用于有大量的垃圾的内存空间使用 挪动的对象少 效率高 优点:
在新生代 对常规应用的垃圾回收 一次通常可以回收70%-90%的内存空间 回收性价比高 所以现在的商业虚拟机都是 用这种收集算法回收新生代 8.3.3标记-压缩(整理)(Mark-Compact)算法 复制算法的高效性是建立在存活对象少 垃圾对象多的前提下 这种情况在新生代经常发生 而在老年代 更常见的情况大部分对象都是存活对象 如果依然使用复制算法 由于存活对象多 复制的成本也搞 因此 基于老年代垃圾回收的特性 需要使用其他的算法 标记清除算法的确可以应用到老年代 但是该算法不仅执行效率低 而且执行完还会产生内存碎片 所以JVM的设计者在此基础上进行了改进 标记-压缩算法由此诞生 标记压缩算法的最终效果就等同于 标记清除算法执行完成后 再进行一次内存碎片整理 二者的本质差异在于标记-清除算法是一种非移动式的回收算法 标记-压缩是移动式的 是否移动回收后的存活对象是一项优缺点并存的风险决策 可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。 缺点:
8.3.4三种算法对比 8.3.5分代收集算法 PPT_2.114
老年代:标记清除与标记整理混合实现 8.3.6增量收集算法 分区算法 PPT_p115 增量算法基本思想: 如果一次性将所有的垃圾进行处理 需要造成系统长时间的停顿 那么就可以让垃圾收集线程和应用程序线程交替执行 每次 垃圾收集线程只收集一小片区域的内存空间 接着切换到应用程序线程 依次反复 直到垃圾收集完成 总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法 增量收集算法通过对线程间冲突的妥善处理 允许垃圾收集线程以分阶段的方式完成标记 清理 或复制工作 缺点: 使用这种方式 由于在垃圾回收过程中 间断性的还执行了应用程序代码 所以能减少系统的停顿时间 但是 因为线程切换和上下文转换的消耗 会使得垃圾回收的总成本上升 造成系统吞吐量的下降 分区算法一般来说,在相同条件下,堆空间越大,一次Gc时所需要的时间就越长,有关cc产生的停顿也越长。为了更好地控制cc产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次Gc所产生的停顿。 分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。 每一个小区间都独立使用,独立回收。这种算法的好处是可以控制 -次回收多少个小区间 9.垃圾回收相关概念 9.1System.gc的使用 在默认情况下 通过System.gc() 或者Runtime.getTuntime().gc()的调用 会显示触发Full GC 同时对老年代和新生代进行回收 然而 System.gc() 调用附带一个免责声明 就是说我调用了这个方法 但是你回不回收是你的事 只是提醒JVM的垃圾回收器执行GC 但是不确定是否马上执行 System.runFinalization()强制调用对象的finalize()的方法 手动GC理解对象不可达视频P157 PPT_2.127内存问题代码 9.2内存溢出 JavaDoc对OutOfMemoryError的解释是 没有空闲内存 并且垃圾收集器也无法提供更多的内存 在抛出OOM之前 通常会进行GC 尽其所能去清理出空间 当然 也不是任何情况下垃圾收集器都会被触发 比如 我们去分配一个超大对象 类似一个超大数组超过堆的最大值 JVM判断出 GC并不能解决 这个问题 所有直接抛出OOM 9.3内存泄露 严格来说 :只有对象不会再被程序用到 但是GC又无法回收的情况才叫内存泄露 但实际情况很多时候我们的程序会导致对象的生命周期变长 导致OOM 也可以叫做宽泛意义上的内存泄露 举例:
9.4安全点与安全区域 视频P163 PPT_145 9.5Java各种引用 强引用(StringReference): 最传统的 “引用” 的定义 是指在代码中普遍存在的引用赋值 就相当于Object object = new Object(); 这种引用无论什么情况下只要强引用关系还存在 垃圾收集器就永远不会回收掉被引用的对象 软引用(SoftReference): 在系统将要发生内存溢出之前 将会把这些对象列入回收范围之中进行第二次回收 如果这次回收后还没有足够的内存 才会排除内存溢出异常 **弱引用(WeakReference):**被弱引用关联的对象只能生存到下一次垃圾回收之前,当垃圾收集器工作时 无论内存空间是否足够 都会回收掉被弱引用关联的对象 **虚引用(PhantomReference) |
|||
|
|||
只有小组成员才能发言,加入小组>>
调试STM32H750的FMC总线读写PSRAM遇到的问题求解?
1979 浏览 1 评论
X-NUCLEO-IHM08M1板文档中输出电流为15Arms,15Arms是怎么得出来的呢?
1761 浏览 1 评论
1233 浏览 2 评论
STM32F030F4 HSI时钟温度测试过不去是怎么回事?
819 浏览 2 评论
ST25R3916能否对ISO15693的标签芯片进行分区域写密码?
1776 浏览 2 评论
2015浏览 9评论
STM32仿真器是选择ST-LINK还是选择J-LINK?各有什么优势啊?
893浏览 4评论
stm32f4下spi+dma读取数据不对是什么原因导致的?
320浏览 3评论
STM32F0_TIM2输出pwm2后OLED变暗或者系统重启是怎么回事?
673浏览 3评论
662浏览 3评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2025-2-24 15:23 , Processed in 0.751065 second(s), Total 44, Slave 38 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191