- 内存的划分
- 方法区/元数据区:存储加载到内存的.class文件,也就是类对象。
- 堆:代码中new的对象,占据最大的空间
- 虚拟机栈:代码执行过程中方法的调用关系,每一个元素称为栈帧,包含了方法的入口,返回的位置,返回值,形参,局部变量等等。每个线程都有一份
- 本地方法栈:存储了c/c++写的一些方法
- 程序计数器:,存储了下一条应该执行的指令的地址。
局部变量位于栈,成员变量位于堆,静态变量位于方法区。
- 类加载
类加载的基本流程:首先java代码被编译成.class文件交给JVM构成类对象加载到内存中
- 加载:找到.class文件,打开文件读取文件内容
- 验证:.class文件是一个二进制文件,有他自己要遵守的格式.
- 准备: 给类对象分配内存空间,此时还未初始化
- 解析: 对类对象中的字符串常量进行一个初始化操作.比如在类对象中保存这个常量的地址引用(文件偏移量).
- 初始化: 针对类对象的初始化,设置属性,初始化静态成员,执行静态代码块,或者父类的一些东西.
双亲委派模型
属于类加载中加载的一个环节负责找到.class文件,先交给双亲来查找,双亲在往上传递,传不了了就会开始查找(在自己负责的范围),双亲找不到就传给儿子.负责查找的是三个类加载器.
- BootStrap ClassLoader:负责扫描标准库,是最高级的类加载器
- Extension ClassLoader: 负责JDK一些拓展的库
- Application ClassLoader: 负责当前项目目录和第三方库对应目录,是最低的类加载器,也是入口.
之所以这么设定是为了保证类库加载的顺序.不过也是可以被打破的.
- GC垃圾回收机制
把申请出来的内存让JVM自动检测是否不用了,然后回收掉.这就带来两个问题1.系统开销:需要一些线程不断的去扫描所有对象是否需要回收.2.效率问题:扫描是周期性的,不一定及时.一旦同一时间大量的对象都要回收,就会造成很大的开销和负担.(STW问题)
回收的对象是内存中的,具体就是我们new出来的堆上的.在栈上的对象随着栈帧的销毁就回收了,静态的则是跟随整个程序.
GC可以分为两个步骤,找到垃圾,回收垃圾.
- 找到垃圾分为两种方案
- 引用计数: 在对象中单独有一块区域来存储引用这个对象的数量.出了{}引用就-1.PHP,Python都用的这个机制.java是没有用的,因为这存在两个问题1.内存的浪费,如果我这个对象就一个字节那么这个引用计数最起码占用了一半的内存(另外用一个字节来存计数).2.循环引用,a引用b,b引用a.a和b销毁了,但是里面的引用却没有.不过这些一定都有自己的解决方案.
- 可达性分析: 用一些线程周期性的去全力遍历所有对象,能遍历到的标记为可达,未被标记的为不可达对象也就是垃圾.出发点有很多,统称为GCRoots
- 垃圾回收
三种思路
- 标记删除: 把未被标记可达的直接释放掉.缺点是容易大量的产生内存碎片.
- 复制算法: 把内存分为两部分,一半进行存储,另一半进行就把可达的数据复制过去.这样就避免的内存碎片.但是内存利用率只有一半.而且数据的复制也要消耗计算机资源.
- 标记整理: 有点像链表的删除一样,把后面的对象复制到前面空缺的地方.
那么JVM采用了那种方案呢?是后两种的综合使用.就是什么情况适合就是用那种.
new的对象默认放到伊甸区里,然后通过复制算法放到幸存区,在幸存区又通过复制算法放到另一半.幸存区来回多次就转入老年区,老年区的对象扫描周期就变长了许多了.