目录
JVM内存区域划分
一个运行起来的Java进程,就是一个JVM虚拟机,需要从操作系统申请一大块内存,然后就会把这个内存,划分成不同的区域,每个区域都有不同的作用。
1、方法区(1.7及其以前)/元数据区(1.8开始)
存储的内容就是类对象,.class文件加载到内存之后,就成了类对象
2、堆
存储的内容,就是代码中new的对象(占据空间最大的区域)
3、虚拟机栈(栈)
存储的内容是代码执行过程中,方法之间的调用关系
4、程序计数器
比较小的空间,主要就是存放一个"地址",表示下一条要执行的指令,在内存中的哪个地方,方法区里的每个方法,里面的指令,都是以二进制的形式,保存到对应的类对象。
5、本地方法栈
本地方法指的是使用native关键字修饰的方法,这个方法不是使用java实现,而是在jvm内部通过C++实现的。也就是JVM内部的C++代码调用关系。
PS
- 虚拟机栈和程序计数器每个线程都会有一份,堆和元数据区在整个JVM进程中只有一份
- 局部变量处于栈上,成员变量处于堆上,静态变量(也叫做类属性)也就是放在元数据区
- Test t=new Test();t这个变量引用一个new出来的对象,但是t本身还是一个局部变量,还是存储在栈上。
- 局部变量处于栈上;成员变量处于堆上;静态变量处于方法区上
类加载
类加载的基本流程
java代码会被编译成.class文件(包含了一些字节码),java程序要想运行起来,就需要让jvm读取到这些.class文件,并且把里面的内容,构造成类对象,保存到内存的方法区中。
1、加载:找到.class文件,打开文件,读取文件类容,代码中,往往会给定某个类的"全限定名",例如java.lang.String,jvm就会根据这个类名,在一些指定的目录范围内查找。
2、验证:.class文件是一个二进制的格式。(某个字节,都是有某些特定含义的)就需要验证你当前读到的这个格式是否符合要求。
3、准备:给类对象分配内存空间(最终目的,是要构造出类对象),这里只是分配内存空间,还没有初始化,此时这个空间上的内存的数值,就是全0的,类的static成员就是全0的
4、解析:针对类对象中的字符串常量进行处理,进行一些初始化操作,java代码中用到的字符串常量,在编译之后,也会进入到.class文件中。(final String s="test")
5、针对类对象进行初始化,把类对象中需要的各个属性都设置好,还需要初始化好static成员,还需要执行静态代码块,可能还需要加载一下父类。
双亲委派模型
属于类加载中,第一个步骤,"加载"过程中,其中的一个环节,负责根据全限定类名,找到.class文件。
类加载器,是JVM中的一个模块
JVM中内置了三个类加载器:
- BootStrap ClassLoader
- Extension ClassLoader
- Application ClassLoader
双亲委派,类加载的过程(找.class文件的过程)
- 给定一个类的全限定类名,形如java.lang.String
- 从Application ClassLoader作为入口,开始执行查找的逻辑
- Application ClassLoader不会立即去扫描自己负责的目录(负责的是搜索项目当前的目录和第三方库对应目录),而是把查找任务交给它的父亲,Extension ClassLoader
- Extension ClassLoader也不会立即扫描自己负责的目录(负责的是JDK中一些扩展的库对应的目录)
- BootStrap ClassLoader也不想路基扫描自己负责的目录(负责的是标准库的目录),但因为BootStrap ClassLoader没有上层,只能亲自负责扫描标准库的目录。
- BootStrap ClassLoader没有扫描到,就会回到Extension ClassLoader,Extension ClassLoader就会扫描负责的扩展库的目录,如果找到,就执行后续的类加载操作,此时查找过程结束,如果没找到,还是把任务交给下层来执行
- Extension ClassLoader没有扫描到,就会回到Application ClassLoader,Application ClassLoader就会负责扫描当前项目和第三方库的目录,如果找到,就执行后续的类加载操作,如果没有找到,就会抛出一个ClassNotFoundException
GC垃圾回收机制
让JVM自行判定,某个内存是否不再使用,如果这个内存后面确实不用了,JVM自动把这个内存给回收掉。
GC的过程
1、找到垃圾(不需要的内存)
在GC,有两种主流的方案
引用计数(python,PHP)
new出来的对象,单独安排一块空间,来保存一个计数器,这个计数器描述这个new出来的对象有几个引用指向它,当一个对象没有引用指向了,就可以视为是垃圾了。
java没有采取引用计数的方法,因为引用计数存在两个重要的问题:1、比较浪费内存 2、存在"循环引用"的问题
可达性分析(java)
可达性分析,本质上时间换取空间的这样的方式,有一个/一组线程,周期性的扫描我们代码中所有的对象,从一些特定的对象出发,尽可能的进行访问的遍历,把所有能够访问到的对象,都标记成"可达",反之,经过扫描之后,未被标记的对象就是垃圾了。
可达性分析比较消耗系统资源,开销比较大
2、回收垃圾
三种思路
标记清除
把对应的对象,直接释放掉,就是标签清除的方案。
缺点:但是这个方案会产生很多的内存碎片,释放内存,目的是为了让别的对象能够申请,申请内存,都是申请连续的内存空间。
复制算法
通过复制的方式,把有效的对象,归类到一起,再同一释放剩下的空间。
也就是说把内存分成两半,一次只用其中一半,这个方案可以有效解决内存碎片的问题,但是缺点也很明显:
- 内存要浪费一半,内存利用率不高
- 如果有效的对象非常多,拷贝开销就很大
标记整理
既能够解决内存碎片的问题,又能处理复制算法中利用率,类似于顺序表删除元素的搬运,但是搬运的开销仍然很大。
实际上JVM采取的释放的思路,是上述思路的结合体,让不同的方案,扬长避短。
JVM的垃圾回收过程
刚new的新对象,放到伊甸区,从对象诞生,到第一轮可达性分析扫描,这个过程中,虽然时间步长(往往就是毫秒-秒)但是,在这个时间里,大部分的对象都会还成为垃圾 。
(1)伊甸区==>>幸存区,复制算法,每一轮GC扫描之后,都把有效对象复制到幸存区中,伊甸区就可以整个释放了,因为真正需要复制的对象不多,非常适合复制算法
(2)GC扫描线程也会扫描幸存区,就会把活过GC扫描的对象(扫描过程中可达)拷贝到幸存区的另一个部分
(3)当这个对象已经在幸存区存活过很多轮GC扫描之后,JVM就认为这个对象短时间应该是释放不掉了,就会把这个对象拷贝到老年代。
(4)进入老年代的对象,虽然也会被GC扫描,老年代GC扫描的频率就会比新生代低很多。
新生代,主要使用复制算法;老年代,主要使用标记整理。分代回收是JVM中主要的回收思想方法
GC也存在缺陷
1、系统开销,需要有一个/一些特定的线程,不停的扫描内存中的所有的对象,看是否能够回收,此时是需要额外的内存+CPU资源的。
2、效率问题,这样的扫描线程,不一定能够即使的释放内存(扫描总是有一定的周期),一旦同一时刻,出现大量的对象需要被回收,GC产生的负担就会很大,甚至引起整个程序都卡顿(STW问题,stop the world)