目录
2.5.6为什么对象在经过15次垃圾回收后就被放到老生区,垃圾回收的次数可以改变吗
2.6.1方法区中什么时候类才会被卸载(方法区的垃圾回收机制)
一.认识jvm
1.什么是jvm
简单来说jvm是将.class文件(字节码)编译为其他平台上对应的机器码指令,实现跨平台,帮助管理内存和线程。
二.jvm的组成及每个部分的作用
1.类加载器
类加载器:顾名思义就是加载类的,只加载.class文件(字节码),将硬盘上的字节码通过流的方式加载到内存中,存储在方法区中,在jvm中充当快递员的身份。
1.1类加载器过程
1.1.1图例
1.1.2加载过程
通过流将硬盘上的字节码读取到内存中的方法区,生成该类对象的class对象。
1.1.3 验证过程
检验字节码格式是否正确如果错误不符合字节码(class文件)的格式会出错。
如图1.1.3.1和1.1.3.2及1.1.3.3所示:
图1.1.3.1
图1.1.3.2
如上 图1.1.3.1和 图1.1.3.2 上述字节码编译正确,所以检查无误
下图1.1.3.3所示字节码编译错误
图1.1.3.3
1.1.4准备过程
在准备阶段将类中的静态属性进行初始化赋值。
例如:static int a=111 在准备阶段a的值初始化为0,在初始化阶段才赋值为111。注意不包含用final修饰的static常量。
1.1.5解析过程
将字节码中的符号引用(逻辑符号)替换为直接引用(内存地址)。
1.1.6初始化
(1)当类被初始化后该类才算真正被加载完毕。
(2)初始化过程将类中的静态成员(成员方法和成员变量)初始化赋值。
1.2什么时候类才算初始化(加载)完毕
简单来说只要类被用到了就算初始化(加载)完毕。
1.类中的静态成员(类名.成员变量,类名.成员方法)被使用。
2.运行该类的main方法。
3.new了该类的对象。
4.加载该类的子类时。
5.通过反射机制Class.forName()。
如下示例:
package com.ffyc.javapro.jvm.classloader;
public class Student {
public static final int M=10;//静态常量
public static int X=22;//静态变量
static {
System.out.println("此类被加载了");
}
}
package com.ffyc.javapro.jvm.classloader;
public class Test {
public static void main(String[] args) {
System.out.println(Student.M);//只访问类中的静态常量类不会被加载
System.out.println(Student.X);
}
}
1.3类在以下两种情况不会被加载
System.out.println(Student.M);//只访问类中的静态常量类不会被加载
Student[]students=new Student[10];//创建数组对象时,类只是被作为类型存在不会被加载,
1.4类加载器分几种
大方向上类加载器分为java编译的以及非Java编译(c或者c++编译的)的。
1.4.1如下图实例类加载器分类
1.4.2启动类加载器(引导类加载器)
这个类加载器使用 C/C++语言实现,嵌套在 JVM 内部.它用来加载 java 核心类库。
1.4.3扩展类加载器
Java 语言编写的,从JDK 系统安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。
1.4.4应用程序类加载器
Java 语言编写的,加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类。(该类加载器是程序中默认的类加载器)
1.5双亲委派机制
当加载一个类时,先由上级类加载器完成,优先加载系统中的类,一直到启动类加载器,如果到达了顶部类加载器,找到了就返回该类,未找到就委派到他的子级让他的子级类加载器去加载,在哪一级找到了就在哪一级返回,如果向下都没找到,就抛出类找不到异常。(优先加载系统中的类,防止我们自己的类替换了系统中的类)
1.5.1如下图所示
1.5.2代码实例
创建一个java.lang包下的String类并创建该类对象,验证双亲委派机制是否起作用,加载的优先是系统中的类。
package java.lang;
public class String {
static {
System.out.println("MyString加载了");
}
public String(){
System.out.println("成功了");
}
}
如下图该类调的依然是java库中的String类。
1.5.3如何打破双亲委派机制
可以自定义类加载器
package com.ffyc.javapro.jvm.classloader;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
/**
* 自定义类加载器
*/
public class MyClassLoader extends ClassLoader{
//类的路径
private String classPath;
public MyClassLoader(ClassLoader parent, String codePath) {
super(parent);
this.classPath = codePath;
}
public MyClassLoader(String codePath) {
this.classPath = codePath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
BufferedInputStream bis=null;
ByteArrayOutputStream baos=null;
//完整的类名
String file = classPath+name+".class"; // E:/Hello.class
try {
//初始化输入流
bis = new BufferedInputStream(new FileInputStream(file));
//获取输出流
baos=new ByteArrayOutputStream();
int len;
byte[] data=new byte[1024];
while ((len=bis.read(data))!=-1){
baos.write(data,0,len);
}
//获取内存中的字节数组
byte[] bytes = baos.toByteArray();
//调用defineClass将字节数组转换成class实例
Class<?> clazz = defineClass(null, bytes, 0, bytes.length);
return clazz;
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public static void main(String[] args) {
MyClassLoader myClassLoader = new MyClassLoader("E:/");
try {
//Class<?> clazz = myClassLoader.loadClass("Hello");//用系统的类加载流程
Class<?> clazz = myClassLoader.findClass("Hello");//用我们自己重写的加载类的方式
//打印具体的类加载器,验证是否是由我们自己定义的类加载器加载的
System.out.println("测试字节码是由"+clazz.getClassLoader()+"加载的。。");
Object o = clazz.newInstance();
System.out.println(o.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.运行时数据区
运行时数据区就是用来存储各种运行时的数据。(包括堆,方法区,程序计数器,虚拟机栈,本地方法栈)
2.1图示运行时数据区组成
2.2 程序计数器
用来记录每个线程执行的指令的位置,因为cup要切换线程。
特点:
(1)程序计数器占内存小,速度快。
(2)线程私有的每个线程都有一个计数器。
(3)生命周期与线程相同。
(4)不会出现栈溢出情况。
2.2.1如图所示程序计数器
2.3虚拟机栈
(1)虚拟机栈是运行结构,主要用来运行java的方法的。
(2)栈是线程私有的,每个线程运行都有一个自己的虚拟机栈空间。
(3)栈运行特点:先进后出,后进先出。
(4)栈是会出现内存溢出的。(递归调用)
(5)一个方法被调用后,在栈中被称为一个栈帧,保存局部变量,操作数栈,方法返回地址。
2.3.1如图所示虚拟机栈
2.4本地方法栈
用来运行调用本地方法(java中用native修饰的方法)。
2.4.1例如:
(1)new Object().hashCode(); (2)线程中的new Thread().start();
(3)文件与I/O操作中的
FileInputStream
/FileOutputStream
的open
方法
注意:运行时数据区中程序计数器,虚拟机栈,本地方法栈都没有垃圾回收机制。堆和方法区才有垃圾回收机制。
2.5堆
java中的堆是用来存放java产生的对象,Java产生的对象都放在堆空间中。
2.5.1堆的特点
(1)堆是虚拟机中空间最大的一块区域,在运行时数据区(除了程序计数器外,其他四个区域的内存空间大小都可以调整)。
(2)堆是可以被所有的线程所共享的。
(3)堆是垃圾回收的重点区域。
2.5.2堆分类
堆从大方向上分为新生代/新生区和老生区。
新生区分为:Eden(伊甸园)新创建的对象存放在此处,幸村者(0)和幸存者(1)
2.5.3为什么要分区
分区是为了将存活时间不同的对象存放到不同的区域,对这些不同的区域分别进行垃圾回收,减少对老生区的垃圾回收,频繁对新生区的垃圾回收。
2.5.4对象在堆空间的分配过程
新创建的对象都存放在伊甸园区。
(1)在经过一次垃圾回收后,伊甸园区中存活的对象就会被存放到幸存者0区。
(2)再次进行垃圾回收时就会将伊甸园区和幸存者0区存活的对象存放到幸存者1区。
(3)每次进行垃圾回收时都要保证一个幸存者区是空闲的,减少空间碎片。
(4)当一个对象经过15次垃圾回收后依然存活,那么就将此对象存放到老生区。
(5)如果一个对象很大,也可以直接存放到老生区。
2.5.5 对象在堆空间的分配过程图例
2.5.6为什么对象在经过15次垃圾回收后就被放到老生区,垃圾回收的次数可以改变吗
如下图所示:
(1)如上图在对象的组成对象头中有一块区域(分代年龄区)用来记录垃圾回收的最大次数,最大占四个比特位也就是最大是15,所以垃圾回收的最大次数就是15次。
(2)垃圾回收的次数可以通过设置一个参数来改变:-XX:MaxTenuringThreshold=<N>
在对象头中,它是由 4 位数据来对 GC 年龄进行保存的,所以最大值为 1111, 即为 15。所以在对象的 GC 年龄达到 15 时,就会从新生代转到老年代。
注意:
在老年代,相对悠闲,当老年代内存不足时,触发 GC,进行老年代的内存清理.若老年代执行了 GC 之后发现依然无法进行对象存储,会对整堆进行 GC, 之后依然无法进行对象存储, 就会产生 OOM 异常. (Java.lang.OutOfMemoryError:Java heap space)
2.6 方法区
存储加载类的字节码,主要存储加载到内存中的类信息。
特点:
(1)方法区在物理上与堆是属于一个空间,但是逻辑上是分开的。
(2)方法区的大小可以通过设置-XX:MetaspaceSize=N参数改变,方法区一旦空间不足就会触发 Full GC(整堆收集),会影响应用线程,一般情况可以把方法区大小设置大一点。
(3)方法区是会出现内存溢出的。
(4)方法区也存在垃圾回收。
2.6.1方法区中什么时候类才会被卸载(方法区的垃圾回收机制)
方法区中的类要被卸载需要满足三个条件
(1)该类以及该类的子类对象不存在了。
(2)加载该类的类加载器不存在了。
(3)该类的class对象不存在了。
2.6.1方法区与堆图例
3.执行引擎
执行引擎就是将Java编译的.class文件编译为机器码。
3.1执行引擎有两个编译
(1)第一次编译将Java文件编译为class文件,在开发阶段执行与运行无关,称为前端编译。
(2)第二次是在运行时虚拟机通过执行引擎将class文件编译为机器码,称为后端编译。
3.2解释器和编译器
1.解释器:无需进行编译,直接逐行对字节码进行解释,由于无需编译只需要解释就可以立即执行,速度快但效率低。
2.编译器:将字节码整体编译为机器码后执行,编译需要花费时间多,但效率高。
3.3为什么Java是半编译解释的(为什么这么设计)
解释执行虽然效率低,但是只需解释,在程序开始运行后就能立即执行,速度快。
编译执行虽然可以将一些热点代码缓存起来效率高,但是编译需要时间,速度慢。
所以Java在程序执行时使用解释器,编译器编译完成后使用编译器执行。
4.本地库接口
用来对接本地的方法,以及配合操作系统。
4.1什么是本地方法
本地方法就是用native修饰的方法。
4.2为什么要用本地方法
有时候我们需要用java操作我们电脑上的硬盘或者内存,因为Java属于应用程序语言没有权限对我们电脑上的硬盘或者内存直接操作。
例如:需要读取硬盘数据(new
FileInputStream.read)
或者读取内存地址(new Object().hashCode()),这时候就需要调用我们的本地方法。
二.垃圾回收
Java中的垃圾就是运行程序中没有被指向和引用的对象,该对象被称为垃圾对象。
1.早期垃圾回收
早期垃圾回收用使用c或c++需要程序员手动进行垃圾回收,方便灵活对内存的管理控制,但是需要程序员频繁对垃圾进行回收,一旦程序员忘记对某区域垃圾进行回收,时间一长,垃圾对象未清理就会产生内存泄露。
好处:可以精确管理内存,用的时候申请,不用的时候可以立即释放。
坏处:给程序员带来负担,一旦忘记释放垃圾就会一直存在。
2.现代的垃圾回收机制
Java,c#,python等都是使用的垃圾回收机制。
优点:解放了程序员,不用再关心什么时候需要释放垃圾对象。
缺点:降低了程序员对内存的管理。
3.内存溢出和内存泄漏
(1)内存溢出:内存满了不够用了,新产生的对象无法存放,Java会抛出内存溢出错误。
(2)内存泄露:一些对象已经不使用,垃圾回收器不能回收它,例如数据库连接对象,网络连接Socket对象还有IO对象,提供close()但是没有调用,那么垃圾回收器认为此对象还在使用中,这些对象就悄悄的占用着内存,这种现象被称为内存泄露,久而久之就会变为内存溢出。
4.垃圾回收的区域
运行时方法区里的堆和方法区是回收的区域。
堆是回收的重点区域,较少对老生代的垃圾回收,频繁对新生代的垃圾回收。
基本不回收方法区(在full gc整堆回收时才对方法区进行回收)。
三.垃圾标记阶段算法
垃圾标记阶段算法主要就是判断哪些对象是垃圾对象,把没有指向引用的垃圾对象标记出来,在垃圾回收时进行垃圾回收。
1.引用计数器算法(目前不使用)
对每个对象都加一个计数器,当有对象被指向或者引用时该计数器计数加一,当计数器为零时,表示该对象没指向或引用实现简单,则该计数器的对象为垃圾对象。(但引用计数器无法解决循环引用)
1.1循环引用
当多个对象之间相互引用,计数器不为零,他们和外界没有联系了,不被使用了,但是他们之间又相互引用,垃圾回收无法回收,就会造成内存泄露。
2.可达性分析算法
可达性分析算法可以解决引用计数器中的循环引用问题。
可达性分析算法先从活跃对象开始查找,跟活跃对象相连的对象都是被使用的,不与活跃对象相连接的对象是垃圾对象。
2.1.哪些对象可以被称为活跃性对象
1.虚拟机栈中被引用的对象:例如各个线程被调用的方法中被使用的局部变量,参数等。
2.方法区中类静态属性引用的对象:Java类中引用类型静态变量。
3.所有被同步锁synchronized持有的对象。
4.Java虚拟机内部的引用。基本类型数据对应的class对象,一些常驻的异常类型对象(NullpointerException等),系统类加载器。
3.finalize机制
Java中的Object类中提供的finalize方法。
这个方法是当对象被垃圾回收器判定为垃圾时,在垃圾回收器回收这个对象时,会调用这个方法。
finalize方法规定只调用一次,当一个对象第一次被回收时,调用finalize,当这个对象在finalize中使用了,复活了,当第二次被判定为垃圾时,回收时不在调用finalize方法。
我们可以重写finalize方法,在对象被垃圾回收前执行一些最终操作,但是不建议在自己主动去调用该方法,此方法的内容要慎重,否则会影响垃圾回收器的性能。
例如:
package com.ffyc.javapro.jvm.gc;
public class CanReliveObj {
public static CanReliveObj obj;//类变量,属于 GC Root
//此方法只能被调用一次
@Override
protected void finalize() throws Throwable {
System.out.println("调用当前类重写的finalize()方法");
obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
}
public static void main(String[] args) {
try {
obj = new CanReliveObj();
// 对象第一次成功拯救自己
obj = null;
System.gc();//调用垃圾回收器,触发FULL GC 也不是调用后立刻就回收的,因为线程的执行权在操作系统
System.out.println("第1次 gc");
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("第2次 gc");
// 下面这段代码与上面的完全相同,但是这次自救却失败了
obj = null;
System.gc();
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.1finalize方法存在分类的三种对象
由于finalize方法的存在,在虚拟主机中将对象的状态分为三种
可触及的:使用中的对象,没有被判定为垃圾 。可复活的:被判定为垃圾对象但是还没有调用finalize方法 ,该对象有可能在 finalize()中复活。不可触及的:对象第二次被判定为垃圾, 并且finalize被调用过了,该对象没有复活,那么就会进入不可触及状态。以上 3 种状态中,是由于 finalize()方法的存在,进行的区分。只有在对象不可 触及时才可以被回收。
四.回收阶段算法
在垃圾标记算法标记处垃圾对象后,再使用回收阶段算法,进行垃圾回收。
1. 标记复制算法
可以有多个内存,将内存分为多块,每次进行垃圾回收时将正在使用内存中存活的对象,复制到另一块未使用的内存块中,然后清除原来的内存块。
特点:
(1)使用多个内存块。
(2)回收时需要移动对象,适合存活对象少,内存小,垃圾对象多的场景。(新生代)
(3)回收后内存碎片少。
1.1如图所示标记复制算法图例
2.标记清除算法
只需要一块内存空间,存活对象不需要移动,直接清除垃圾对象。
特点:
(1)只需要一块内存。
(2)不需要移动存活对象,只需要清除垃圾对象。
(3)回收后会产生内碎片问题。
(4)适合存活对象多,请对象较大的场景。(老生代)
2.1标记清除算法图例
3.标记压缩算法
在标记清除算法的基础上,要移动存活对象,将存活对象移动到内存一端,按顺序排放,清除边界之外区域。
特点:
(1)是一种移动式垃圾回收算法,回收后,内存中没有碎片。
(2)适合老年代回收
3.1标记压缩算法图例
4.分代收集
不同的对象的生命周期是不一样的。因此, 不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。年轻代存活对象少,垃圾多,使用标记复制算法进行回收。
老生代对象大,存活时间长,使用标记清除算法和标记压缩算法。
五.垃圾收集器
垃圾收集器是jvm中内存回收的实践者。
垃圾回收器有很多,不同的虚拟机可以使用不同的垃圾回收器。
1.垃圾回收器分类
1.1从垃圾回收器线程数量上划分
(1)单线程:垃圾回收器只有一个线程在进行垃圾回收操作。(使用于小型简单的使用场景,垃圾回收时,其他用户的线程会暂停)
(2)多线程:垃圾回收器中有多个线程在进行垃圾回收操作。(在多 cpu 情况下大大提升垃
圾回收效率,但同样也是会暂停其他用户线程)
1.2从工作模式上分
(1)独占式的:当垃圾回收线程执行时,其他用户线程暂停。(把用户线程暂停的线程称为stw
(2)并发式的:垃圾回收线程和用户线程可以同时执行,减少用户暂停次数和时间。
1.2.1工作模式垃圾回收器如下图
1.3按照工作内存上分
(1)新生代垃圾收集器
(2)老生代垃圾收集器
2.CMS回收器
CMS(Concurrent Mark Sweep,并发标记清除)收集器,首创了垃圾收集线程和用户线程并发执行,追求低延时。回收过程:(1)初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。 (独占式的)(2)并发标记:垃圾回收线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。 (并发式的)(3)重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。 (独占式的)(4)并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。这个过程非常耗时。 (并发式的)
3.G1垃圾收集器
G1垃圾收集器适合整堆收集,不在区分新生代和老年代,适合服务器场景。
G1垃圾收集器将每个区域划分为更小的区域,例如将伊甸园区划分为多个小区域,优先回收垃圾对象较多的区域,故名做Garbage First,也支持并发执行。
适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。
3.1g1垃圾收集内存分配如图
3.2G1垃圾收集器收集过程图示
六.Jvm总结
总结:总的来说jvm是将java编译的字节码(.class)文件通过类加载器(也就是运输员)存放到内存也就是运行时数据区,因为字节码是底部数据指令不能由操作系统直接处理,所以需要再通过执行引擎(也就是翻译器)将将字节码转化为cpu能够处理的底部数据指令,这个过程要通过本地接口库借用其他语言接口来实现。