JVM底层和GC原理

1 java一次编译到处运行是怎样实现的

        java语言是一种高级语言,想要让计算机执行就需要通过编译。而java编译程序不是直接将java源代码编译成计算机能识别的0、1序列的机器指令。而是通过javac 将其编译为字节码.class文件。去让java虚拟机JVM运行。(要想运行java程序必须安装该机器平平台的JVM)。java其跨平台运行的本质是因为JVM的存在。 jvm相当于是java的操作系统,他屏蔽了操作系统之间的差异。

JVM底层和GC原理_第1张图片
jvm加载class文件

总结:Java源代码通过javac第一次编译成.class字节码文件,然后通过ClassLoader加载.class到内存,通过执行引擎Exection Engine 将命令解析,native interface 是由不同语言的原生库为java所用。

2 类加载器(ClassLoader)

        类加载器(ClassLoader)是java的核心组件,它负责将class文件的二进制数据流装载到系统,交给java虚拟机进行连接,初始化等操作。

2.1 类加载器的种类

1.启动类加载器(BootStrapClassLoader):主要负责支撑jvm自身运行所需要的类由c++编写。包名为java开头的类。

2.扩展类加载器(ExtClassLoader):java编写,加载扩展库包名javax.*的类。

3.应用程序类加载器(AppClassLoader):java编写,用户可以看到的classpath下自己写的那些应用程序的类。

4.自定义ClassLoader:java编写,用户自定义加载类(继承ClassLoader重写findClass方法)。

2.2 类加载器的双亲委派机制

原理:

1.如果一个类加载器收到了类加载请求,它并不会自己先直接加载,而是把这个请求委托给父类的加载器去查找有没有装载过这个请求。

2.如果父类的加载器未加载过此请求并且还存在其父类加载器,则进一步向上委托,依次递归,请求最终会到达顶层的启动类加载器。

3.如果到顶层父类都未加载过请求,那么就从上到下开始尝试加载,此类加载器可以完成类加载任务,就成功返回,倘若无法完成此加载任务,则委派给它的子加载器去加载。

JVM底层和GC原理_第2张图片
双亲委派机制

好处:避免多份重样的字节码加载,减少资源浪费。

3 JVM内存模型(jdk8)

JVM底层和GC原理_第3张图片
jvm内存划分

线程私有的:

1. 程序计数器:是当前所执行的字节码行号指示器,改变计数器的值来选取下一条需要执行的字节码指令,对java方法计数,对native方法则计数器数值是undifiend。不会发生内存泄漏。

2. java虚拟机栈:是支持java虚拟机进行方法调用和执行的数据结构,它包含多个栈帧,每一个方法从调用至执行完成的过程就是栈帧在虚拟栈中入栈到出栈的过程。栈帧中存储了局部变量(方法中的所有变量),操作数栈(入栈,出栈,复制,交换产生的消费变量),动态链接,方法出口等信息。

3.本地方法栈:跟虚拟机栈类似,虚拟机执行Native方法服务的。

线程共享:

1.元空间MateSpace(1.8以后):存储calss文件中的类的版本,字段,方法,接口等描述信息,与永久代类似,只不过元空间不在虚拟机中而使用的是本地内存,1.7以后将字符串常量移动到了堆内存中。

2.堆内存:java对象实例分配的区域,占用虚拟机内存很大的一块,虚拟机启动即被分配,不用手动回收,是GC主要负责自动回收的区域。

3.常量池,这个不多说了。

4.垃圾回收GC

4.1为什么要进行垃圾回收

        因为随着程序的运行,内存中存在的实例对象、变量等信息占据的内存越来越多,如果不及时进行垃圾回收,必然会带来程序性能的下降,甚至会因为可用内存不足造成一些不必要的系统异常。

4.2哪些垃圾需要回收

        垃圾回收主要是回收堆内存里面的某些已经不存在任何引用的对对象。

4.3判断对象是否为垃圾的算法

4.3.1 引用计数算法

        对每个对象添加一个引用计数器,每被引用一次,计数器加1,失去引用,计数器减1,当计数器在一段时间内保持为0时,该对象就认为是可以被回收了。(在JDK1.2之前,使用的是该算法)

        缺点:当两个对象A、B相互引用的时候,当其他所有的引用都消失之后,A和B还有一个相互引用,此时计数器各为1,而实际上这两个对象都已经没有额外的引用了,已经是垃圾了。但是却不会被回收。

4.3.2 可达性分析算法

        该算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

JVM底层和GC原理_第4张图片
可达性分析算法

目前java 中可作为GC Root 的对象有:

        虚拟机栈中引用的对象(本地变量表)

        方法区中静态属性引用的对象

        方法区中常量引用的对象

        本地方法栈中引用的对象(Native Object)

