引自:https://www.cnblogs.com/wbyp/p/7753528.html
引导类加载器(bootstrap class loader)
--用来加载java的核心库(String,Integer,List......)在jre/lib/rt.jar路径下的内容。使用c代码来实现的,并不继承自java.lang.ClassLoader.
--加载扩展类加载器和应用程序加载器,并指定他们的父类加载器。
扩展类加载器(extensions class loader)
--用来加载java的扩展库(jre/ext/*.jar路径下的内容),java虚拟机的实现会自动提供一个扩展目录。该类加载器在此目录里面查找并加载java类。
应用程序类加载器(application class loader)
--它根据java应用的类路径(classpath路径),一般来说java应用的类都是由它来完成加载的。
自定义类加载器
--开发人员可以通过继承java.lang.ClassLoader类的方式实现在即的类加载器,以满足一些特殊的要求。
扩展类加载器、应用程序类加载器和自定义类加载器都是由java实现,都继承java.lang.ClassLoader类。
当某个类加载器在接收到加载类的请求后,首先将加载任务委托给父类加载器,依次追溯,如果父类加载器能够完成类加载任务,就成功返回,只有父类加载器无法完成加载任务是,才自己加载。
双亲机制是为了保证java核心库的类型安全,不会出现用户能自定义java.lang.Object类的情况。
双亲委托机制是代理模式的一种,并不是所有类加载器都采用双亲委托机制,Tomcat服务器类加载器也使用代理模式,不同的是它是首先尝试自己去加载某个类,如果找不到再代理给父类加载器。
jvm把class文件加载到内存,并对数据进行校验、解析和初始化,最终形成jvm可以直接使用的java类型的过程。
类加载过程:类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括7个阶段:加载、验证、准备、解析、初始化、使用、卸载(其中验证、准备和解析这三个部分统称为连接)。其中加载、验证、准备、初始化和卸载这五个阶段的顺序是一定的,而解析阶段不一定,在某种情况下,可以在初始化之后再开始,这是为了支持java语言的运行时绑定。
类的主动引用(一定会发生类的初始化):
--new一个类的对象
--调用类的静态成员(除了final常量)和静态方法
--使用java.lang.reflect包的方法对类进行反射调用
--当初始化一个类,如果父类没有被初始化,先初始化其父类
--当要执行某个程序时,一定先启动main方法所在的类
类的被动引用(不会发生类的初始化)
--当访问一个静态变量时,只有真正声明这个静态变量的类才会初始化(通过子类引用父类的静态变量,不会造成子类的初始化)
--通过数组定义类应用,不会触发此类的初始化A[] a = new A[10];
--引用常量(final类型)不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)
首先java源代码文件(.java)会被java编译为字节码文件(.class),然后由jvm中的类加载器加载各个类的字节码文件,加载完毕之后,交由jvm执行引擎执行。
jvm区域可以根据线程分成线程隔离和线程共享两个部分,其中线程隔离即这些区域是线程独有的,每个线程都会分配这样的区域,包括程序计数器、Java栈和本地方法栈;线程共享的有方法区和堆。
程序计数器(Program Counter Register)
由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此在任意具体时刻,一个CPU只会执行一个线程中的指令,为了能够使得每个线程都在线程切换或能够恢复到切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否认就会影响到程序的正常执行次序。所以程序计数器是每个线程所私有的。在jvm规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中保存的值是undefined。
java栈(vm stack)
java栈也称为虚拟机栈(java vitual machine stack),java栈中存放的是一个个栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(local variables)、操作数栈(perand stack)、指向当前方法所属的类的运行时常量池的引用、方法返回地址和一些额外的附加信息。
本地方法栈(native method stack)
本地方法栈与java栈的作用和原理非常相似,只不过java栈是为执行java方法服务,而本地方法栈是为执行本地方法服务的。在jvm规范中,并没有对本地方法的具体实现方法以及数据结构做强制规定,虚拟机可以自由实现它。在Hotsopt虚拟机中直接就把本地方法栈和java栈合二为一。
方法区(Method Area)
方法区在JVM中是一个非常重要的区域,与堆一样是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法、字段信息)、静态变量、常量以及编译器编译后的代码。在方法区有一个非常重要的部分就是运行时常量池它是每一个类或者接口的常量池的运行时表示形式,在类和接口被加载到jvm后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间,也可将新的常量放入运行时常量池中,比如String的intern方法。可以认为方法区就是永久代。
堆(Heap)
java中的堆是用来存储对象以及数组,数组的引用是存放在java栈中的。堆被所有线程共享,在jvm中只有一个堆。
在java中,堆被划分成两个不同的区域:新生代(Young)、老年代(Old)。
新生代又被划分为三个区域:Eden和两个幸存区。
这样划分的目的是为了使JVM能够更好地管理堆内存中的对象,包括内存的分配及回收。
新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)后仍然存活的对象。
方法区主要存放类与类之间关系的数据,这部分数据被加载到内存以后,基本上不会发生变更,但是后期方法区也会被回收,回收的条件非常的苛刻;java堆中的数据基本上是朝生夕死的,用完之后就会被回收;java栈和本地方法栈中的数据,满足先进后出的原则,当要获取栈低的元素,必须把栈顶的元素出栈,回收率为100%;程序计数器是唯一一块不会内存溢出的区域。
java中如果一个对象,没有一个引用指向它,那么它就被认为是一个垃圾。
java中引用越弱表示对垃圾回收器的限制越少,对象越容易被回收。
1、引用计数器算法:当创建对象时,为这个对象在堆栈空间中分配地址,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1,而当其中一个引用销毁时,引用计数器-1,当引用计数器被减为0的时候,标志着这个对象已经没有引用了,可以被回收。但是当代码出现下面的情形时,该算法无法适用,objA指向objB,而objB又指向objA,这样其他所有引用都消失了之后,objA和ObjB还是有一个相互的引用,无法回收,但实际上这两个对象都已经没有额外的引用了,已经是垃圾了。
ObjA.obj = ObjB;
ObjB.obj = ObjA;
2、根搜索算法(GC Root):把所有的引用关系看做一张图,从一个节点GC Root开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。java中可作为GC Root的对象有:虚拟机栈中的引用对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的对象。
3、收集后的垃圾通过什么算法来回收?
新生代:绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可达,所以很多对象被创建在新生代,然后消失。对象此区域消失的过程称为“minor GC”.
一共有三个空间,其中包含一个伊甸园区(Eden)和两个幸存区(survivor)。各空间执行顺序如下:
1、绝大多数刚刚被创建的对象会存放在伊甸园空间。
2、在伊甸园空间执行了一次 GC后,存活的对象被移动到其中一个幸存者空间。
3、此后,在伊甸园空间执行GC后,存活的对象会被堆积在同一个幸存者空间。
4、当一个幸存者空间饱和户,还在存活的对象会被移动到另一个幸存者空间,之后会清空已经饱和的那个幸存者空间。
5、在以上的步骤中重复几次依然存活的对象就会被移动到老年代。
老年代:对象没有变的不可达,并且从新生代中存活下来,就会被拷贝到这里,其所占的空间要比新生代多。也正是因为其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,称为“major GC”。
永久代:也被称为方法区,用来保存类常量以及字符串常量。因此这个区域不是用来永久的存储那些从老年代存活下来的对象。这个区域也可能发生GC,并且发生在这个区域上的GC时间也被称为major GC.