1)JVM的内存布局
2)类加载机制
3)垃圾回收机制
我们创建一个对象的过程:
1)代码中new 一个对象的时候,会先看看这个类是否在内存中,如果不在,就通过类加载器,找到这个类的.class文件,加载内容到方法区,同时也会对依赖的父类进行加载;
2)类加载的时候会先执行这个类中的static代码块的内容
3)根据这个类的描述信息,去申请内存空间,申请的内存空间的大小与这个类密切相关
4)申请到的内存空间会先进行初始化全0的操作
5)先加载父类,在进行加载子类
6)先创建父类对象,在执行(父类的就地代码块,构造快,构造方法)
7)然后再继续构建子类对象(执行子类的代码中的就地代码块,构造快,构造方法)
1JVM的(运行时数据区)区域划分
一个操作系统是一个写字楼,一个公司就视为一个进程,这个公司就可以在这个写字楼里面租一块地方(JVM从系统中申请到整块内存)
我们就可以把这块地方划分成好几个区域,每一个区域有他各自的功能,每一块区域都有它的用处,所以JVM这一个java进程把内存空间分隔成了多块区域,每一块区域都有自己对应的功能,各自自己实现自己的功能;
JVM本质上就是一个java进程,他是管理也能硬件资源例如内存,JVM启动后就会从操作系统这里面申请到一大块内存,自己进行使用;
但是这里面为什么会有多个栈呢?因为JVM是一个进程,一个进程里面又包含着很多的线程,每一个线程,都有属于自己的栈,每一个线程都有属于自己的程序计数器(是私有的);堆和方法区都是只有唯一的一份;
1)注意 :类的成员变量都在堆上,只有方法里面定义的基础变量,在方法里面定义的引用其他对象的引用放在栈上(这个时候被引用的对象和它的成员变量还是在堆上)
一个操作系统,就相当于是写字楼,一个公司,就相当于是一个进程,也相当于是JVM;
.java我们原生态的代码,想要让编译器来识别这些代码,就要经过编译;编译器中有一个命令,javaC,会转化成字节码文件,他的本质上是一个二进制文件,包含了类中的所有信息;
2)堆:里面放的就是new的对象还可以放成员变量;
3)方法区:里面放的就是类对象,还会放静态变量,静态方法(因为类的Static成员作为类属性,同样也是在类对象中);.Java文件会被编译生成.Class文件,JVM会把.class文件进行加载,放到内存里面;
类中的static成员,作为类属性,同样也是在类对象中,也就是在方法区里面;
类对象中有啥?
1)包含了这个类中的各种属性的名字,类型,访问权限;
2)包含了这个类的各种方法的名字,参数类型,返回值类型,访问权限,以及方法实现的二进制代码;
3)包含了这个类的static成员........
4)程序计数器:是在内存区域中,最小的一个部分,里面只是放了一个内存地址,这个地址的含义就是接下来要执行的指令的地址;
咱们写的.java文件会转化成字节码文件.class(二进制字节码文件,就是一些指令)这些指令会被放到内存中,每个指令都会有自己的地址,CPU执行指令就会从内存中取地址,然后再从CPU上执行;
5)栈:放局部变量
本地方法栈:指的是JVM内部的方法,这是使用C/C++实现的方法;
虚拟机栈:给上层的java方法来进行使用的
和内存区域相关的异常主要有两个:
堆异常:堆的空间溢出,频繁的new对象,而不去释放;java.lang.OutOfMemoryError
栈溢出:无限递归,除了放局部变量,还要放方法之间的调用关系;StackOverflowError
成员变量是独立于方法外的变量在堆上,局部变量是类的方法中的变量,在栈上;
成员变量在堆上,局部变量在栈上;
下面在做堆栈判断时,在main方法里面(t在栈上面)那和方法外面(在new HelloWorld()里面,本身new HelloWorld()在堆里面,这里面的t就是引用类型;
class Test
{
public int value=0;
}
public class HelloWorld {
public static void main(String[] args) {
Test t=new Test();
}
}
这里面的new Test(),new出来的对象,以及类中的成员变量都在堆中 ,但是对于这个对象的引用,t就在栈中,他里面放了个0X1234的地址,通过这个地址,就可以访问堆中的它所指向的对象和变量;
class Test
{
public int value=0;
}
public class HelloServlet {//这个HelloWorld将来是要被new出来的,所以要放在堆中
Test test=new Test();
public static void main(String[] args) {
}
}
t是一个静态成员变量,是一个类属性,类对象是在方法区里面
class Test
{
public int value=0;
}
public class HelloServlet {//这个HelloWorld将来是要被new出来的,所以要放在堆中,里面没有方法嵌套
static Test test=new Test();
public static void main(String[] args) {
}
}
2.java中的类加载
类加载主要功能就是把JVM中的.class文件,转化成JVM中的类对象;
编译:我们自己写的代码,首先要把源代码转化成可执行文件程序(编译器干的事情)(.java-->.class)
加载:让这个可执行文件跑起来(类加载器干的事情)(.class文件-->可执行文件)
想要完成类加载,就必须要明确的知道,.class文件里面都有什么?按照什么样的规则来进行解析;
编译的过程中编译器和类加载器必须商量好.class文件的格式,编译器首先知道把.class文件要按照什么样的规则进行生成,加载器按照什么样的格式来进行解析,编译器所编译后的最终格式类加载器可以看的懂,才可以进行加载,这也相当于是一个协议,java虚拟机规范文档里面所约定的;
里面有java语法规范(这是用来约束java程序员和编译器的,Java程序员按照啥样的方式来进行写代码,编译器按照啥样的格式来进行编译代码),还有java虚拟机规范(他是来约束类加载器编译器和JVM的)
3.常见的习题
1)类加载的基本流程从.class文件到=>>>内存中的类对象?
1)加载:先把.class文件找到(他是根据类的全限定类名(包命+类名)来找是加载哪一个目录中的.class文件),代码中需要加载某个类,就需要在某个特定的目录中找到这个.class文件,找到之后就需要打开这个文件,并需要读文件,此时就把这些数据读到内存里面了;
2)验证:把刚才读到内存里面的东西进行一个校验,验证一下刚才读到的这个内容是不是一个合法的.class文件,这个.class文件是依据文件格式(是依据java虚拟机标准文档)来进行组装的,只有编译器生成的.class文件才可以通过验证;咱们要是自己写一个文件,后缀名是.class就不可以通过验证;也会验证字节码指令,也是方法里面进行执行的指令;
3)准备阶段:这个准备阶段,其实就是为了可以让类对象中的一些成员分配内存空间,例如静态变量,例如有一个变量,public static int a=10;会给这个int分配内存空间,同时会给a设置成0,这是一个初步的初始化(把初始的空间设置成全0);
4)解析:这个是针对字符串常量进行的处理,在.class文件中就会涉及到一些字符串常量,在这个类加载的过程中,就会把这些字符串常量给替换成当前JVM内部已经持有的字符串常量池的地址;不是程序一启动就会加载所有的类,而是用到哪个类就要加载哪个类,而字符串常量池是最开始启动JVM就有的,是在堆里面
5)初始化:此时会把真正的对静态变量进行初始化,同时也会执行static代码块;
注意:static变量的初始化以及static代码块的执行,在类加载阶段就执行了,是在对象的实例化之前执行的
class A{
public A() {
System.out.println("我是A的构造方法");
}
static {
System.out.println("我是A的静态方法");
}
}
class B extends A{
public B()
{
System.out.println("我是B的构造方法");
}
static {
System.out.println("我是B的静态方法");
}
}
public class HelloWorld {
B b=new B();
}
Astatic
Bstatic
A的构造方法
B的构造方法
解析:由于静态变量是先被,所以静态代码块时最先被执行的,由父及子,静态先行,当我们new B()的时候,会先进行加载B这个类,发现它继承自A,于是再加载A;只有把两个类加载完毕了,在执行实例化操作,在实例化B的过程中,对B进行构造方法时,首先要帮助A来进行构造;
4.类加载的双亲委派模型:
这是在加载阶段,是描述JVM去哪个目录中去找.class文件的,同时也是JVM再进行查找类的时候的一个优先级规则
进行类加载的时候,其中一个非常重要的环节就是根据这个类的名字"java.lang.String"找到对应的.class文件
双亲:parent指的是父亲或母亲,进行类加载的过程,一个非常重要的过程就是根据这个类的名字java.lang.String来找到对应的.class文件;
在JVM中,有三个类加载器,也就是三个特殊的对象,来负责这里面来找文件的操作;
1)JRE/bit/rt.jar(所有的文件都在这里面)他是负责加载java标准的一些类(String,ArrayList)BootStrap
2)JRE/lib/ext/*.jar这里面放的是JVM扩展出来的库中涉及的一些类--ExtClassLoader来进行查找
3)CLASSPATH指定的所有jar或者目录,他是负责加载程序员自己写的类
这三个类加载器之间存在父子关系(这个关系并不是继承中的父类子类,而是类似于链表一样,每一个类中有一个parent字段,指向了发类的加载器)
当我们在代码中使用某个类的时候,就会触发类加载,先是从AppClassLoader开始,但是AppClassLoader并不会真的开始立即去扫描自己所负责的路径,而是先去找他的爸爸,ExtClassLoader,但是此时ExtClassLoader也不会立即开始扫自己所负责的路径,而是先去找他的爸爸,BootStrapClassLoader,他此时也不会立即扫描自己所负责的路径,也想要找自己的爸爸,他没有爸爸,只能自己干活,去寻找自己所在的路径,如果在自己的目录中,找到了复合的类,就进行加载;也就没有别的类的事情了,就进行后续的一些操作;
但是如果没有找到匹配的类,就会告诉儿子(ExtClassLoader)我没找到,然后ExtClassLoader就来寻找自己所负责的目录区域;如果找到就进行加载,如果还没找到,就会再告诉自己的儿子AppicationClassLoader,再去扫描自己所在的目录,找到就进行加载,如果没找到,就会抛出异常(ClassNotFoundException)
搞出这么一套原则,实际上就是来约定三个被扫描目录的优先级,优先级最高的是JRE/bit/rt.jar,其次是JRE/lib/ext/*.jar,最低是CLASSPATH指定的所有jar或者目录,优先级从上向下依次递减;
然后在JVM的源码当中,针对这里面的优先级规则的实现逻辑,就被称为双亲委派模型;
如果是咱们自己写的类加载器,其实不需要严格遵守双亲委派模型,咱们自己写类加载器,就是为了告诉程序,向一些的目录中去找.class
比如说某个类的名字,同时出现在了多个目录当中,这个时候这个优先级就决定了最中要加载的类是神马?标准库中有一个类叫做java.lang.String,咱们自己写代码,也是可以创建一个类,叫做java.lang.String,我们要下进行加载java.lang.String
Tomact是没有遵守双亲委派模型的;
5.JVM的垃圾回收机制(GC,内存是有限的,况且内存是要给很多个进程来进行使用的,都是以对象为单位进行回收,此时对象在对象上面所占用的内存此时也会回收了)
1)背景介绍
垃圾回收,主要回收的是内存,JVM本质上来说是一个进程,一个进程中会有持有很多的硬件资源,带宽资源。例如CPU,硬盘,带宽资源,系统的内存总量是一定的,程序在使用内存的时候,必须先申请,才可以使用,用完之后,还需要进行释放;内存是有限的,用完之后一定要记得归还;
为了保证后续的进程有充足的内存空间,所以使用过的内存一定要进行释放;
从代码的编写来看,内存的申请时机,是十分明确的;但是内存的释放时机,是十分不确定的;这样也带来了一些困难,这个内存我是否还要继续使用吗?
C语言中,我们都学过malloc,free,如果malloc开辟出来的内存,不手动调用free,这个内存就会一直持有,此时内存的释放就全靠程序员自己来控制,一旦程序员忘了,或者该释放的时候,没有进行释放,就会造成内存资源泄露的问题;一直申请不释放,系统的可用内存资源越来越少,直到耗尽,此时其他资源想要再申请内存,就申请不到了;
对于手动回收内存来说,谁申请的谁就要进行释放
对于垃圾回收机制来说,谁申请都行,最后有一个统一的人来进行释放(对于java来说,代码中的任何地方都可以申请内存,然后再JVM中统一回收);是由JVM中一组专门用来进行垃圾回收的线程来做这样的工作;
优点:可以非常好的保证不会出现内存泄漏的情况
它的缺点是:需要消耗额外的系统资源,内存的消耗可能存在延时,不是说这个内存在用完后的第一时间释放,而是可能等一会再释放;可能会导致出现STW(stop the world)问题;(C++的核心目标有两个:和C兼容,追求极致的性能(机器运行的性能),另外要保证尽可能小的资源开销)
STW:例如说一个男人的生活习惯,换完衣服之后,自己把衣服整理好,放到柜子里面,这是相当于自己手动回收内存
但是如果说换完衣服之后就随即将衣服往沙发上面一扔,最后还要媳妇进行整理,这就相当于是进行垃圾回收机制
但是如果说这个媳妇出差了几天,回到家里面之后发现全部是衣服,那么此时媳妇就要画出全部的时间和精力来进行整理衣服,别的什么事情也没有时间干了;
Java中Stop-The-World机制简称STW(java程序员已经做了很多努力,争取把他的时间缩短),是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止
此时用java写了一个服务器,这个服务器在每秒钟同时要处理很多服务器发送过来的请求,可能每一秒钟有几万个请求,此时出发了STW,那么此时处理请求的线程无法进行工作了,暂停一下,此时客户端收到服务器的请求的时间就会变长,给用户带来很不好的体验;
2)垃圾回收要回收什么?------我们日常情况下所说的垃圾回收,本质上是指堆上面内存的回收,因为堆占用的内存空间就是最大的,本来就是占据了一个程序中绝大部分的内存
JVM中的内存有很多,栈,堆,程序计数器,方法区;
栈和程序计数器他们都是和具体的进程绑在一起的,当我们访问某个方法或者是代码块的时候,进入到方法或者代码块之后会申请内存,代码块结束,方法和代码块执行完之后会自动释放内存,所以栈和程序计数器是自动申请,自动进行释放的;
我们要回收的是堆和方法区尤其是堆区,方法区里面是类对象,通过类加载的方式得到的,对方法区进行垃圾回收,就相当于是类的卸载(类不进行使用了,就把他从内存中拿掉);堆占据的内存空间是最大的;
在堆上,是new出了很多对象,这里面主要分成三种
1)完全要使用
2)完全不使用
3)一半要使用,一半不使用(不会进行回收)
所以说java的垃圾回收,是以对象为基本单位进行回收的,一个对象,要么是完全被回收,要么完全不回收
回收垃圾的思路:先去找垃圾再去回收垃圾,对于一些对象来说,宁可放过1000,也不可以错杀一个
3)如何找垃圾/如何标记垃圾/如何判定垃圾(面试题)
1)引用计数(在java中没有使用引用计数):就是使用一个变量来保存这个对象被几个引用来,往往是在对象里面包含一个单独的计数器,随着引用的增加,我们的计数器就进行自增,随着引用减少计数器就自减例如:
Test a=new Test();//此时有一个引用指向它
Test b=a;//此时有两个引用来指向它
func(a);//这里面的函数里面的形参和下一个函数里面的形参都是由引用来执行这个对象的
void ss(Test a)
{
}
在new Test(),在这个对象中有一个计数器,随着引用增加,计数器就增加,引用减少,计数器就减一
Test a=new Test();
b=a;
a=null;
b=null;
此时没有引用指向new Test();此时这个引用计数为0,就认为这个对象是垃圾,在java中,只可以通过引用来访问这个对象,如果没有引用了,那么就认为这个对象在代码中再也无法进行使用了;因此我们就可以判断引用是否存在,来进行判断对象的生死
优点:规则简单,实现方便,比较高效,就是需要注意一下线程安全问题,程序运行效率比较高
缺点:1)空间利用率比较低,尤其是针对大量的小对象,如果一个对象很大(里面加个int没问题,因为这个对象所占用的内存很大,里面就算放一个int类型,才占用4个字节,因此影响也并不会很大,负担也不会太大)程序中对象数目也不多,这时候程序计数没问题;
但是如果是一个小对象,在程序中对象数目也很多,此时引用计数就会带来不可忽视的空间开销,一个int就已经占4个字节;
2)存在循环引用的问题,循环引用会导致引用计数出现问题,有些特殊的代码使用情况下,循环引用会使代码的引用计数出现问题,从而导致无法进行回收;
例:
class Test{
Test t=null
};
Test t1=new Test(); 1
Test t2=new Test(); 1
t1.t=t2;(下面变成2)
t2.t=t1;(上面变成2)
当执行这样的操作的时候:
t1=null,这样的一个操作其实是销毁了两个引用,但是此时的引用计数只减了1,这个操作即销毁了t1,也销毁了t1.t
t2=null,
此时就相当于少了t1(t1被销毁),也少了t1.t,此时这个计数器就会出现问题,没了t1,也就无法使用了t1.t
2)(java中的垃圾回收机制)---可达性分析
我们从一组初始的位置进行出发,向下进行深度遍历,把所有可以访问到的对象都标记成可达(是可以被访问到的),这个时候不可达的对象(没有进行标记的对象就是垃圾)
来举一个二叉树的例子:
A
B C
D E F G
class TreeNode{
char val;
TreeNode left;
TreeNode right;
}
TreeNode root=...;
假设root是一个方法中的局部变量,当前栈中的局部变量,也是进行可达性分析的一个初始位置
默认情况下,递归性进行访问,默认情况下,整个树都是可达的,都不是垃圾
但是如果在代码中写,root.right.right=null,那么此时G点就不可达了,就成了垃圾;
但是如果写了这个代码,root.right=null;那么C和G都是不可达的,也都变成了垃圾;
所以说在JVM中,就存在一组线程,来周期性的,来执行上述遍历过程,不断找出这些不可达的对象
由JVM进行回收
我们把可达性的初始位置,称为GCRoot,下面三种类型可作为GCRoot
1)栈上的局部变量表的引用
2)常量池中引用所指向的对象
3)方法区中,引用类型所指向的静态成员变量
基于上述过程,就完成了垃圾对象的标记,,他和引用计数相比,可达性分析确实要麻烦一些,同时实现可达性分析的遍历成本开销也是比较大的,但是他解决了了引用计数的两个缺点,内存上不需要消耗额外的空间(需要额外的空间来进行保存引用计数),也没有循环引用的问题;
不管是引用计数,还是可达性分析,实际上都是通过是否有引用来指向对象,通过引用来决定对象的生死
4)垃圾回收算法
1)标记-清除:标记阶段就是运用可达性分析,来进行标记一些将要被回收的对象,白色是正在使用的对象,灰色是已经被释放的空间
虽然可以释放不用的内存空间,但是此时会出现一个严重的问题:内存碎片,此时空闲的内存和正在使用的内存,是交替出现的
如果想要分一小块(直接把讲题出现的一小块内存给你),还行;
但是很多时候,申请的内存,是一块连续的内存空间,但是如果想要申请一个较大的连续内存空间,new Array[10000],整个系统空间,空闲100M,但是此时如果想要申请50M的内存空间,仍然可能分配失败,每10M是一个单位,分散在各个地方,是不连续;
系统看似有好多内存,但是分配不出来,在频繁申请释放内存,是非常严重的;
2)复制算法:有效的解决内存碎片问题;
当前我们只需要使用左边这儿一大块内存,接下来1,3要被回收了,这时就会把剩下的2和4拷贝到另外一侧,然后再回收1234这一整块空间,全部回收(2,4已经在右侧复制了一块内存,没有进行回收之前,相当于是左侧有1234内存,右侧有2,4内存,进行回收之后,左侧的内存全部被清空,右侧剩下了2,4内存,此时左侧就是一块连续的内存空间了;
复制算法的缺点:
1)可用内存空间只有一半
2)如果需要回收的对象比较少,剩下的对象比较多,复制的开销成本就比较大了
所它适用于:对象可以被快速回收,整体内存不大的情况下
3)标记整理:解决内存空间利用率较低的问题;这是类似于顺序表删除工作的搬运,既可以解决内存碎片,向前填充空闲的内存,是可以很好的避免内存碎片,也可以解决空间利用率,比复制算法开销很大(移动元素);
4)分代回收:我们把内存中的对象分成了几种情况,每种情况下,要采用不同的回收算法;我们是根据对象的年龄去进行划分的;
在JVM中进行垃圾回收扫描,也就是可达性分析,也是周期性的;每一个对象经历了一次·扫描周期,就认为长了一岁,我们就根据根据这个对象的年龄,来对整个内存进行分类;
把年龄短的对象放在一起,年龄长的对象放在一起,不同年龄的内存就可以采用不同的垃圾回收算法来进行处理;
整个堆内存分成三个区域:
1)新生代,伊甸区:刚刚new出来的新鲜对象就放在这里(hr收到的简历)
2)生存区,幸存区:放一些不是很新鲜的对象,也不是一些很老的对象(通过到简历筛选,进入到笔试)
3)老年代:年龄比较大的对象
分代回收的过程:
1)产生了一个新的对象,诞生于伊甸区;
2)如果是活到1岁的对象,对象经历了一轮GC还没有死,就把它拷贝到生存去里面;
这里要注意一个点,生存去看起来比伊甸区要小很多,空间里可以放得下这些对象吗?
根据经验规律,伊甸区的对象绝大多数都是活不过1岁的,只有少数对象可以活到生存区
对象大部分都是朝生夕死的,大部分对象的生命周期是很短的;
3)在生存区里面,有两个格子,左侧是A,右侧是B,在生存去里面,对象也要经历一轮GC,每一轮GC逃过的对象,都通过复制算法拷贝到另一块生存区里面,对象来会回进行拷贝,每一轮都会淘汰掉一些对象,复制算法在这里面只是使用了一小块内存空间,利用率低这个问题算是解决了,况且对象也很少;(进入到面试环节,每一纶面试就要淘汰一些同学)
4)在生存区里面,熬过一轮轮的GC之后,仍然屹立不倒,JVM就认为这个对象未来还会更持久的存储下去,这样的对象就把它拷贝到老年代(经历多轮面试之后,同学拿到了Offer,成功进入到公司)
5)进入到老年代的对象,JVM都认为这是可以能够持久存在的对象,这些对象也需要通过GC来进行扫描,但是扫描的次数和频率会大大的进行减少;新生代会被扫描的次数更多,老年代这里面使用的是标记整理算法
6)老年代的内存空间也是很大的,因为老年代的对象不宜回收,会一直进行累积,有的对象占据的内存比较大,会被直接放到老年区里面;因为这个对象占据的内存比较大,直接放到新生代,拷贝来拷贝去就会开销比较大
谈谈Java的垃圾回收机制?
找垃圾----可达性分析回收垃圾
跟中算法的优点和缺点
分代回收