4.4 常用的垃圾回收算法

4.4.1 标记—清除 算法

标记:从根记合进行扫描对存活的对象进行标记。

清除:对堆内存从头到尾进行线性遍历,回收不可达(可达性算法标记的不可达对象)的对象内存。

缺点:

1. 效率问题,标记和清除两个过程的效率都不高;

2. 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

如下图:我们通过扫描阶段标记处ACDHI是不可达对象,清除后在下面图中存在三个不连续的内存方块,假设有一个三个方块连续内存的对象要被创建的话就没有连续的内存空间可以使用了。

JVM底层和GC原理_第5张图片
JVM底层和GC原理_第6张图片
标记清除法

4.4.2 复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。反复去交换两个内存的角色,完成垃圾收集。

适用于存活率比较低的场景,避免了出现不连续碎片内存块。

java中新生代(年轻代)的from和to空间就是使用这个算法。

JVM底层和GC原理_第7张图片
复制算法

4.4.3 分代收集算法

1、根据对象存活周期的不同将内存划分为几块。

2、一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

3、在新生代中,每次垃圾收集时都发现有大批对象死去(回收频率很高),只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

其中,新生代又细分为三个区:Eden,From Survivor,ToSurviver,比例是8:1:1

4、老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

JVM底层和GC原理_第8张图片
分代收集算法

5.java中的引用类型

JDK 1.2之后,对引用进行了扩充,引入了强、软、若、虚四种引用,被标记为这四种引用的对象,在GC时分别有不同的意义。

5.1 强引用(Strong Reference)

就是为刚被new出来的对象所加的引用,它的特点就是,永远不会被GC,除非显示的设置null,才会GC。代码如下:

Object ojb = new Object();

5.2 软引用(Soft Reference)

非必须引用,内存溢出之前进行回收。如果JVM内存并不紧张,这类对象可以不被回收,如果内存紧张,则会被回收。此处有一个问题,既然被引用为软引用的对象可以回收,为什么不去回收呢?其实我们知道,Java中是存在缓存机制的,就拿字面量缓存来说,有些时候,缓存的对象就是当前可有可无的,只是留在内存中如果还有需要,则不需要重新分配内存即可使用,因此,这些对象即可被引用为软引用,方便使用,提高程序性能。代码如下:

Object obj = new Object();

SoftReference sf = new SoftReference(obj);

obj =null;

sf.get();//有时候会返回null

这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;

5.3 弱引用(Weak Reference)

第二次垃圾回收时回收。代码如下:

Object obj = new Object();

WeakReference wf = new WeakReference(obj);

obj =null;

wf.get();//有时候会返回null

wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。

弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器

5.4 虚引用(Phantom Reference)

垃圾回收时回收,无法通过引用取到对象值。代码如下

Object obj = new Object();

PhantomReference pf = new PhantomReference(obj);

obj=null;

pf.get();//永远返回null

pf.isEnQueued();//返回从内存中已经删除

虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。虚引用主要用于检测对象是否已经从内存中删除。

6 java中堆,栈,方法区的区别

6.1Java堆

        堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。

        根据垃圾回收机制的不同,Java堆有可能拥有不同的结构,最为常见的就是将整个Java堆分为新生代和老年代。其中新声带存放新生的对象或者年龄不大的对象,老年代则存放老年对象。

        新生代分为den区、s0区、s1区,s0和s1也被称为from和to区域,他们是两块大小相等并且可以互相角色的空间。绝大多数情况下,对象首先分配在eden区,在新生代回收后,如果对象还存活,则进入s0或s1区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄后,则进入老年代。

6.2 Java栈

        Java栈是一块线程私有的空间,一个栈,一般由三部分组成:局部变量表、操作数据栈和帧数据区

        局部变量表:用于报错函数的参数及局部变量。

        操作数栈:主要保存计算过程的中间结果,同时作为计算过程中的变量临时的存储空间。

        帧数据区:除了局部变量表和操作数据栈以外,栈还需要一些数据来支持常量池的解析,这里帧数据区保存着访问常量池的指针,方便计程序访问常量池,另外当函数返回或出现异常时卖虚拟机子必须有一个异常处理表,方便发送异常的时候找到异常的代码,因此异常处理表也是帧数据区的一部分。

6.4 Java方法区

        Java方法区和堆一样,方法区是一块所有线程共享的内存区域,他保存系统的类信息。比如类的字段、方法、常量池等。方法区的大小决定系统可以保存多少个类。如果系统定义太多的类,导致方法区溢出。虚拟机同样会抛出内存溢出的错误。方法区可以理解为永久区。

你可能感兴趣的:(JVM底层和GC原理)