JVM初探
一、了解JVM
1.JVM的概述
JVM是Java Virtual Machine(java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,用来执行java字节码(二进制的形式)
JDK(Java Development Kit),它是实际上存在的,它包含JRE+编译、运行等开发工具.
JRE(Java Runtime Environment),它用于提供运行时环境。它是JVM的实现。它是实际存在的。它包含一组系统类库和JVM。
Java平台可分为两部分,即Java虚拟机(Java virtual machine,JVM)和Java API类库
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码,就可以在多种平台上不加修改地运行。
2.JVM的作用
JVM是用来解析和运行Java程序的
Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
3.JVM的分类
- Sun 公司
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)
- BEA
JRockit
- IBM
J9VM
4.JVM的位置
jvm是运行在操作系统之上的,与硬件没有任何关系。
5. JVM的体系结构
二、类加载器
类加载器通常由JVM提供,
作用:类的class文件读入到内存,并为之创建一个java.lang.Class对象
类是模板(抽象的),对象是具体的。
分类:
- 虚拟机自带的加载器
- 启动类(根)加载器(BootstrapClassLoader):主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader
- 扩展类加载器 (ExtClassLoader) : 主要负责加载jre/lib/ext目录下的一些扩展的jar
- 应用程序加载器 (AppClassLoader) : 主要负责加载应用程序的主函数类
三、双亲委派机制
介绍:
-
类加载器收到类加载的请求!
-
将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
-
启动类加载器检查是否能加载这个类,能加载就结束,使用当前的加载器,否则则,抛出异常,通知子类加载器进行加载
-
重复步骤3
-
Class Not found
null : java调用不到,c,c++
Java = C++ --: 去掉繁琐的步骤例如指针,内存管理交给JVM
**双亲委派机制作用:**防止替换系统级别的类
例如如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
四、沙箱安全机制
1.概述
Java安全模型的核心就是Java沙箱(sandbox)。什么是沙箱?沙箱是一个限制程序运行的环境。
沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
五、Native
例:
public class Test {
public static void main(String[] args) {
new Thread().start();
}
}
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
凡是带了native关键字的,说明java的作用范围达不到了,回去调用底层c语言的库!
会进入本地方法栈,调用本地方法,本地接口(JNI:java native interface)
JNI作用:扩展Java的使用,融合不同的编程语言为Java使用! 最初:c,c++
**为什么要用Native:**Java诞生的时候c,c++横行,想要立足,必须要调用c,c++的程序
它在内存中专门开辟一块标记区域:Native Method Stack(本地方法栈) , 登记 native方法,在最终执行的时候,通过JNI加载本地方法库中的方法
**应用:**java程序驱动打印机,管理系统,在企业级应用中比较少见!
调用其他接口:Socket…WebService~…http
六、PC寄存器
Pc寄存器是什么?
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要指向的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
PC寄存器中存储的就是操作数栈的指令地址,交由java的执行引擎执行
七、方法区(Method Area)
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单的说,所有定义的方法的信息都保存在该方法中,此区域属于共享区间。
静态常量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
static, final, Class , 常量池
public class Test {
private int a = 6;
private String name = "qinjiang";
public static void main(String[] args) {
Test test1 = new Test();
}
}
八、栈(Stack)
栈特点:先进后出
队列:先进先出(FIFO:FIrst Input First Output)
栈:栈内存,主管程序的运行,生命周期和线程同步。
线程结束,栈内存释放,对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就over.
栈:8大基本类型 + 对象引用 + 实例的方法
栈运行原理:栈帧
1.一个对象在内存中实例化的过程(粗略):
public class People{
String name; // 定义一个成员变量 name
int age; // 成员变量 age
void showInfo(){
System.out.println("人的姓名:"+name);
System.out.println("人的年龄:"+age);
}
public static void main(String[] args) {
String name; // 定义一个局部变量 name
int age; // 局部变量 age
People people = new People() ; //实例化对象people
people.name = "张三" ; //赋值
people.age = 18; //赋值
people.showInfo(); //调用方法showInfo
}
}
-
在程序进行过程中,首先类中的成员变量和方法会进入到方法区
-
程序执行到 main() 方法时,main()函数方法体会进入Java栈,定义了一个用于指向 People 实例的变量 people。然后将成员变量和成员方法放在 new 实例中都是取成员变量和成员方法的地址值 如图:
-
接下来对 People对象进行赋值,people.name = “张三” ; people.age = 18;
先在栈区找到 People,然后根据地址值找到 new Person() 进行赋值操作
4.当程序走到 people.showInfo()方法时,先到栈区找到 peo这个引用变量,然后根据该地址值在堆内存中找到 new People() 进行方法调用。
在方法体void showInfo()被调用完成后,就会立刻马上从栈内弹出(出站 )
最后,在main()函数完成后,main()函数也会出栈 如图:
九、堆(Heap)
一个JVM只有一个堆内存,堆内存大小可以调节
类加载器读取了类文件后,一般会把什么东西放到堆中?
类,方法,常量,变量,保存我们所有引用类型的真实对象;
堆内存分为三个区域:
- 新生区(伊甸园区) Young/New
- 养老区 old
- 永久区 Perm
GC垃圾回收,主要在伊甸园和养老区
假设内存满了,OOM,堆内存不够!,java.lang.OutOfMemoryError:lava heap space
在JDK 8 以后,永久区改了名字(原空间);
1.新生区
类:诞生和成长的地方,甚至死亡;
- 伊甸园:所有的对象都是在伊甸园中new出来。
- 幸存者区(0,1)
2.永久区
永久区常驻内存的。用来存放JDK自身携带的class对象,interface元数据,存储的是java运行时的一些环境或者类信息,永久区不存在垃圾回收,关闭虚拟机就会释放这个区域的内存。
一个启动类,加载了大量的第三方jar包,Tomcat部署了太多应用,大量动态生成反射类,不断的被加载,直到内存满,就会出现OOM.
-
jdk1.6 之前:永久代
-
jdk1.7 : 永久代,但是慢慢的退化了,
去永久代
,常量池在堆中 -
jdk1.8 之后:无永久代,常量池在元空间。
元空间逻辑上存在堆中,物理上不存在堆中
public class Car { public static void main(String[] args) { //返回虚拟机视图使用的最大内存 long max = Runtime.getRuntime().maxMemory(); //返回JVM的初始化总内存 long total = Runtime.getRuntime().totalMemory(); System.out.println("max = "+max+"字节\t"+(max/(double)1024/1024)+"MB"); System.out.println("total = "+total+"字节\t"+(total/(double)1024/1024)+"MB"); } }
由图可知,默认情况下分配的总内存是电脑内存的1/4,初始化内存为电脑的1/64。
接下来,调整JVM可使用的内存大小
接下来查看控制台输出
重新写一个测试类,测试OOM情况
public class Test {
public static void main(String[] args) {
String str = null;
while(true){
str += str + new Random().nextInt(88888888)+new Random().nextInt(99999999);
}
}
}
调整JVM可使用的内存大小为8MB
测试运行,查看运行结果
排查OOM错误:研究为什么会出错
-
能够看到第几行代码出错:内存快照分析工具,MAT(eclipse), Jprofiler
MAT(eclipse), Jprofiler作用:
- 分析Dump内存文件,快速定位内存泄漏
- 获得堆中的数据
- 获得大的对象
- …
-
debug : 一行行地分析代码!
安装Jprofiler的安装和使用:
- IDEA File->Setting 安装插件jprofiler,安装好后重启IDEA
-
前往jprofiler官网 https://www.ej-technologies.com/products/jprofiler/overview.html下载jprofiler9.2版本
-
jprofiler客户端安装路径不能有中文和空格,安装好后无脑下一步,直到输入许可证信息,许可证秘钥
L-Larry_Lau@163.com#36573-fdkscp15axjj6#25257
- 打开IDEA 配置JProfiler,找到安装路径下的jprofiler/bin/jprofiler.exe
- 配置 Car calss ,测试OOM问题所在
-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
- 打开生成的dump文件,查看进程详细信息。
十、GC
1.GC的概述
GC(garbage collec) 垃圾回收
GC是对JVM中的内存进行标记和回收,Sun公司的JDK用的虚拟机都是HotSpot
对象化的实例是放在heap堆内存中的,这里讲的分代收集也是指对堆内存的回收
JVM在进行GC的时候,大部分的时候回收都是新生代。
GC 两种类:轻GC(普通GC), 重GC(全局GC)
1.GC 题目
- JVM的内存模型和分区~详细到每个分区放什么?
- 堆里面的分区有哪些?fdem,form,to old ,老年区,元空间
- gc算法有哪些?标记去除法,标记压缩,复制算法,引用计数器,怎么用的
- 轻GC和重GC分别在什么时候发生?
2.GC算法
-
复制算法
如果一个对象经历类15此GC都没有死,进入到养老区。
可以通过 -XX:MaxTenuringThreshold=16 来重新设定进入老年区的时间
- 复制算法的优点:没有内存的碎片
- 复制算法的坏处:浪费了内存空间:多了一半空间永远是空to,万一对象100%存活(极端情况)
复制算法最佳使用场景:对象存货度较低的情况。
-
标记清除算法
- 优点:不需要额外的空间。
- 缺点:两次扫描,严重浪费时间,会产生内存碎片。
-
标记压缩算法
对标记清除算法的优化。
- 缺点:多了移动成本
-
标记清除压缩
先标记清除几次,再压缩。
-
总结
内存效率:复制算法>标记清除算法> 标记压缩算法(时间复杂度)
内存整齐度:复制算法 = 标记压缩算法>标记清除算法
内存利用率:标记压缩算法 = 标记清除算法>复制算法
难道没有最优算法吗?
答:没有,没有最好的算法,只有最合适的算法-------->GC:分代收集算法
年轻代: 存活率低 ---->复制算法
老年代:区域大:存活率 ----->标记清除(内存碎片不是太多)+标记压缩混合实现
十一、JMM
1.JMM概述
JMM的全称是 Java Memory Model(Java内存模型)
JMM的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的,这也是Java解决多线程并行机制的环境下,定义出的一种规则,意在保证多个线程间可以有效地、正确地协同工作。
2.JMM作用
作用:缓存一致性协议,用于定义数据读写的规则!
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
解决共享对象可见性这个问题:voliate
JMM的八种交互操作(每个操作都为原子操作)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用 - write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对八种操作的规则
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
十三、总结与补充
1.问题
- 请你谈谈对JVM的理解,java8虚拟机和之前的变化更新
- 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?
- JVM的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?
- 谈谈JVM中,你对类加载器的认识
推荐:多学习,多了解面试题,书籍《深入了解JVM》