typora-root-url: img\JVM_img
JVM
1.常见问题
●请你谈谈你对JVM的理解? java8虚拟机和之前的变化更新? ●什么是OOM,什么是栈溢出StackOverFlowError? 怎么分析? ●JVM的常用调优参数有哪些? ●内存快照如何抓取,怎么分析Dump文件? ●谈谈JVM中,类加载器你的认识
2.JVM的位置
JVM 上承开发语言,下接操作系统,它的中间接口就是字节码。 与硬件没有直接交互。
3.JVM的体系结构
99%的JVM调优都是在堆(方法区、堆)中调优,Java栈、本地方法栈、程序计数器是不会有垃圾存在的。
3.1类加载器
作用:加载Class文件
- 虚拟机自带的加载器
- 启动类加载器/根加载器
- 扩展类加载器
- 应用程序加载器
- 双亲委派机制
3.1.1双亲委派机制
双亲委派机制:安全
APP(应用程序加载器)–> EXC(扩展类加载器)—> B0OT(根加载器,在这里执行)
详细步骤:
(1)类加载器收到类加载的请求
(2)将这个请求向上委托给父类加载器去完成,一 直向上委托,直到启动类加载器。
(3)启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则, 抛出异常,通知子加载器进行加载。
(4)重复步骤3,一次一次向下检查,找不到抛出Class Not Found异常。
3.1.2沙箱安全机制
沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。
组成沙箱的基本组件:
(1)字节码校验器(bytecode verifier)
确保Java类文件遵循Java语言规范。可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
(2)类裝载器(class loader)
- 它防止恶意代码去干涉善意的代码;
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
(3)存取控制器(access controller)
存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
(4)安全管理器(security manager)
是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
(5)安全软件包(security package)
java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括: 安全提供者 消息摘要 数字签名 加密 鉴别
3.2PC寄存器
程序计数器(PC寄存器)
- JVM中每个线程都有自己的PC寄存器和Java栈。
- PC寄存器表示指令在主存中的地址,每执行一条指令后PC+1,即指向下一条指令。
每个线程都有一个程序计数器,是线程私有的,就是一个指针, 指向方法区中的方法字节码(用来存储指向像一条指令的地址, 也即将要执行的指令代码),在执行引擎读取下一条指令, 是一个非常小的内存空间,几乎可以忽略不计。
3.3Java栈
栈内存,主管程序的运行,生命周期和线程同步; 线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就Over。
- Java栈代表线程在执行过程中的所有方法调用信息,JVM对栈只有压入栈桢和弹出栈桢两个操作,“栈”由"栈桢“组成,线程每调用一次方法JVM即会为它产生并压入一个“栈桢”,方法执行完毕即会弹出对应的栈桢。
- 局部变量和操作数栈的大小在编译时计算出来,并放置到class文件中,然后JVM就可以知道方法栈桢的大小,当调用一个方法时jvm将压入一个适当大小的栈桢至栈中,但栈在jvm中是有深度限制的,当线程调用的栈深度超出该限制时将抛出StackOverflowError异常,如果jvm在扩展栈时无法获取更多内存则会抛出OutofMemoryErro。
**栈帧:**每一个线程都会对一个虚拟机栈线程中的每个方法都会创建一个栈帧。可以理解为一个数据结构(数组,map),存放了本次方法执行过程中所需要的所有数据,如果一个线程中有多个方法的调用,发挥虚拟机栈的作用,先进后出,后进先出,会对栈帧进行压栈操作,方法的调用就类似于栈帧的压栈和出栈。 当前正在执行的方法,它的栈帧一定在栈顶,我们只能获取到栈顶位置栈帧中的数据。
栈中存储:8大基本类型+对象引用+实例的方法
栈帧图解: 栈底部子帧指向上一个栈的方法 上一个栈的父帧指向栈底部方法。
3.4本地方法栈
Native
- 本地方法栈即代表了线程在执行过程中调用本地方法的一系列信息,与Java栈不同的地方在于,本地方法并不受JVM的限制,对本地方法的调用不会导致JVM往Java栈中压入栈桢。关于本地方法与Java方法的调用可以简单的假设一下,假设某个线程在执行Java方法过程中调用了本地方法C1,且本地方法C1最终又调用了某Java方法,则在这个过程中JVM会先由Java栈进入本地方法栈最终又回到Java栈中,下图简单的描述了这种情况。
- native :凡是带有native关键字的,说明java的作用范围达不到,回去调用底层c语言的库。
- 会进入本地方法栈。
- 调用本地方法本地接口 JNI (Java Native Interface) 。
- JNI作用:开拓Java的使用,融合不同的编程语言为Java所用!最初: C、C++ 。
- Java诞生的时候C、C++为主,必须要有调用C、C++的程序。
- 它在内存区域中专门开辟了一块标记区域: Native Method Stack,登记native方法 。
- 在最终执行的时候,加载本地方法库中的方法通过JNI。
- 例如:Java程序驱动打印机,管理系统,掌握即可,在企业级应用比较少。
Native Method Stack
它的具体做法是Native Method Stack中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies。[本地库]
Native Interface本地接口
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序, Java在诞生的时候是C/C++为主,必须有调用C、C++的程序,于是就在内存中专门开辟了块区域处理标记为native的代码,它的具体做法是在Native Method Stack 中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies。 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service等等。
3.5方法区
- 方法区在设计上也是所有线程共享,主要存储类相关信息(如字段/方法信息、常量池信息、对当前ClassLoader和Class的引用等等),在JVM加载某个类时,会抽取出对应.class文件中类相关的信息并以某种结构(依赖于JVM实现)存到方法区中,当程序运行时,JVM则会到方法区中去查找使用对应类信息(比如创建对象)。
- 由于所有线程共享方法区中的数据,所以方法区中数据的访问必须被设计成线程安全的。另外方法区虽然也被称为“永久代”,但实际上其中的数据也是可以被垃圾回收器回收的,回收内容主要包括常量池中无用的常量、无用的类。
- 虽然类信息具体的存储结构依赖于具体JVM实现,但为了提高方法的检索效率,部分JVM实现会为每个非抽象类生成一个方法表(方法表虽然加快了检索速度,但本身也会占用一定的内存空间,算是以空间换时间),方法表是一种数组结构,每个元素代表一个方法实例(从Java角度来说,每个元素就相当于一个Method对象)。这种情况下,对象不再直接指向方法区中的存放类信息的地址,而是指向方法表,通过方法表来间接关联对象与类型信息。
方法区(Method Area)是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
3.6堆
- Java程序在运行过程中(加载也是运行的一部分)创建的所有对象都位于堆内存中,且JVM中堆内存在设计上是多线程共享的,所以堆中数据的访问必须进行安全控制;
- 运行期间很可能创建大量对象,且大部分对象占用空间小及生命周期短,堆内存的管理尤其重要。但JVM中并没有释放对象的指令,这表示开发中不能通过代码管理对象的释放,所以JVM内置了垃圾回收器来管理,堆也是垃圾回收主要集中的地方(对于栈由于栈桢的大小可以在编译期就根据类结构数据确定,所以这部分的回收具有一定的确定性)。
- 在Java代码中,通过obj.getClass()获取对象所属的类,也可以通过new ClassName()创建一个对象。这是因为在JVM中对象数据包含了一个指向方法区中对应类型信息的指针,可以通过该指针获取对应类信息。反过来JVM也可以根据方法区中类信息创建该类对象,甚至知道该类对象应该占用多少空间(但实际分配大小依赖于JVM实现)。同样通过class.getClassLoader()可以获取当前类加载器也是同样的原理。
- 对象锁synchronized(obj),Java中每个对象都可以作为锁,同样也是因为对象本身包含了一个指向锁数据的指针,但由于绝大部分对象的锁都用不到,所以大部分JVM的实现,都只在第一个线程尝试获取对象锁时,才给该对象分配对应的锁数据。
一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把什么东西放到堆中? 类, 方法,常量,变量,保存我们所有引用类型的真实对象。
堆内存中还要细分为三个区域: ●新生区(伊甸园区) Young/New ●老年区old ●永久区Perm
新对象进入伊甸园区,经过垃圾回收(轻GC)后没有被回收,进入幸存区。幸存0区和幸存1区每次清理都会交换位置。当幸存区已满,进行垃圾回收(重GC),存活下来的对象进入老年区。当新生区养老区都满了,堆溢出异常。
(1)新生区
- 类 诞生、成长、死亡的地方。
- 伊甸园,所有的对象都是在伊甸园区new出来的。
- 幸存者区(0,1)
(2)老年区
存放幸存区满,在重GC下存活的对象。
(3)永久区
永久区常驻内存。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境这个区域不存在垃圾回收,关闭虚拟机就会释放内存。
- jdk1.6之前:永久代,常量池是在方法区;
- jdk1.7 :永久代,但是慢慢的退化了,去永久代,常量池在堆中
- jdk1.8之后:无永久代,常量池在元空间
元空间:逻辑上存在,物理上不存在 (因为存储在本地磁盘内) 所以最后并不算在JVM虚拟机内存中。
3.6.1 堆内存调优
测试代码:
//OOM异常
public class Test {
public static void main(String[] args) {
String s = "";
while (true){
s += s+"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ;
}
}
}
/*
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at Test.main(Test.java:6)
*/
启动配置:
结果:
一开始轻GC,新生区内存不足开始重GC,存储到老年区,重复步骤,直到新生区老年区都满了,抛出OOM异常。
3.6.2 JPofiler工具
在一个项目中,突然出现了OOM故障,那么该如何排除研究为什么出错:
- 能够看到代码第几行出错:内存快照分析工具,MAT, Jprofiler。
- Dubug, 一行行分析代码。
MAT, Jprofiler作用:
- 分析Dump内存文件,快速定位内存泄露;。
- 获得堆中的数据。
- 获得大的对象。
Jprofiler使用:
安装Jprofiler JProfiler12 最新使用教程_刘子平的博客-CSDN博客_jprofiler12注册码
启动配置:-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError
启动后根目录出现 java_pid18768.hprof
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tUQIJ384-1640151944386)(Jprofiler结果.png)]
3.7执行引擎
- 执行引擎负责字节码指令的执行,方法的字节码流由一系列有序指令组成,指令又由一个单字节的操作码 + 0个或多个操作数组成。操作码表示需要执行的操作,操作数表示操作的数据,一般来源于当前栈桢中的局部变量或当前Java栈桢中操作数栈的顶部,至于操作数的个数,由操作码决定(操作码本身就决定了它是否需要操作数,以及操作数的形式等等)。
- 不同的JVM中执行引擎也可能不同,最简单同时效率也最低的执行引擎是一次性解释字节码,它在每次运行方法时都把字节码翻译成本地代码再执行; 其次是即时编译(JITC),它在第一次执行方法时,会把对应的字节码翻译成本地机器代码并缓存,后续调用就可以重用缓存的本地机器代码;另外一种是自适应优化器(特殊的即时编译器), JVM一开始也会解释字节码,但它会监视程序的活动,并记录活动过程中使用最频繁的代码,然后把这些代码编译成本地代码,而其它代码则继续采用解释的方式。
4.对象实例化过程
一个对象实例化过程:
Person p = new Person();
1.JVM 会读取指定的路径下的 Person.class 文件,并加载进内存,并会先加载 Person 的父类(如果有直接的父类的情况下)。
2.在堆内存中的开辟空间,分配地址。
3.并在对象空间中,对对象中的属性进行默认初始化。
4.调用对应的构造函数进行初始化。
5.在构造函数中,第一行会先到调用父类中构造函数进行初始化。
6.父类初始化完毕后,在对子类的属性进行显示初始化。
7.在进行子类构造函数的特定初始化。
8.初始化完毕后,将地址值赋值给引用变量
classFu{
Fu(){
super
show();
return
}
void show(){
System.out.println("fu show");
}
}
class Zi extends Fu{
int num = 8;
Zi(){
super
//-->通过 super 初始化父类内容时,子类的成员变量并未显示初始化。
//等 super() 父类初始化完毕后,才进行子类的成员变量显 示初始化。
System.out.println( println("zi cons run...."+num);
return;
}
void show(){
System.out.println( println("zi show..."+
}
}
class ExtendsDemo5{
public static void main(String[] args){
Zi z = new Zi();
z.show();
}
}
5.三种JVM
- Sun公司HotSpot Java Hotspot™ 64-Bit Server VM (build 25.181-b13,mixed mode)
- BEA JRockit
- IBM J9VM
我们学习都是: Hotspot
6.GC:垃圾回收
JVM在进行GC时,并不是对这三个区域统一回收。 大部分时候,回收都是新生代。
- 新生代
- 幸存区(form,to)
- 老年区
GC两种类:轻GC (普通的GC),重GC (全局GC)
GC常见面试题:
(1)JVM的内存模型和分区,详细到每个区放什么? [3]
- 方法区:储存已被JVM加载的类信息、常量、静态变量、即时编译后的代码等。
- 堆:存放对象实例。包括新生区、老年区、永久区。
- java栈:每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。
- 程序计数器(PC寄存器):属线程私有内存区域,占一小块内存区域,用于指示当前执行字节码的行号,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 本地方法栈:存储本地native方法。
(2)堆里面的分区有哪些? [3.6]
Eden, form, to, 老年区。
(3)GC的算法有哪些?
引用计数器法,标记清除法,标记整理法,复制算法。
引用计数器法(java未采用):
定义:
- 给每个对象分配一个计算器,当有引用指向这个对象时,计数器加1,当指向该对象的引用失效时,计数器减一。最后如果该对象的计算器为0时,java垃圾回收器会认为该对象是可回收的。
优点:
- 实时性:无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
- 应用无需挂起:在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报outofmember 错误。
- 区域性:更新对象的计数器时,只是影响到该对象,不会扫描全部对象
缺点:
-
每次对象被引用时,都需要去更新计数器,有一点时间开销。另外无法解决循环引用问题:
//虽然a和b都为null,但是由于a和b存在循环引用,这样a和b永远都不会被回收。 class TestA{ public TestB b; } class TestB{ public TestA a; } public class Main{ public static void main(String[] args){ A a = new A(); B b = new B(); a.b=b; b.a=a; a = null; b = null; } }
-
浪费cpu,即使内存够用,仍然在运行时进行计数器的统计
标记清除法:
定义:
将垃圾回收分为2个阶段,分别是标记和清除。
- 标记:从根节点开始标记引用的对象。
- 清除:未被标记引用的对象就是垃圾对象,可以被清理。
有效内存空间耗尽时,JVM将会停止应用程序的运行并开启GC线程,然后开始进行标记工作。所有对象标志位默认为0,按照根搜索算法,从root开始搜索,可达到的对象标为1,标记结束后,清除标志位0的对象。
优点:
- 解决了引用计数法中的循环引用的问题,没有从root节点引用的对象都会被回收。
缺点:
- 效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。
- 通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所有清理出来的内存是不连贯的。
复制算法:
定义:
将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。即幸存区to,幸存区from。
优点:
使每次都是对整个半区进行内存回收,内存分配时不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:
将内存缩小为了原来的一半,代价太高。
由于老年代中对象存活率较高,而且找不到其他内存进行分配担保,所以老年代一般不能直接选用这种收集算法。
标记整理法:
定义:
根据老年代的特点,有人对“标记 - 清除”进行改进,提出了“标记 - 整理”算法。“标记 - 整理”算法的标记过程与“标记 - 清除”算法相同,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
针对老年代进行回收,针对回收效率不高,回收的垃圾较小的情况。
优点:
- 解决内存碎片问题。
缺点:
- 整理阶段,由于移动了可用对象,需要去更新引用。
(4)轻GC和重GC分别在什么时候发生?
轻GC:新生区发生的GC,一般出现在伊甸区满,对伊甸区和幸存0区分析,将活跃对象复制到1区,清空伊甸区和0区。
重GC:Eden区已满,开始轻GC,轻GC后内存仍然不足,存储到老年区,老年区内存不足,触发重GC,放得下成功,否则抛出OOM异常。
总结:
内存效率:复制算法 > 标记清除算法 > 标记整理算法
内存整齐度:复制算法 = 标记整理算法 > 标记清除算法
内存利用率:标记整理算法 = 标记清除算法 > 复制算法
年轻代:存活率低,复制算法。
老年代:区域大,存活率高,标记清除+标记整理混合。
参考视频:
【狂神说Java】JVM快速入门篇_哔哩哔哩_bilibili
参考博客:
狂神jvm复习笔记_byteyoung-CSDN博客_狂神jvm笔记
JVM(一):JVM体系结构详解_天涯明月-CSDN博客_jvm体系结构
一个对象实例化的过程(细节)_Noah_yang的博客-CSDN博客_一个对象实例化的过程
java垃圾回收算法之-引用计数器_Sam_Deep_Thinking-CSDN博客_引用计数器
JVM垃圾回收之标记清除法_yanghenan19870513的专栏-CSDN博客_jvm标记清除
垃圾回收机制策略三(标记整理算法)_VRival的博客-CSDN博客_标记整理算法的优缺点
轻gc和重gc分别在什么时候发生_什么是 Minor GC/Major GC_正儿八经的胡说八道的博客-CSDN博客_轻gc和重gc