java内存分配
JVM启动时,会在内存中占用一块空间。这块内存空间被分为:方法区、虚拟机栈、本地方法栈、堆、程序计数器。
这些内存区域组成java运行时数据区。
程序计数器:一块较小的内存,为当前线程所执行的字节码的行号指示器。字节码的解释执行需要通过这个计数器的值来取下一条指令。
而分支(if,switch),循环(for,while),跳转(break,continue),异常处理(try...catch),线程恢复都依赖计数器完成。
对于单核CPU来说,多线程是通过线程快速切换执行来完成的。而为了多线程切换后能快速恢复正确位置,每个线程有一个独立的程序计数器,所以程序计数器是线程私有内存。
此内存区域是唯一一个不存在内存溢出的区域。执行native方法时,计数器为空。
虚拟机栈:即我们常说的栈内存。线程私有,生命周期与线程相同,用于描述方法的执行。一个方法被调用至执行完成,对应入栈-出栈。
栈内存中存储:局部变量,操作栈,动态链接,方法出口
局部变量表:栈内存中存储局部变量的部分:存储八种基本数据类型、对象的引用(记录地址)、returnAddress(字节码指令地址)
栈内存在编译时就确定了大小,不会再改变(long,double占2个内存空间,其他占1个内存空间)
栈内存可能出现的异常:StackOverflowError(单线程内存溢出),OutOfMemoryError(由于每个线程都有私有栈,线程越多,栈内存总量越大,超出规定值溢出)
本地方法栈:同虚拟机栈,为native方法服务
堆:线程共享内存,虚拟机启动时创建,用于存放对象,数组。堆内存的特点是垃圾清理。可能出现异常OutOfMemoryError:对象过多。
方法区:线程共享内存,存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码。可能出现异常OutOfMemoryError。
常量池:方法区的一部。编译期生成的各种字面量,符号引用在类加载后存入常量池。
- Object obj = new Object();
- //Object obj存入栈内存的变量表
- //new Object()存入堆内存
- //而Object的类加载信息(类型,父类,接口,方法)存入方法区
复制代码
java的垃圾回收机制
程序计数器,虚拟机栈,本地方法栈三块内存生存周期与线程相同,这几块内存从分配到回收都是确定的,不存在垃圾回收问题。垃圾回收只涉及堆内存和方法区,这两块内存的分配回收都是动态的。
堆内存对象回收算法:一个基本的算法是引用计数器,即有一个引用指向对象,该对象的引用计数器+1,引用失效,引用计数器-1,当引用计数器为0时,启动垃圾回收。这种算法简单,高效,但存在极大问题。当A对象的属性引用B对象,B对象的属性引用A对象,除此之外两个对象再无引用。实际上这两个对象已经不可能被访问,但由于互相引用,导致永远不可能被回收,造成内存泄漏。由于上述问题,垃圾回收机制不会使用引用计数器算法。
JVM实际回收算法:根搜索算法:取一系列对象作为起始点,从这些节点上开始向下搜索,搜索所走过的路径称为引用链。当一个对象到起始点没有任何引用链连通时,说明该对象永远不可能到达,启动垃圾回收。
可作为起始点的对象:栈中本地变量表中引用的对象;方法区中静态类属性引用的对象;方法区中常量引用的对象;本地方法区中native方法引用的对象。
四种引用:强引用,软引用,弱引用,虚引用
JDK1.2之前,只存在引用,非引用。引用的定义即引用类型数据中存储的是一块内存地址,那这块内存即为引用。JDK1.2之后对引用进行了细化,当内存是足够的,一些引用可以存储于内存中,当内存在垃圾回收之后还非常紧张时,这些引用可以抛弃。
基于上述思想,划分出四种引用:
强引用:Object obj = new Object();这类引用在失效前永远不会被垃圾回收
软引用:系统在将要发生内存溢出之前,扫描软引用并回收,如果软引用都被回收,内存还是不够,才抛出内存溢出
弱引用:一次垃圾回收之后,下次垃圾回收之前的垃圾,一旦垃圾回收,这些弱引用必定会回收
虚引用:该引用获取不到引用对象,其作用仅仅是在这个对象被回收时,获取一个通知(知道哪个对象被回收了)
finalizer()方法
Object的方法,所有对象都有。一个对象的回收,经过两次搜索。第一次确定该对象是垃圾,之后会检查finalizer方法是否被重写,未重写则对象回收。若finalizer方法被重写,会将对象放入一个序列中,由虚拟机执行finalizer方法。此时,该对象有一次自救的机会,即在finalizer方法内部重新建立有效的引用,引用建立成功,则该对象又活了。自救失败,则垃圾回收。finalizer方法只会被执行一次,自救一次后,再垃圾回收,不会再判断finalizer方法。该方法的使用是极其不被推荐的,建议大家忘记这个方法。
方法区中的垃圾包括两部分:废弃常量,无用的类
废弃的常量:与堆内存中的对象相似,没有任何引用的常量即可执行垃圾回收
无用的类:必须同时满足三个条件:
1、该类所有的实例已经被回收
2、该类的classloader已经被回收了
3、该类的Class对象无引用,即该类不可能被反射
类加载
从类加载至卸载经过7个阶段:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析合称连接。
类执行初始化的条件:new对象,读取/设置一个类中的静态字段(非final的,final的在常量池中),调用类中的静态方法。
反射时。
初始化一个类时,发现其父类未初始化,则初始化父类。
启动虚拟机执行一个主类(带main方法的类)。
以上四种情况称为主动引用,而被动引用不会引发类初始化
被动引用:通过子类调用父类静态字段,子类不被初始化。
通过定义类类型数组,类不会被初始化。
常量在常量池中,与类无关,类不会初始化。
加载执行过程:
1、获取字节码二进制流(不一定是class文件)
2、将字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在堆内存中生成一个Class对象,作为静态数据入口
验证执行:检验代码是否可用,是否安全(不会对系统造成危害)
准备:为类变量(static)分配内存,设置类变量初始值。初始值为类型默认值,不是第一次赋的值
解析:将常量池中的符号引用替换为直接引用
初始化:执行所有静态变量赋值,执行static代码块
类加载器
JVM需要的是一个字节码文件的二进制流,而不是一个文件。该流的获取动作称为类加载器。
java中判断两个类是否相同,首先看是否由同一个类加载器加载,若不是,即使文件是同一个,也不是同一个类(包含equals()和instanceof)
静态绑定和动态绑定
静态绑定:在编译过程中已经绑定,与对象无关,哪个类型访问,调用哪个类的成员。java中成员变量(类变量,实例变量),被final,static,private修饰的方法,构造方法都是静态绑定。
动态绑定:运行时,根据对象的类型绑定,这也是多态的体现。java中非静态绑定的方法都是动态绑定。
|