目录
堆区:存放new出来的对象,所有线程共享堆区
栈区:存放方法之间的调用关系,每个线程对应有一个栈区
方法区:存放类对象(加载好的类),所有线程共享方法区
程序计数器:存放下一个要执行的指令的地址,每个线程对应有一个程序计数器
注意: 方法区在JDK8之后被替换为元数据区/元空间
JVM的内存划分一般会结合代码来考,举个例子:
- class Test2{
- private int z;
- }
- public class Test {
- //x是成员变量,在堆区
- public int x;
-
- //y是静态成员变量,类变量,在方法区
- public static int y;
-
- //test2也是静态成员变量,在方法区,但是它new的对象在堆区,所以test2中的z也在堆区
- public static Test2 test2 = new Test2();
- public static void main(String[] args) {
- //test是一个局部变量,在栈区,但是它new的对象在堆区
- Test test = new Test();
- }
- }
注意:变量存在于哪个区域,和变量类型无关,和变量的形态(局部/成员/静态)有关!
java程序在运行之前,需要先进行编译,把.java文件编译为二进制字节码的.class文件,当程序运行的时候,JVM就会读取对应的.class文件,并且解析文件中的内容,在内存中构造一个类对象并初始化。
类加载的过程大体可分为以下几个步骤:
JVM找到对应的.class文件,读取文件内容,并按照.class规范的格式来解析。
注意:“加载”阶段只是整个“类加载”过程中的一个阶段,不要搞混了~
连接过程又可以细分为三个步骤:
(1) 验证
检查当前的.class文件的字节流中包含的信息是否符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害JVM自身的安全。
(2) 准备
给类里的静态变量分配内存空间,初始值设为0。
(3) 解析
将常量池内的符号引用替换为直接引用,即初始化字符串常量的过程。
一个.class文件中包含很多字符串常量,比如代码里有一个字符串常量:String s = "hello",但是在类加载之前,"hello"还没有分配到内存空间,s里也就无法保存"hello"的真实地址,只能先使用一个占位符(即符号引用)来标记一下(这里存的是"hello"的地址),等到真正给"hello"分配内存空间后,就能使用真正的内存空间地址替代占位符~
执行类中的构造方法,对类进行初始化,加载父类、初始化静态变量、执行静态代码块……
(1) 第一次创建类的实例时;
(2) 使用了类的静态方法或静态属性;
(3) 使用了类的子类。
JVM加载类,是由类加载器来负责的,JVM中自带了多个类加载器(程序猿也可以自己实现)。
描述这几个类加载器相互配合的工作过程就是双亲委派模型。
如果一个类加载器收到了类加载的请求, 它不会自己去尝试加载这个类,而是先把这个请求交给父类加载器去完成,每一个类加载器都是这样,因此所有的请求最终都会先交给最顶层的类加载器去完成,只有当父类加载器无法完成这个请求时(它的搜索范围中没有找到所需的类),子类加载器才会尝试自己去完成。
如果父类加载器无法完成这个请求,就会交还给子类加载器去完成。
安全性:使用双亲委派模型可以保证Java的核心API不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己负责的类就可能会带来一些问题,比如程序猿自己写了一个全限定名称为“java.lang.Object”的类,那么程序运行的时候,就会加载程序猿自己写的Object类,而不是标准库中的Object类,因此安全性就不能得到保证。
由于申请内存资源后,需要手动释放内存资源,否则会造成内存泄漏问题,但是手动释放内存资源最大的问题就是容易忘记释放,GC(Garbage Collection)是一种自动释放资源的机制,程序猿只需要负责申请内存,释放内存的工作,由JVM完成。
GC主要针对堆区来进行内存回收。
栈区的变量,释放时机确定(出了作用域生命周期就结束),不必回收;程序计数器是固定内存空间,不必回收;方法区中的静态属性,在类加载之后一般是不会卸载的,所以也不需要进行处理。
在Java中,如果一个对象没有被任何引用指向的时候,就说明这个对象无法再被使用了,那么这个对象就是垃圾。
两种典型的判定对象是否存在引用的方法:
给每个对象都加上一个引用计数器,每当有一个引用指向这个对象时,计数器就+1;每当一个有引用不再指向这个对象时,计数器就-1,当计数器为0为,就说明这个对象不再被使用了。
但是,这种方法并不是JVM采取的方法,因为引用计数法无法解决对象的循环引用问题。
循环引用的代码示例:
- public class Demo {
- public Demo demo;
- public static void demoGC(){
- Demo demo1 = new Demo();
- Demo demo2 = new Demo();
- demo1.demo = demo2;
- demo2.demo = demo1;
- demo1 = null;
- demo2 = null;
- //强制jvm进行垃圾回收
- System.gc();
- }
- public static void main(String[] args) {
- demoGC();
- }
- }
约定一些特定的对象——GC Roots作为起始点,每隔一段时间,就从GC Roots出发进行遍历,能够被访问到的对象就被称为“可达”,否则就是“不可达”。
可达性分析是JVM中采用的判断对象是否存在引用的方法。
在Java中,可作为GC Roots的对象有以下几种:
(1) 栈上的引用类型变量;
(2) 常量池中的对象;
(3) 方法区中,静态的引用类型变量。
算法分为“标记”和“清除”两个阶段:首先标记出所有可回收的对象,在标记完成后统一回收所有被标记的对象。
标记清除算法的缺点:
(1) 效率问题:标记和清除这两个过程的效率都会带来额外的时间开销;
(2) 空间问题:标记清除后会产生大量的内存碎片,内存碎片太多的话,可能会导致程序在后续运行中需要给一个较大对象分配空间时,无法找到足够连续的内存。
复制算法将可用内存按容量分为大小相等的两块空间,每次只使用其中的一块。当内存1需要进行垃圾回收时,会将内存1上还存活的对象复制到内存2上,然后再把内存1上的对象全部清理掉。这样做就不会产生大量的内存碎片。
复制算法的缺点:
(1) 空间利用率更低,只有一半的内存被使用,另一半的内存空着。
(2) 如果内存上的存活对象比较多,可回收的对象比较少时,复制算法的效率就会大大降低。
标记整理算法的标记过程和标记清楚算法的标记过程一样,而整理过程类似于顺序表中的删除元素操作,但搬运对象的过程也比较耗时。
分代算法是根据对象存活的周期而把内存划分为几个部分。Java堆中分为新生代和老年代。在新生代中,每次垃圾回收都会有大量对象被回收,只有少数对象存活,因此在新生代中使用“复制算法”;而老年代中对象存活率高,一般使用标记整理法。
哪些对象会进入新生代/老年代?
新生代:一般刚创建的对象会进入新生代的伊甸区,如果经过一轮GC还存活的话会进入到新生代中的生存区;
老年代:刚创建的比较大的对象和经历了N次(一般是15次)GC后还存活的对象对象会从新生代进入到老年代。