学习笔记系列开头惯例发布一些寻亲消息
链接:https://baobeihuijia.com/bbhj/contents/3/192489.html
类装载器classLoader:
-
将本地的字节码文件.class 加载到内存方法区中成为元数据模板(两个class对象是否为同一个类要求:完整类名包括包名一致+加载类的classloader要一致)
-
启动类加载器,用于提供JVM自身需要的类(除了启动类加载器,其他都继承自classloader)
-
扩展类加载器:从用户指定目录中加载类
-
应用程序类加载器:加载环境变量/系统属性指定路径下的类库,是程序中默认的类加载器
-
用户自定义类加载器
-
双亲委派机制:
- 按需加载,需要用到该类的时候才会加载到内存中生成class对象
- 向上委托,父类加载失败则由子加载器处理
- 优点:
- 避免类重复加载
- 防止核心api被随意更改【沙箱安全】:自定义string类,在加载的自定义string类的时候会率先使用引导类加载器加载jdk自带的string.class文件,可以保证对java核心源代码的保护
运行时数据区:
-
JAVA内存布局规定了java运行过程中内存申请,分配和管理的策略:
-
每个线程独立拥有程序计数器、栈、本地方法栈
-
线程共享,随虚拟机存亡的:方法区、堆
-
程序计数器
-
(由于cpu一直在线程中切换,利用程序计数器可以记住切回来后继续执行的位置【cpu时间片:cpu分给各个线程的时间段】)
-
pc寄存器用来存储指向下一条指令的地址/如果该线程执行的是native方法,则是未指定值
-
通过pc来控制程序的分支、循环、跳转等
-
字节码解释器就是通过pc来找到下一条即将要执行的字节码指令
-
是jvm中唯一一个没有规定任何outofmemoryerror情况的
-
虚拟机栈(栈帧就类似于寄存器之间的倒换)
- java的指令都是基于栈来设计的,因为不同的平台cpu架构不同,所以不能设计基于寄存器的
- 随着方法的执行,java栈的操作就是对栈帧的入栈还有出栈,不存在垃圾回收问题(之前的基于寄存器的方法执行是通过寄存器来转移操作数据)
- 栈可能出现的异常
- 线程请求分配的栈容量超过java的栈最大容量,就抛出stackoverflowerror异常
- 如果是没有内存来创建或者满足栈要求,就抛出outofmemoryerror异常
- 每个线程都有自己的栈,栈中数据是以栈帧格式存在的,执行引擎运行所有的字节码指令只针对当前栈帧进行,也就是说一个时间点只会有一个活动的栈帧,如果该方法调用了别的方法,那么别的方法的栈帧就会被创建并放到栈顶端,成为当前栈帧
- 栈帧中包括:
- 局部变量表
- 局部变量表所需的大小在编译的时候就写在了字节码文件中,所以大小是固定的
- 以slot为基本存储单位
- 当一个方法被执行引擎调用的时候,该方法的方法参数和内部定义的局部变量都会按照执行顺序被复制到局部变量表中的slot上
- 局部变量表必须进行人为的初始化,没有系统自带的准备阶段初始为0,没有赋值就不能使用
- 操作数栈
- 保存计算过程的中间结果,同时是计算过程中变量的临时存储空间
- 并非使用索引来进行数据访问,而是通过数据的出栈和入栈来完成一次访问
- 栈顶缓存技术:将频繁的栈顶读写缓存到物理cpu中,降低对内存的读写
- 动态链接(指向运行时常量池的方法引用,将符号转化为调用方法的直接引用)
- 静态链接(早期绑定):在编译期可知,且运行期不变,用静态链接直接替换
- 动态链接(晚期绑定):编译期无法确定,要根据程序运行时的实际类型才能确定【比如我们是通过一个变量值>3来判断执行哪个方法,像这样就无法直接替换,就需要在运行时进行动态的链接/支持多态】
- 虚方法表:每次动态分派的过程都要在类的方法元数据中找到合适的目标,因此在类的方法区建立虚方法表使用索引来代替查找
- 方法返回地址
- 存放调用该方法的pc寄存器的值,即调用该方法的下一条指令的地址
- 若是异常退出,则返回地址是要通过异常表来确定
- 局部变量表
Java方法内的局部变量是否线程安全问题_在方法中会产生线程安全问题吗对吗-CSDN博客
本地方法栈
-
调用本地的一些基于C编写的程序时,采用本地方法栈来管理本地方法的调用
-
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
堆
-
是JVM中分配内存最大的一块区域,如果内存大小超过-Xmx指定的最大内存就会抛出outofmemeryerror
-
为什么xms和xmx要设置成相同的值?【精选】jvm调优技巧 - 内存抖动 、Xms和Xmx参数为什么要设置相同的值_xmx和xms为什么要一样-CSDN博客
xms是发现空余堆内存大于阈值,jvm减小直到xms最小
xmx是发现空余堆内存小于阈值,jvm增大直到xmx最大
1、首先,如果-Xms起初值设置的比较小,那么就频繁触发GC操作。当GC操作无法释放更多内存时,会进行内存的扩充。
2、内存扩充的时候,会出现内存抖动的情况
【就比如我上街看到一个很喜欢的商品,想要买下这个商品,但是我身上的钱不够了, 于是我向你借钱,你同意了,然后你掏出钱包, 把钱拿出来,然后再交给我,你拿钱给我的这整个过程也是需要时间的。所以我为了省下这个拿钱的时间,我在一开始出门的时候就直接带上足够的钱,这样就可以省下借钱的时间了】
-
-
为对象分配内存TLAB
- 为避免多个线程操作同一堆区的对象地址,使用TLAB机制,如果失败那就使用加锁机制确保数据操作的原子性
-
对象不一定全部分配在堆上,采用逃逸分析时,即对象只在方法体内使用时,也可以分配到栈中
方法区
- 实际物理内存空间和堆一样可以不连续的,如果方法区定义了太多的类,那么方法区也会outpfmemoryerror
- 在这个类加载阶段包括两部分,一是对静态变量的加载(分为static和final static),二是对类的加载
- 内部结构
- 类型信息:全名(包名.类名),直接父类的完整有效名,修饰符,直接接口
- 域信息
- 方法信息:方法名,方法返回类型,参数,修饰符,字节码,操作数栈,局部变量表以及大小,异常表
- 方法区是运行时常量池、字节码文件内部包含常量池
常量池(字节码)和运行时常量池(方法区)
-
一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。
-
这部分内容在类加载后存放到方法区的运行时常量池中,其中每个已加载的类都会维护一个常量池,通过索引访问
- 编译期就明确数值字面量
- 运行期才能获得方法或者字段引用,此时就不再是常量池中的符号地址了,而是真实地址
大概梳理流程:
【创建轮子,车架】我将我的java文件打包生成字节码文件,当我点击运行的时候,启动类会被主动加载,jvm的类加载器就会
- 加载类中的静态变量到方法区,直接分配内存(可能分配到永久代/堆)
- 加载字节码中的类信息到方法区(按需加载,需要用到该类的时候才会加载到内存),包括类名,域名,方法名,并且将类的常量池放到方法区的运行时常量池中(根据这个信息才能将类完整的刻画出来),将符号引用变为直接的内存地址
【实例化四个轮子,车架】jvm开启线程,主动对启动类进行类的初始化:先进行类初始化,即将方法区/堆区的静态变量进行初始化,执行静态代码块,然后是对象初始化,new的对象放到jvm的堆中,执行赋值语句,普通代码块,最后是构造函数代码。
【组装】jvm在栈中开启一个栈帧记录线程,在运行时加载该方法的局部变量表,利用操作栈对数据进行循环,遍历运算,如果在执行过程中遇到了对象引用,那就去方法区的运行时常量池将符号转化为直接引用,执行完毕后,堆中的对象被清除。
面试题
当在多次minor GC后仍然存活的满足一定存活代数的对象,或者是由于太大无法在新生代中分配的对象
java8之前,两种模式
- 所有的类信息(运行时常量池)和静态变量,字符串常量都放在永久代上
- 只剩类信息(运行时常量池)在永久代 / 静态变量,字符串常量放在堆中
java8之后
- jvm没有永久代的概念,类信息(运行时常量池)放在本地内存上,叫元空间
为什么有这个变动呢?
- 因为随着类加载变多,我们无法定义一个合适的永久代空间,因此放到本地的话,元空间的大小只受限于本地的内存大小
- 原始字符串常量放在永久代中,只有full GC(老年代或者永久代不足)的时候才会被清理,回收效率不高,所以新的改进是放到堆中,跟随对象及时回收内存
行时常量池)放在本地内存上,叫元空间
为什么有这个变动呢?
- 因为随着类加载变多,我们无法定义一个合适的永久代空间,因此放到本地的话,元空间的大小只受限于本地的内存大小
- 原始字符串常量放在永久代中,只有full GC(老年代或者永久代不足)的时候才会被清理,回收效率不高,所以新的改进是放到堆中,跟随对象及时回收内存