一、JVM
JVM就是Java虚拟机,Java虚拟机就是JVM
1. JVM位置
-
1、
Java
程序(跑的环境是在jvm
(虚拟机)跑的,也可以说是在jre
上跑的)java
运行是需要在特定的环境的也就是这个jre
这种。 -
2、
jvm
(也就是jre
,jre
包括了jvm
):jvm
是用c
写的 -
3、操作系统(也是个软件
-
4、硬件体系(Intel,sapc)
2. JVM体系结构
2.1. jvm结构图
-
1、
java
编译 - 命令javac
-
2、编译生成
Class File
-
3、类装载器(类加载器
Class Loader
) -
4、运行时数据区:(类加载完成后进入这个运行时数据区:
Runtime Data Are
)运行时异常是不可捕获的。这是在类加载器后的产物!运行时数据区里面就有(方法区(Method Area
),java
栈(stack
),本地方法栈(Native Method Stack
),堆(heap
) ,程序计数器(pc寄存器
))本地方法栈
: 本地方法栈也是线程私有的数据区,本地方法栈存储的区域主要是 Java 中使用native
关键字修饰的方法所存储的区域。程序计数器
:程序计数器也是线程私有的数据区,这部分区域用于存储线程的指令地址,用于判断线程的分支、循环、跳转、异常、线程切换和恢复等功能,这些都通过程序计数器来完成。方法区
:方法区是各个线程共享的内存区域,它用于存储虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据。堆
:堆是线程共享的数据区,堆是 JVM 中最大的一块存储区域,所有的对象实例都会分配在堆上。JDK 1.7后,字符串常量池从永久代中剥离出来,存放在堆中。运行时常量池
:运行时常量池又被称为Runtime Constant Pool
,这块区域是方法区的一部分,它的名字非常有意思,通常被称为非堆
。它并不要求常量一定只有在编译期才能产生,也就是并非编译期间将常量放在常量池中,运行期间也可以将新的常量放入常量池中,String 的 intern 方法就是一个典型的例子。
-
5、本地方法接口(
native
)(和本地方法库相连),同时这一层还有执行引擎
。
2.2. jvm垃圾回收
垃圾回收,指的的堆内存的垃圾回收,垃圾回收机制简称GC
程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。
垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理
为什么java栈,本地方法栈,程序计数器是不会有垃圾回收的?
因为他们是栈,最终是要出栈的,若是上面的是一个垃圾阻塞了,那他就无法出栈了,Jvm调优也就是垃圾回收,调的就是方法区和堆,99%是调堆。
手动执行GC
System.gc(); // 手动回收垃圾
finalize方法作用
-
1、finalize()方法是在每次执行GC操作之前时会调用的方法,可以用它做必要的清理工作。
-
2、它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
public class Test {
public static void main(String[] args) {
Test test = new Test();
test = null;
System.gc(); // 手动回收垃圾
}
@Override
protected void finalize() throws Throwable {
// gc回收垃圾之前调用
System.out.println("gc回收垃圾之前调用的方法");
}
}
2.3. jvm调优
二、类加载器
类加载器: 负责把class文件加载到内存中
类加载机制
Java 虚拟机负责把描述类的数据从 Class 文件加载到系统内存中,并对类的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称之为 Java 的类加载机制。
1. 类加载的过程
一个类从被加载到虚拟机内存开始,到卸载出内存为止,一共会经历下面这些过程。
类加载机制一共有五个步骤,分别是加载、链接、初始化、使用和卸载阶段,这五个阶段的顺序是确定的。
其中链接阶段会细分成三个阶段,分别是验证、准备、解析阶段,这三个阶段的顺序是不确定的,这三个阶段通常交互进行。解析阶段通常会在初始化之后再开始,这是为了支持 Java 语言的运行时绑定特性(也被称为动态绑定)。
1.1.加载
1、获取class文件加载成二进制字节流
2、把该文件的编码结构-->运行时的内存结构
3、在内存中生成该类的一个Class对象
1.2.链接
验证
确保 Class 文件的字节流中的内容符合《Java虚拟机规范》中的要求
准备
为类中的变量分配内存并设置其初始值
解析
相当于翻译的过程,Java虚拟机将常量池内的符号引用替换为直接引用的
1.3.初始化
类加载过程的最后一个步骤,在之前的阶段中,都是由 Java 虚拟机占主导作用,但是到了这一步,却把主动权移交给应用程序。
1.4.使用
初始化之后的代码由 JVM 来动态调用执行
1.5.卸载
当代表一个类的 Class 对象不再被引用,那么 Class 对象的生命周期就结束了,对应的在方法区中的数据也会被卸载。
JVM 自带的类加载器装载的类,是不会卸载的,由用户自定义的类加载器加载的类是可以卸载的。
2.类加载器的分类
-
虚拟机自带 的加载器
-
启动类加载器 - null,获取不到,底层c++编写,向上委托到这里
-
扩展类加载器 -
ExtClassLoder
-
应用程序类加载器 -
AppClassLoder
3.双亲委派机制
- 1、类加载器接收到一个加载请求时,他会委派给他的父加载器,实际上是去他父加载器的缓存中去查找是否有该类,如果有就加载返回,如果没有则继续委派给父类加载,直到顶层类加载器。
- 2、如果顶层类加载器也没有加载该类,则会依次向下查找子加载器的加载路径,如果有就加载返回,如果都没有,则会抛出异常。
4.沙箱安全机制
如果我们要编写一个和核心类库全限定命一模一样的类,JDK为了保证核心代码的一个安全 阻止你的代码和全限定名相同
优点:保证原生JDK的安全,保证核心源代码 防止API被篡改,避免重复加载类
5.Native方法区
5.1. native
凡是使用了native关键字的,说明Java的作用范围已经达不到了,它会去调用底层的C语言的库。
- 进入本地方法栈。
- 调用本地方法接口。
5.2. 方法区
Method Area方法区(此区域属于共享区间,所有定义的方法的信息都保存在该区域)
方法区是被所有线程共享,所有字段、方法字节码、以及一些特殊方法(如构造函数,接口代码)也在此定义。静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
5.3. PC寄存器
又叫程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
6.java栈(虚拟机栈)
6.1. 栈的作用
栈是运行时的单位 程序如何运行 如何处理数据
栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就释放了,对于栈来说,不存在垃圾回收问题。
6.2.栈帧
a.概念
每个线程都有自己的栈,栈中的数据以栈帧的格式存在,每个方法对应一个栈帧
b.存储内容
局部变量表
每个方法的局部变量,数组结构,存放的形参
没有线程安全问题 因为数据是线程私有的
操作数栈
根据指令进行入栈 出栈
c.原理
先进后出
6.3.栈存储的东西
8大基本类型、对象引用,实例的方法
6.4.五道面试题
1.举例栈溢出的情况
- 1、函数中采用了很大的结构体,或者数组;
- 2、有很深的函数调用,或者递归调用
- 3、访问了非法的地址
通过-Xss1m调整栈空间
2.调整栈大小,就能保证不出现溢出吗?
不能
3.分配的栈内存越大越好吗?
不是
4.垃圾回收是否会涉及到虚拟机栈?
不会
5.方法中定义的局部变量是否线程安全?
是的
7.堆
堆内存的大小是可以调节的
7.1. 三种JVM
- Sun公司的HotSpot。(java -version查看)
- BEA的JRockit
- IBM的J9VM
7.2.堆的概述
一个JVM实例对应一个进程实例,一个JVM实例有一个运行时数据区(Runtime)
一个Runtime就有一个独立的方法区和堆
一个进程有多个线程,多个线程共享一个方法区和堆空间
一个线程拥有自己独立的程序计数器/本地方法栈/虚拟机栈
为了解决多个线程访问出现线程不安全问题–>TLAB(线程私有空间)
垃圾回收只会在堆(方法区)当中进行回收
7.3.堆内存中细分
主要区别在于jdk8以前是新生区、养老区、永久区。jdk8即以后使用元空间代替了永久区。
1.新生区
新生区又叫做伊甸园区,包括:伊甸园区、幸存0区、幸存1区。
新生区:老年区=1:2
新生区=eden:from:to 【谁空谁是to】
创建对象在eden
2.永久区
这个区域是常驻内存的。
用来存放JDK自身携带的Class对象、Interface元数据,存储的是Java运行时的一些环境或类信息~。
这个区域不存在垃圾回收。
关闭JVM虚拟机就会释放这个区域的内存。
什么情况下,在永久区就崩了?
- 一个启动类,加载了大量的第三方jar包。
- Tomcat部署了太多的应用。
- 大量动态生成的反射类;不断的被加载,直到内存满,就会出现OOM
3.老年区和元空间
什么是老年区和元空间??
方法区是一种规范,不同的虚拟机厂商可以基于规范做出不同的实现,老年区和元空间就是出于不同jdk版本的实现。
方法区就像是一个接口,老年区与元空间分别是两个不同的实现类。
只不过老年区是这个接口最初的实现类,后来这个接口一直进行变更,直到最后彻底废弃这个实现类,由新实现类—元空间进行替代。
jdk1.8之前:
jdk1.8以及之后:在堆内存中,逻辑上存在,物理上不存在(元空间使用的是本地内存)
4.常量池
在jdk1.7之前,运行时常量池+字符串常量池是存放在方法区中,HotSpot VM对方法区的实现称为永久代。
在jdk1.7中,字符串常量池从方法区移到堆中,运行时常量池保留在方法区中。
jdk1.8之后,HotSpot移除永久代,使用元空间代替;此时字符串常量池保留在堆中,运行时常量池保留在方法区中,只是实现不一样了,JVM内存变成了直接内存。
8.GC垃圾回收
8.1. 垃圾回收的区域
主要都是在方法区和堆中,且99%都是在堆中
8.2.引用计数法
每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,计数器为0就代表该对象死亡,这时就应该对这个对象进行垃圾回收操作。
优点:引用计数算法的实现简单,判定效率高,但建议不要使用
缺点:术无法解决对象之间的循环引用问题
8.3.复制算法
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。
优点:在存活对象不多的情况下,性能高,能解决内存碎片和java垃圾回收算法之-标记清除 中导致的引用更新问题
缺点:会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,复制算法的性能会变得很差
8.4.标记清除算法
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。
分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。优点:解决循环引用的问题、必要时才回收(内存不足时)
缺点:1、回收时,应用需要挂起,也就是stop the world;2、标记和清除的效率不高,尤其是要扫描的对象比较多的时候;3、会造成内存碎片(会导致明明有内存空间,但是由于不连续,申请稍微大一些的对象无法做到)
8.5.标记压缩算法
在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。
优点:解决标记清除算法出现的内存碎片问题
缺点:压缩阶段,由于移动了可用对象,需要去更新引用
8.6.GC算法总结
- 内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
- 内存整齐度:复制算法=标记压缩算法>标记清除算法
- 内存利用率:标记压缩算法=标记清除算法>复制算法