JVM
1.JVM内存划分
JVM其实是一个Java进程,一个进程在运行过程中,就需要从操作系统中申请到内存资源,JVM把这一块内存划分成几个区域,作为不同的用途.
- 堆区: 创建的对象都保存在堆上.分为新生代和老年代不同区域.
- 栈区:存放方法的调用信息.
- 程序计数器:记录线程的执行位置.
- 方法区:存放类对象,被加载到内存后的地方.用于存储类信息,常量,静态变量等数据.
2.JVM类加载机制
JVM类加载机制就是把类,从硬盘(文件)加载到内存中,Java程序一开始是编写一个java文件,编译成.class文件(字节码),当运行java程序时,JVM就会读取class文件把文件的内容放到内存中并且构造成class对象.
2.1 类加载过程
一个类的生命周期包含以下几个步骤:
- 加载:找到class文件,打开文件,读取文件内容,并且尝试解析格式,在Java中,加载阶段就是将类的字节码文件(.class文件)加载到内存中.
- 连接:确保class文件的内容是可以理解的和合理的
- 验证:检查class文件的格式和内容,确保没有错误
- 准备:给类对象分配内存,构造出完整的类对象,(分配内存+初始化),分配出来的内存空间,内容就是全0的值,静态成员的值也是0.
- 解析:主要是初始化类对象中涉及到的一些字符串常量,其实字符串常量已经在.class文件中存在了,直接读取到内存中即可.java虚拟机将常量池内的符号引用(相对的位置)替换为直接引用的过程(真实的内存地址),也就是初始化常量的过程.符号引用不知道字符串的真实的内存地址在哪里,只知道一个相对的偏移量,知道字符串的内容在哪个地方,等到字符串加载到内存中,就可以把真实的地址,替换掉刚才的符号引用.
- 初始化:对类对象进行具体的初始化操作,初始化静态成员,执行静态代码块,加载父类…
总的来说,类的加载就像是准备读一本书,你需要招到这本书,确保它的内容是正确的和可理解的,然后开始阅读.
2.2 双亲委派模型
Java的双亲委派模型是一种类加载的机制,用于保证Java类的安全性和一致性.双亲委派模型描述了类加载过程中,如何找到.class文件.其中JVM自带了三个类加载器.
Bootstrap ClassLoader:负责加载标准中的类,Java有一个标准文档,描述了需要提供哪些类
Extension ClassLoader:负责加载JVM扩展的库,除了标准库之外,实现JVM的厂商会添加一些类
Application ClassLoader:负责加载第三方库,比如mysql,jdbc,servlet,jackson
- 从Application ClassLoader开始,不会立即搜索第三方库的目录,而是把加载任务委派给父亲,让父亲先尝试加载.
- 到了Extension ClassLoader也不会立即搜索扩展库的目录,而是把加载任务委派给父亲,让父亲先尝试加载.
- 到了Bootstrap ClassLoader就没有父类了,此时就会自己搜索类,如果找到了这个类就会加载,没有搜索到就会把任务交回给孩子,也就是Extension ClassLoader.
- 任务回到Extension ClassLoader就要搜索扩展库的目录,如果有就会加载,没有就会把这个任务交回给孩子.
- 任务回到Application ClassLoader就要搜索第三方库的目录(一般是你的项目目录,以及和jvm的配置项有关),如果找到了类,就会加载,没有找到就会抛出一个异常.
一个类的加载时机,属于一种懒汉模式,只有被用到了才会加载
- 1.构造类的实例.
- 2.使用了类的静态方法/属性.
- 3.子类的加载会触发父类.
类加载之后,后续就不必被加载(类对象已经是现成的了)
3.Java垃圾回收机制(GC)
垃圾回收是一种自动管理内存泄露的机制,通过周期性的检查和释放不再需要使用的对象来减少内存泄露和提高程序性能…
垃圾回收一般分为两步:
- 判定对象是否是"垃圾".
在Java中一个对象,如果没有任何引用指向它,就可以认为是垃圾了.
判定的方式有两种:1.引用计数 2. 可达性分析
- 引用计数
给对象增加一个引用计数器,每当有一个地方引用时,计数器就会+1;当引用失效时,计数器就会-1,当计数器为0的时候就表示这个对象就是个垃圾了.但是主流的JVM没有选用引用计数的法来管理内存.
引用计数有两个明显的缺陷:-
- 空间利用率比较低,浪费更多的内存空间,如果给引用技术分配了2个字节的空间,对象本体才4个字节,引用计数就浪费了50%的空间,如果代码中是小对象(所消耗的空间不大),并且数量众多,那么引用计数的空间就会非常浪费空间.
-
- 可能存在循环引用的问题,导致对象不能正确识别为垃圾.
-
public class Test {
public Object instance = null;
private static int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
//Test实例1被a引用,引用数+1, a的引用数为1
Test a = new Test();
//Test实例2被b引用,引用数+1, b的引用数为1
Test b = new Test();
//a.instance的引用指向b,则a的引用数为2
a.instance = b;
//b.instance的引用指向a,则b的引用数为2
b.instance = a;
//a不再指向Test实例1,引用数-1,a的引用数为1
a = null;
//b不再指向Test实例2,引用数-1,b的引用数为1
b = null;
// 强制jvm进行垃圾回收
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
//此时a和b的两个对象的引用计数不为0,不能被当作垃圾,要想使用对象a就先访问到对象b,要想访问到对象b就得访问到对象a,最终这两个对象无法释放,就会产生内存泄露的问题.
- 可达性分析
JVM首先会从现有的代码中的能直接访问到的引用出发,尝试遍历所有能访问的对象,只要对象能够访问到,就会标记成"可达",完成遍历之后,可达之外的对象,也就是"不可达",相当于是垃圾.
通过GC Root为起点,向下开始搜索,当一个对象到GC Root没有任何引用链的时候,证明对象是不可用的.
虽然对象c和f之间有联系但是他们到GC Root是不可达的,因此会被判定为回收对象.
在Java中可以作为GC Root对象有:1.栈上的局部变量.2.常量池里的引用3.方法区的静态成员.
- 释放对象的内存.
- 标记 -清除(直接释放)
首先标记出要回收的对象,标记完成后统一回收所有被标记的对象.
但是,直接释放对象,就可能会引起"内存碎片",当我们在申请内存的时候,都是申请的"连续"内存空间,使用标记清除释放内存,就会破环原有的连续性,导致有内存,但是申请不了.这种问题如果随着程序的运行越来越多,就会导致申请内存越来越难申请. - 复制算法
将堆内存划分成两个大小相等的区域,每次只是用其中一个区域,当需要进行垃圾回收时,将存活的对象存放到另外一个区域,将原区域中对象都视为垃圾,这样就可以保证内存分配的连续性.但是会浪费一部分空间 - 标记整理
首先标记出从根对象可达的对象,然后进行整理,将存活的对象移动到一端,然后清理不可达的对象(垃圾).但是如果堆中存活的比例较高,可能需要移动大量对象,开销也是很大的. - 分代算法
在新生代中,每次垃圾回收都会有大量对象死去,只有少量对象存活,存活下来的对象,通过复制算法继续进行GC,当经历过N次GC后就会进入老年代,老年代对象存活率高,可以通过标记整理的算法继续GC.
新生代:新创建对象进入新生代.
老生代:比较大的对象和经历了N次垃圾回收依然存活下来的对象进入老生代.
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!