第一次看到这些真真实实的面试题的时候,我~
这都什么玩意???????
经过一段时间的研究!!接下来,我将以大白话从头到尾给大家讲讲Java虚拟机
!!
不对的地方还请大家指正~
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
百度的解释云里雾里,对于我们Java程序员,说白了就是:
程序
,它能识别.class
字节码文件(里面存放的是我们对.java
编译后产生的二进制代码),并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作!跨平台性
,就是因为JVM,我们可以将其想象为一个抽象层,只要这个抽象层JVM正确执行了.class
文件,就能运行在各种操作系统之上了!这就是一次编译,多次运行
对于JVM的位置:
JDK(Java Development Kit):Java开发工具包
JRE(Java Runtime Environment):Java运行环境
JDK = JRE + javac/java/jar 等指令工具
JRE = JVM + Java基本类库
Java虚拟机主要分为五大模块:
作用:加载
.Class
字节码文件
public class Student {
//私有属性
private String name;
//构造方法
public Student(String name) {
this.name = name;
}
}
//运行时,JVM将Test的信息放入方法区
public class Test{
//main方法本身放入方法区
public static void main(String[] args){
//s1、s2、s3为不同对象
Student s1 = new Student("zsr"); //引用放在栈里,具体的实例放在堆里
Student s2 = new Student("gcc");
Student s3 = new Student("BareTH");
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s3.hashCode());
//class1、class2、class3为同一个对象
Class<? extends Student> class1 = s1.getClass();
Class<? extends Student> class2 = s2.getClass();
Class<? extends Student> class3 = s3.getClass();
System.out.println(class1.hashCode());
System.out.println(class2.hashCode());
System.out.println(class3.hashCode());
}
}
根据结果,我们发现:
我们画图分析以下new一个对象的流程:
.class
文件,加载初始化生成Student模板类
Student模板类
new出三个对象那么Class Loader具体是怎么执行我们的.class
字节码文件呢,这就引出了我们类加载器~
我们编写这样一个程序
根据返回结果,我们来讲解以下三种加载器:
级别从高到底
启动类(根)加载器:BootstrapClassLoader
c++
编写,加载java
核心库 java.*
,构造拓展类加载器
和应用程序加载器
。
根加载器
加载拓展类加载器
,并且将拓展类加载器
的父加载器设置为根加载器
,
然后再加载应用程序加载器
,应将应用程序加载器
的父加载器设置为拓展类加载器
由于引导类加载器涉及到虚拟机本地实现细节,我们无法直接获取到启动类加载器的引用;这就是上面那个程序我们第三个结果为null
的原因。
加载文件存在位置
拓展类加载器:PlatformClassLoader
java
编写,加载扩展库,开发者可以直接使用标准扩展类加载器。ExtClassloader
,Java9以后改名为PlatformClassLoader
应用程序加载器:AppClassLoader
java
编写,加载程序所在的目录默认
的类加载器用户自定义类加载器:CustomClassLoader
java
编写,用户自定义的类加载器,可加载指定路径的class
文件BootstrapClassLoader
举个例子:我们重写以下java.lang包下的String类
发现报错了,这就是双亲委派机制
起的作用,当类加载器委托到根加载器
的时候,String类
已经被根加载器
加载过一遍了,所以不会再加载,从一定程度上防止了危险代码的植入!!
作用总结:
.class
。通过不断委托父加载器直到根加载器,如果父加载器加载过了,就不用再加载一遍。保证数据安全。.class
,如上述的String类
不能被篡改。通过委托方式,不会去篡改核心.class
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个class
对象。这样保证了class
执行安全。这里引用了这篇博文引用链接,了解即可
Java安全模型的核心就是Java
沙箱
(sandbox)
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
在Java中将执行程序分成本地代码
和远程代码
两种
可信任
,可以访问一切本地资源。不可信信
在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。如下图所示
如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。
因此在后续的 Java1.1
版本中,针对安全机制做了改进,增加了安全策略
,允许用户指定代码对本地资源的访问权限。
如下图所示
在Java1.2
版本中,再次改进了安全机制,增加了代码签名
。
如下图所示
当前最新的安全机制实现,则引入了域 (Domain)
的概念。
系统域
和应用域
系统域
部分专门负责与关键资源进行交互应用域
部分则通过系统域的部分代理来对各种需要的资源进行访问。字节码校验器
(bytecode verifier) 确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类(如上述java.lang.String)。
类装载器
(class loader)其中类装载器在3个方面对Java沙箱起作用
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
存取控制器
(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。安全管理器
(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。安全软件包
(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
JNI:Java Native Interface
native
:凡是带native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库!进入本地方法栈,调用本地方法接口JNI
,拓展Java的使用,融合不同的语言为Java所用
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用 Socket通信,也可以使用 Web service等等,了解即可!
程序计数器
: Program Counter Register
线程私有
的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间
,几乎可以忽略不计方法区
:Method Area
共享区间
;Non-Heap(非堆)
,目的应该是与Java 堆区分开来。常量池
例如这个例子中,生成了对应的Person模板类,name常量“zsr”放在常量池中,三个对象的引用放在栈中,该引用指向放在堆中的三个实例对象。
这就是堆、栈、方法区的交互关系
又称
栈内存
,主管程序的运行,生命周期和线程同步,线程结束,栈内存就释放了,不存在垃圾回收
栈帧(Stack Frame)
,每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程。举个例子:
public class Test {
public static void main(String[] args) {
new Test().a();
}
public void a() {
b();
}
public void b() {
a();
}
}
最开始,main()方法压入栈中,然后执行a(),a()压入栈中;再调用b(),b()压入栈中;以此往复,a与b方法不断被压入栈中,最终导致栈溢出
Heap
,一个JVM只有一个堆内存(栈是线程级的),堆内存的大小是可以调节的![]()
实例化的对象
对象诞生、成长甚至死亡的区
Eden Space(伊甸园区)
:所有的对象都是在此new出来的Survivor Space(幸存区)
幸存0区
(From Space
)(动态的,From和To会互相交换)幸存1区
(To Space
)Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1
。
存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭虚拟机就会释放这个区域内存!
名称演变
永久代
永久代
慢慢退化,去永久代
永久代
改名为元空间
注意:元空间在逻辑上存在,在物理上不存在
新生代 + 老年代的内存空间 = JVM分配的总内存
如图所示:
内存溢出
java.lang.OutOfMemoryError
产生原因:
GC垃圾回收,主要在年轻代和老年代
首先,对象出生再伊甸园区
伊甸园区
只能存一定数量的对象,则每当存满时就会触发一次轻GC(Minor GC)
轻GC
清理后,有的对象可能还存在引用,就活下来了,活下来的对象就进入幸存区
;有的对象没用了,就被GC清理掉了;每次轻GC
都会使得伊甸园区
为空幸存区
和伊甸园
都满了,则会进入老年代
,如果老年代
满了,就会触发一次重GC(FullGC)
,年轻代+老年代
的对象都会清理一次,活下的对象就进入老年代
新生代
和老年代
都满了,则OOMMinor GC:伊甸园区满时触发;从年轻代回收内存
Full GC:老年代满时触发;清理整个堆空间,包含年轻代和老年代
Major GC:清理老年代
什么情况永久区会崩?
一个启动类加载了大量的第三方Jar包,Tomcat部署了过多应用,或者大量动态生成的反射类
这些东西不断的被加载,直到内存满,就会出现OOM
查看我们jvm的堆内存
public class Test {
public static void main(String[] args) {
//返回jvm试图使用的最大内存
long max = Runtime.getRuntime().maxMemory();
//返回jvm的初始化内存
long total = Runtime.getRuntime().totalMemory();
//默认情况下:分配的总内存为电脑内存的1/4,初始化内存为电脑内存的1/64
System.out.println("max=" + max / (double) 1024 / 1024 / 1024 + "G");
System.out.println("total=" + total / (double) 1024 / 1024 / 1024 + "G");
}
}
我们可以手动调堆内存大小
在VM options
中可以指定jvm试图使用的最大内存
和jvm初始化内存
大小
-Xms1024m -Xmx1024m -Xlog:gc*
利用上述方法指定
jvm试图使用的最大内存
和jvm初始化内存
大小
内存快照工具:
作用:
Dump文件是
进程
的内存镜像
,可以把程序的执行状态
通过调试器保存到dump文件中
举个例子
import java.util.ArrayList;
public class Test {
byte[] array = new byte[1024 * 1024];//1M
public static void main(String[] args) {
ArrayList<Test> list = new ArrayList<>();
int count = 0;
try {
while (true) {
list.add(new Test());
count++;
}
} catch (Exception e) {
System.out.println("count=" + count);
e.printStackTrace();
}
}
}
运行该程序,报错OOM
接下来我们设置以下堆内存,并附加生成对应的dump文件
的指令
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
-XX:+HeapDumpOnOutOfMemoryError
表示当JVM发生OOM时,自动生成DUMP文件。再次点击运行,下载了对应的Dump
文件
一直点击上级目录,直到找到.hprof
文件,与src
同级目录下
点击Thread Dump
,里面是所有的线程,点击对应的线程可以看到相应的错误,反馈到具体的行,便于排错
每次打开Dump文件查看完后,建议删除
,可以在idea中看到,打开文件后生成了很多内容,占内存,建议删除
下载客户端 https://www.ej-technologies.com/download/jprofiler/files
安装客户端
选择自定义安装,注意:路径不能有中文和空格
这里name和Company任意,License Key大家可以寻找对应版本的注册机获得
后续默认,安装成功即可!!!
安装完成后,重启IDEA,可以看到我们的内存快照工具
打开IDEA的设置,找到Tools里面的JProfiler,没有设置位置则设置位置
在12.4中,我们已经对GC的流程进行了大概的讲解,这里做一些总结:
JVM在进行GC时,并不是对年轻代
、老年代
统一回收;大部分时候,回收都是在年轻代
GC分为两种:
每个对象在创建的时候,就给这个对象绑定一个计数器。
每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。
这样,当没有引用指向该对象时,该对象死亡,计数器为0,这时就应该对这个对象进行垃圾回收操作。
复制算法主要发生在年轻代
( 幸存0区
和 幸存1区
)
轻GC
,每触发一次,活的对象就被转移到幸存区,死的就被GC清理掉了,所以每触发轻GC
时,Eden区就会清空;From Space
和To Space
,这两块区域是动态交换的,谁是空的谁就是To Space,然后From Space
就会把全部对象转移到To Space
去;复制算法
,其中一个区域会将存活的对象转移到令一个区域去,然后将自己区域的内存空间清空,这样该区域为空,又成为了To Space
;轻GC
后,Eden区清空,同时To区也清空了,所有的对象都在From区这也就是
幸存0区
和幸存1区
总有一块为空的原因
好处:没有内存的碎片(内存集中在一块)
坏处:
最佳使用环境:对象存活度较低的时候,也就是年轻代
为每个对象存储一个标记位,记录对象的生存状态
缺点:两次扫描严重浪费时间,会产生内存碎片
优点:不需要额外的空间
标记-整理法
是 标记-清除法
的一个改进版。
又叫做 标记-清楚-压缩法
可以进一步优化,在内存碎片不太多的情况下,就继续标记清除
,到达一定量的时候再压缩
内存(时间复杂度)效率:复制算法 > 标记清除算法 > 标记压缩算法
内存整齐度:复制算法 = 标记压缩法 > 标记清除法
内存利用率:标记压缩法 = 标记清除法 > 复制算法
没有最优的算法,只有最合适的算法
GC 也称为 分代收集算法
对于年轻代:
对于老年代:
标记清除+标记压缩
混合实现