JVM虚拟机总结—基础篇
JVM(Java Virtual Machine)
JVM体系结构预览
注意 : 其中亮色标识线程共享,也称主存中,灰色表示线程私有,在工作区内存当中
我们接下来就采取由上到下分三层来进行分析!
一、ClassLoader类加载
一、类加载机制
负责加载class文件(小class),class文件在文件开头有特定的文件标识,将文件的字节码文件内容加载到内存,装载到方法区!然后形成了类模板(也就是我们常说的大Class),且全局唯一 !
我们打开一个class文件查看 :
都具有cafe babe 这个标识
Book book = new Book() ;
方法区大Class模板 栈,引用 堆,实例对象
由此得出,我们的对象锁:锁的是对象实例,类锁:锁的就是我们这个方法区中的模板!
测试 :
public class ClassLoaderDemo01 {
public static void main(String[] args) {
ClassLoaderDemo01 demo01 = new ClassLoaderDemo01();
System.out.println(demo01.getClass().getClassLoader().getParent().getParent()); //Bootstrap根加载器
System.out.println(demo01.getClass().getClassLoader().getParent()); //扩展类加载器
System.out.println(demo01.getClass().getClassLoader()); //应用类加载器
}
}
//null 由于跟加载器是c++写的所以,我们的java访问不到!
//sun.misc.Launcher$ExtClassLoader@1b6d3586
//sun.misc.Launcher$AppClassLoader@18b4aac2
类加载流程 (双亲委派机制):
从图中可以看出,每个类加载器都有一个父加载器(有引用指向父加载器,而不是继承),在加载类时,首先check本身是否已加载,如果已加载,则返回,如果未加载,如果有父加载器则交给父加载器,如果没有父加载器则使用根加载器,如果仍没找到,则本加载器加载类,每一级加载器都执行相同的操作,这种机制称为委托机制,英语是parents delegation model
,翻译过来是双亲委派机制
由于双亲委派机制,加载java.lang.String
时会一直往上委派,直到根加载器,而根加载器只会加载jre/lib/rt.jar
中的java.lang.String
,从而确保自定义的java.lang.String
不会加载到jvm中,而不会让jvm错乱。
二、类加载器的分类
1、启动类加载器
也称根加载器(Bootstrap)
-
根加载器主要是用来加载
java_home/jre/lib
下的jar包,比如rt.jar
(含有全部java api的类),根加载器用C/C++实现,用null表示,在java代码中无法获取到根加载器。 -
Object o = new Object(); System.out.println(o.getClass().getClassLoader()); //根加载器
-
根加载器会提前加载好我们rt.jar中的所有基础类,作为我们的基础模板在方法区当中
2、扩展类加载器
拓展类加载器(Extension),于JDK1.9更名为 platform class loader
用来加载System.getproperty("java.ext.dirs")
也就是java_home/jre/lib/ext
下的jar包,扩展类加载器的父加载器是根加载器。
3、应用程序类加载器
应用类加载器 (Application)
用来加载System.getproperty(“java.class.path”)也就是我们常说的classpath下的类,此路径下都是应用程序的类,所以也可称为应用程序类加载器,它的父加载器是扩展类加载器,classLoader.getSystemClassLoader()返回的就是系统类加载器
以上就是java的核心类 + 对核心类补丁 + 用户自定的class,构成了我们java的基本盘!
4、用户自定义加载器
不满意
在程序运行时,如需自定义类加载器,通常继承java.net.URLClassLoader
,重写findClass
方法,这样符合双亲委派机制。
总结:当我们的一个类需要加载的时候,会将其分为给我们的应用类加载器,我们的应用类加载器会向上委托,直到委托给根加载器,判断自己是否加载过这个类,加载过直接返回,没加载过则继续的自顶向下的进行判断,如果都没加载过然后应用类加载器再对其进行加载!
二、native本地方法栈
一个方法被native修饰,标识java管不到这个方法了,这个方法输入c的势力范围!
- 本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它
- Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
- 本地方法栈是一个后入先出(Last In First Out)栈。
- 由于是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。
- 本地方法栈会抛出 StackOverflowError 和 OutOfMemoryError 异常。
三、PC寄存器
程序计数器 Pragrom Counter Register
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。(行号指示器)
- 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域
- 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。
- 程序计数器会存储当前线程正在执行的Java方法的JVM指令地址,若在执行native方法,则是未指定值(undefined)
也可以理解为记录线程执行状态!
四、方法区
Method Area
方法区,在JDK1.8之前 又称永久代(Permanent Generation),常称为PermGen,位于非堆空间,又称非堆区(Non-Heap space)。
在在JDK1.8开始,又被称为元空间 ;我们根加载器加载rt.jar中的类,就是加载到这个元空间
永久代和元空间的区别 :
- 永久代:存储包括类信息、常量、字符串常量、类静态变量、即时编译器编译后的代码等数据。可以通过
-XX:PermSize
和-XX:MaxPermSize
来进行调节。当内存不足时,会导致 OutOfMemoryError 异常。JDK8 彻底将永久代移除出 HotSpot JVM,将其原有的数据迁移至 Java Heap 或 Native Heap(Metaspace),取代它的是另一个内存区域被称为元空间(Metaspace)。 - 元空间(Metaspace):元空间是方法区的在 HotSpot JVM 中的实现,方法区主要用于存储类信息、常量池、方法数据、方法代码、符号引用等。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。,理论上取决于32位/64位系统内存大小,可以通过
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
配置内存大小。
1、方法区保存的数据
方法区是所有线程共享的内存,在java8中移除了永久代的内容,方法区由元空间(Meta Space)实现并直接放到了本地内存中,不受JVM参数的限制并且将原来放在方法区的字符串常量池和静态变量都转移到了Java堆中,方法区与其他区域不同的地方在于,方法区在编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:
类元信息(Klass)
- 类元信息在类编译期间放入方法区,里面放置了类的基本信息,包括类的版本、字段、方法、接口以及常量池表(Constant Pool Table)常
- 量池表(Constant Pool Table)存储了类在编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中
运行时常量池(Runtime Constant Pool)
- 运行时常量池主要存放在类加载后被解析的字面量与符号引用,包含但不限于。运行时常量池具备动态性,可以添加数据比较多的使用就是String类的intern()方法
- 运行时常量池都存储在方法区,而字符串常量池在jdk7时就已经从方法区迁移到了java堆中。
五、类加载顺序
分清主次,静态先行,静态代码块是在类加载的时候执行的,初始化在Class模板中,然后找入口main,然后执行构造代码块,执行完构造代码块之后,执行构造方法!
//测试:
public class ClassLoaderTest {
{
System.out.println("ClassLoaderTest的代码块444444");
}
public static void main(String[] args) {
System.out.println("ClassLoaderTest的main方法77777");
new code() ; //6,7,3,1,2
System.out.println("-==============");
new code() ; //1,2
System.out.println("===============");
new ClassLoaderTest() ; //4,5
}
public ClassLoaderTest(){
System.out.println("ClassLoaderTest的构造方法555555");
}
static {
System.out.println("ClassLoaderTest的静态代码块66666");
}
}
class code{
{
System.out.println("code的代码块11111");
}
public code(){
System.out.println("code的构造方法22222");
}
static {
System.out.println("code的静态代码块3333");
}
}
六、栈
在介绍JVM栈之前,我先了解一下 栈帧 概念
栈帧:一个栈帧随着一个方法的调用开始而创建,这个方法调用完成而销毁。栈帧内存放者方法中的局部变量,操作数栈等数据。
Java栈也称作虚拟机栈(Java Vitual Machine Stack),JVM栈只对栈帧进行存储,压栈和出栈操作。Java栈是Java方法执行的内存模型。下面我们来看一个Java栈图。
总结
- 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)。对象都存放在堆区中。
- 每个战中的数据(基础数据类型和对象引用)都是私有的,其他栈不能访问。
- 栈分为3个部分:基本类型变量,执行环境上下文,操作指令区(存放操作指令).
- 在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。
- 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量
- 分配的内存空间,该内存空间可以立即被另作他用。
StackOverFlowError
java.lang.StackOverFlowError,当我们的方法过多占满了栈空间,那么就会产生该异常(本质是个错误)!
七、栈、堆、方法区之间的交互关系
HotSpot 是通过指针访问对象的!(HotSpot是我们oracle发布的JDK统称)
栈:保存着实例对象在堆内存中的地址
堆:保存着类元数据的地址
Java8中的内存分布 :
与1.7的区别:
1、字符串常量池由方法区移动到堆中
2、方法区从堆内存移出,到本地内存当中
八、堆【重点】
一个JVM实例中是只包含一个堆内存,堆内存的大小是可调节的,类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
区别:
- JDK 7逻辑上分为:新生区 + 养老区 + 永久区 ,实际上:新生区 + 养老区
- JDK 8逻辑上分为:新生区 + 养老区 + 元空间 ,实际上:新生区 + 养老区
由于我们的方法区在1.7被称为永久区,在1.8被称为元空间,逻辑上是将方法区归入堆内存中!也会被称为非堆,目的就是为了和堆区分开来。
永久区占用的是堆内存中空间,元空间占用的是物理机的内存
OOM
出现oom:OurOfMemoryError : java heap space 的原因
- JVM的堆内存不够,可以通过手动设置参数来-Xms,-Xm来调整。
- 代码中创建了大量的对象,并且长时间不能被垃圾GC回收(存在引用)。
堆参数入门
注意:我们设置的堆参数-Xmx,-xms只会作用在堆中的新生区和养老区。
说明 | 备注 | |
---|---|---|
-Xms | 初始堆的分配大小,默认为物理内存的六十四分之一 | |
-Xmx | 堆的最大分配大小(默认为物理内存的四分之一) | |
-Xmn | 新生代的大小 |
测试我们JVM所占用的内存!
@SuppressWarnings("all")
public class HeapMemeoryTest {
public static void main(String[] args) {
long totalMem = Runtime.getRuntime().totalMemory(); //返回当前JVM占用的内存
long maxMemory = Runtime.getRuntime().maxMemory(); //返回当前JVM可占用的最大内存
System.out.println("当前JVM占用的内存" + totalMem/1024/1024);
System.out.println("前JVM可占用的最大内存" + maxMemory/1024/1024);
}
}
//当前JVM占用的内存245
//前JVM可占用的最大内存3625
为什么要使用2个survivor区 ? 可以不用吗 ?
参考文章 :https://blog.csdn.net/antony9118/article/details/51425581
调节堆的占用内存 :
例如 -Xms1024m -Xmx1024m -XX:+PrintGCDetails
如此将堆内存的最大和最小内存都设置为1024MB , 并打印GC日志!
堆中存放的数据
java堆是JVM内存中最大的一块,由所有线程共享,是由垃圾收集器管理的内存区域,主要存放对象实例
1、对象实例
- 类初始化生成的对象
- 基本数据类型的数组也是对象实例
2、字符串常量池
- 字符串常量池原本存放于方法区,从jdk7开始就放置于堆中。
- 字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table
3、静态变量
- 静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中
4、线程分配缓冲区( Thread Local Allocation Buffer)
- 线程私有,但是不影响java堆的共性
- 增加线程分配缓冲区是为了提升对象分配时的效率
- ava堆既可以是固定大小的,也可以是可扩展的(通过参数-Xmx和-Xms设定),如果堆无法扩展或者无法分配内存时也会报OOM
九、GC
Garbage Collection 垃圾回收机制
GC = 新生区的GC(轻量级GC,也称minor GC) +养老区fullGC(重量级GC ,majorGC)
常见面试题 *
1、什么是GC ?
答:GC垃圾回收机制,遵循分代收集的算法思路
- 频繁收集新生代
- 很少收集老年代
- 基本不动元空间(永久代)
2、在JVM当中如何判断一个对象是不是垃圾,对于垃圾啊是不是立即回收?
-
引用计数法(JVM已经弃用,Python等一些还在用) : 效率高,空应用就回收,但是对象互相引用特殊情况无法回收!
public class GCTest { public static void main(String[] args) { Student student1 = new Student(); Student student2 = new Student(); student1.student = student2 ; //这种情况GC就无法回收 student2.student = student1 ; } } class Student{ int id ; Student student ; }
-
GCRoots 根可达算法
所谓GC Roots 或者说tracing GC的根集合就是一组必须活跃的引用!
人话: 从Root根对象开始找,能找到的就是活跃的不可删除的,找不到的需要GC回收
可以作为GCRoot的对象
//1、t1可以叫虚拟机栈中引用的对象(由于强引用不可回收)
GCRootsDemo t1 = new GCRootsDemo();
//2、方法区中的类静态属性引用的对象
private static GCRootsDemo2 t2 = new GCRootsDemo2();
//3、方法区中常量引用的对象
private static finalGCRootsDemo3 t3 = new GCRootsDemo3(); 特点:都不会被回收的,可以作为根
3、说说你知道的垃圾回收算法有哪些 ?
1、复制算法
常用于新生代!
- 优点:不会产生内存碎片,
- 缺点:但是浪费空间,因为始终会存在一个空的幸存区 (浪费是值得的!hh)
2、标记清除算法
常用于老年代 !
第一步:标记出养老区的所有垃圾,第二步:清除所有的垃圾
- 优点:节省了空间
- 缺点:会产生内存碎片
3、标记清除压缩算法
也是常用于老年代!(慢工出细活)
在标记清除的步骤后,在将老年代通过整理算法整理一下空间,解决内存碎片问题
- 优点:解决上述的问题
- 唯一缺点:耗时
算法之间的比较
-
内存效率:复制算法 > 标记清除算法 > 标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
-
内存整齐度:复制算法 = 标记整理算法 > 标记清除算法。
-
内存利用率:标记整理算法 = 标记清除算法 > 复制算法。
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算
相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程
4、JVM内存模型以及分区,需要详细到每个区放什么 ?
- 方法区:主要是存储类信息,常量池(static 常量和 static 变量),编译后的代码(字节码)等数据
- 堆:初始化的对象,成员变量 (那种非 static 的变量),所有的对象实例和数组都要在堆上分配
- 本地方法栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所以还是一个指向地址的指针
- JVM方法栈:主要为 Native 方法服务
- PC计数器:记录当前线程执行的行号
5、堆里面的分区: Eden,survival from to,老年代,各自的特点 ?
新生代:
- Eden: Eden区位于Java堆的新生代,是新对象分配内存的地方
- survicor:在堆内存当中有两个这种分区一种是form,一种是to,from:也是Survivor0区,to:也是Survivor1区,这两个区是相对的,在发生一次Minor GC后,from区就会和to区互换。在发生Minor GC时,Eden区和Survivalfrom区会把一些仍然存活的对象复制进Survival to区,并清除内存。Survival to区会把一些存活得足够旧的对象移至年老代。
老年代:
- 年老代里存放的都是存活时间较久的,大小较大的对象,因此年老代使用标记整理算法。当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源。
6、Minor GC与Full GC分别在什么时候发生 ?
- Minor GC :又称轻量GC,发生在当我们的新生代的Eden区满了后触发!
- Full GC:又称重量GC,执行速度是轻量GC的十分之一,在我们的老年代满了后发生!
进阶篇,需要阅读《深入理解JVM虚拟机》!