JVM在计算机中所处位置
JVM体系结构
类加载器
概念
负责加载class文件,class文件在文件开头有特定的标示(不会加载任意文件),将class文件字节码内容加载进内存进行解析校验初始化,最后形成可以被虚拟机直接执行的文件,类加载器只负责加载类,不负责运行
启动类加载器(Bootstrap),C++编写
主要加载JDK原生自带的类(Object、String、ArrayList等),提供环境支持,如下图所示(Jdk的jre文件)
问题
在main入口程序能获取object的类加载器吗?
答:不可以,因为object属于jdk自带的类,加载时走的是Bootstrap加载器,本身就已经加载完毕,所以没有类的加载器
扩展类加载器(Extension),Java编写
由于java需要与时俱进,避免被淘汰,所以每次都需要在原有的版本上扩展功能,所以就有一个扩展类加载器
应用程序类加载器(AppClassLoader)
用户自定义类加载器(继承ClassLoader)
用户可以自定义类的加载器,实现方式是继承java.lang.ClassLoader
双亲委派机制保证JDK沙箱安全
概念
当一个类需要被类加载器加载时,不会直接去加载这个类,而是先找到父类加载器,父类加载器没有加载到类,子类的加载器才会进行加载,如果都找不到,就抛出ClassNotFoundException异常
好处
假设在项目中创建了一个与jdk中同包同名的类,比如String,那么这时候就会去找最上层的类加载器中找这个类,如果有则返回,保证一个项目只有一个String类和原生JDK的环境中的类不会被污染
执行引擎
运行时数据区
本地方法栈
Native本地接口
为了融合不同语言的实现,起初Java对C++的依赖比较大,所以专门在内存中开辟了一块区域来处理native代码,将方法的实现交给第三方函数库处理,例如Thread中的start0()方法就是用native修饰的,因为线程和进程属于操作系统在处理,Java语言没有办法直接进行处理,所以需要借助本地方法库调度,简单来说就是专业的事交给专业的人来做,支持异构(比如微服务中的服务互相调用,每个服务可以用不同的语言开发,消费者不需要知道提供者的内部实现,我只需要响应)
PC寄存器
记录方法之间的调用和执行情况,每一线程都有一个程序寄存器, 线程私有,相当与一个指针,指向方法区中的方法字节码(下一条指令的地址,也即将执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计,但如果执行的是native方法,那么这个寄存器就是空的.用于完成分支\循环\跳转\异常处理\线程恢复等基础功能,不会发生内存溢出
注意 String的变量在main方法中传给其他方法进行改变后,main方法中的String变量是否会发生改变? 答:不会,因为String虽然是引用类型,但它有一个常量池,每一次新建一个字符串,它就会在这个常量池里面去找有没有这个字符串,如果没有则会开辟新的空间(地址)创建这个字符串,main方法的寄存器指针指向这个字符串后不会改变,在main中调用的那个方法改变时会发现常量池中没有要更改的字符串,就会创建新的地址,也就是改变了指针位置,而main的指针一直指着原来字符串的地址,所以main方法中的字符串变量不会被改变
方法区
为各线程共享运行时内存区域.它存储了每个类的结构信息,例如运行时常量池(Runtime Constant Pool)\字段\方法数据\构造函数和普通方法的字节码内容,上面讲的是规范,在不同的虚拟机内实现也是不一样的,最典型的就是永久代和元空间.但是实例变量存在堆内存中,和方法区无关
Stack(栈)
栈管运行,堆管存储
队列 (FIFO) 先进先出 例子:排队打饭
栈 (FILO) 先进后出 例子:子弹弹夹
概念
1、也叫栈内存,每个线程运行时所需要的内存称之为栈内存。
2、栈是在线程创建时创建的,它的生命周期跟随着线程的生命周期,线程结束栈内存也就被释放,对于栈来说
3、不存在垃圾回收问题。
4、栈的生命周期和线程一致,是线程私有的。
5、每个栈由一个或多个栈帧(Frame)组成,一个栈帧代表着每个方法每次调用时所占用内存。
6、当一个方法中调用了另一个方法时,那么这个栈中会存放这两个方法的栈帧内存,以此类推。
7、栈的执行过程是:先进后出,后进先出;第一个产生的栈帧会放在最栈底,最后一个产生的栈帧会放在栈口;当所有栈帧执行完时,会从栈口开始释放内存。类似于弹夹中的子弹,第一颗被压进弹夹的子弹是最后射出的。
8、每个线程或者栈只能有一个活动栈帧,也就是正在栈口执行中的栈帧叫做活动栈帧。
栈存储什么?
- 8种基本类型的变量(int\float\double\byte\short\reference…)
- 对象的引用变量
- 实例方法
栈帧
栈帧是什么?
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
简单来说,栈帧对应一个方法的执行和结束,是方法执行过程的内存模型。
栈帧主要保存了3类数据:
- 本地变量(Local Variables):输入参数和输出参数,以及方法内的变量。
- 栈操作(Operand Stack):记录出栈、入栈的操作。
- 栈帧数据(Frame Data):包括类文件、方法等。
栈的大小是根据JVM有关,一般在256K~756K之间,约等于1Mb左右。
栈溢出 StackOverflowError
StackOverflowError是错误,非异常
举个栗子,比如MyObject myObject = new MyObject();等号左边MyObject myObject的myObject就是引用,在Java栈里面。等号右边的new MyObject(),new出来的MyObject实例对象在堆里面。简单来说,就是Java栈中的引用myObject指向了堆中的MyObject实例对象
Heap(堆)
堆内存逻辑上分为:新生+养老+元空间(1.7 永久)
新生区(Eden) | 这里就是类的诞生\成长\消亡的区域,程序不断new对象,而到达一定的阀值时,会进行一次GC(垃圾回收),发现是新生区要满了,称为YGC(Young GC),但是其中有对象在垃圾回收中存活了下来,就将其移入幸存者区 |
---|---|
幸存者1区(S0 | From) | 这里存放上一次GC没有被回收的对象,但每一次GC在回收时会看From区域或To区域谁是空的,谁是空的就与变成To,如果from是空的,to有对象,那么就会进行一次交换.to变成from,from变成to,把to里面的对象拉到from与新生区一起干活 |
幸存者2区(S1 | To) | |
养老区(Old memory) | 养老区享福不会被垃圾回收,但是迟早养老区也会满,开启Full GC,多次回收后,养老区已经实在腾不出空间了,那么程序就会弹出OOM(堆内存溢出) |
元空间(永久区Prem) | 永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会被释放此区域所占用的内存 |
永久区(元空间)
堆参调优
-Xms | 设置初始分配大小,默认为物理内存的1/64 |
---|---|
-Xms | 最大分配内存,默认为物理内存的1/4 |
-XX;+PrintGCDetails | 输出最详细GC处理日志 |
Runtime.getRunTime().maxMemory();//返回java虚拟机试图使用的最大内存量
Runtime.getRunTime().totalMemory();//返回Java虚拟机中的内存总量
Idea调整堆参数方法
JVM的初始内存和最大内存一般怎么配?
答:初始内存和最大内存一定是一样大,理由是避免GC和应用程序争抢内存,进而导致内存忽高忽低产生停顿。
堆GC日志的查看方式
YoungGC(Minor GC)
Full GC(Major GC)
GC(垃圾回收机制)
GC是什么(分代收集算法)
- 次数上频繁回收Young区(Eden)
- 次数上较少回收Old区(Old)
- 基本不动元空间(Prem)
全局GC比普通GC慢的原因
eden+from+to占比1/3,old memory占比2/3,回收的范围要比前三个区域要大,所以回收比较慢
GC4大算法
引用计数法
复制算法
复制算法用在Young(eden)新生区
JAVA8最大15岁
From区和To区交换示例图
红块可以看作幸存对象,黄色是要被清理的对象,绿色是剩余内存空间
缺点
它浪费了一半的内存
如果对象的存活率很高,假设100%对象存活,那么就需要把每个对象都给复制一遍,并且引用地址都会重置一次.复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变得不可忽视.所以以上描述不难看出,复制算法要想使用,最起码对象的存活率要很低,并且要克服50%的内存浪费
标记清除算法
标记清除一般用于老年代(Old Memory)由标记清除或者是标记清除与标记整理的混合实现
即当程序运行期间,若可被使用的内存被消耗殆尽的时候,GC线程就会被触发并将程序暂停,随后将要回收的对象标记一遍,最终统一回收这些对象,完成标记清理工作让应用程序恢复运行
缺点
虽然节约了空间,但需要挑选标记降低了效率,比复制算法慢,会产生内存碎片
标记压缩(标记清除压缩)
就是将标记算法和压缩算法结合,标记清除后对该区进行重新整理,清除内存碎片,但是耗时也是最长的
缺点
小结
不过还有比以上算法都要好的算法,那就是JAVA9以后的默认垃圾回收器GE
没有最好的算法,只有最合适的算法
YoungG
特点 相对老年区域小,对象存活率低
使用复制算法比较合适,,因为这种算法速度最快,并且只和当前存活对象的大小有关.而复制算法内存利用率不高可通过hotsopt中的两个survivor的设计得到缓解
老年区
特点 区域大,对象存活率高
具有存活率高的老年区,明显使用复制算法不太合适,这样导致复制算法一次要复制大量对象.一般就是使用标记清除算法或者标记压缩算法
面试题
- 堆里面的分区:Eden\survival from to,老年代,各自的特点
GC的三种收集方法原理和特点,分别用在什么地方?
复制算法:主要应用在新生区,效率高,因为这里的对象存活率低,复制算法会将eden中和from中的幸存对象复制到to区,然后跟from进行交换身份,这些幸存者继续上场干活 标记清除算法:一般与标记整理算法应用在老年区,因为老年区的幸存率高,但标记清除算法会有内存碎片,并且相对于复制算法效率要低一些 标记压缩算法(标记整理算法):就是与标记清除算法结合的一个算法,解决了标记清除算法的内存碎片问题,但仍然效率较低
Minor GC和Full GC分别在什么时候发生?
Minor GC在新生区达到一定阀值时就会进行新生区的垃圾回收 Full GC是在老年区发现也快挤不下时进行的垃圾回收
JMM
(Java Memory Module)Java内存模型,本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
volatile是Java虚拟机提供的轻量级的同步机制: 1.保证可见性 2.不保证原子性 3.禁止指令重排
volatile修饰符将变量设置为线程间可见
- 可见性
通知机制,线程间变量共享
- 原子性
不可分割就称为原子性,像Mysql的事务
- 有序性
代码按照顺序执行
补充 Hostspot是新的虚拟机,顶替了JIT,提高了Java的性能,Java原先是将源代码编译为字节码,在虚拟机中运行,hostspot将常用的代码编译为常用代码