内存
虚拟机规范中,将内存划分为六大部分,分别是PC寄存器(程序计数寄存器)、JAVA虚拟机栈、JAVA堆、方法区、运行时常量池以及本地方法栈。(由于运行时常量池是由方法区分配出来的区域。)
内存管理
对于内存管理可分为内存分配和内存释放
这几个内存区域可以大致分为两部分,全局共享 和线程独有。
对于线程独有这部分内存,有PC寄存器/JAVA虚拟机栈/本地方法栈。它们都是随着线程的启动而创建,随着线程销毁而释放。这一部分内存,不需要垃圾收集器的管理,是由java虚拟机来主动管理。 每一个线程被创建的时候,java虚拟机就会为其分配相应的 pc寄存器和JAVA虚拟机栈,有需要的话还有本地方法栈。 当线程销毁时,java虚拟机会将这个线程占有的内存全部释放。
而全局共享的这部分内存。即堆内存。内存分配 主要由new关键字来触发,至于new出来的内存在哪分配,如何分配,则是由java虚拟机来决定。 而内存释放,则是由内存管理系统 即 GC 来管理的。
通常情况下,堆内存释放是要依赖于GC的策略实现的 。GC策略主要解决了
1.哪些对象可以被回收
2.何时回收这些对象
3.采用什么方式回收
哪些对象可以被回收 。 Jvm根据根搜索算法:
设立若干种根对象,当任何一个根对象到某一个对象均不可到达时,则认为这个对象是可回收的。
根搜索算法解决了基本问题:哪些对象可以被回收。垃圾收集还需要解决何时回收和采用什么方式回收。现代虚拟机 垃圾收集的算法主要有三种。标记-清除算法, 复制算法 ,标记-整理算法
标记-清除算法:
当堆中有效内存被耗尽的时候,就会停止整个程序。然后进行两项工作。标记 和 清除
标记:即 遍历所有的GCRoots,然后将所有GCRoots可达的对象标记为存活的对象
清除:清除的过程将遍历所有的对象,将没有标记的对象全部清除掉
缺点:
首先就是效率比较低。递归和全堆对象遍历。进行GC的时候,需停止应用程序。
第二 这种方式清理出来的空闲内存是不连续的。因为死亡的对象是随机出现在内存各个角落,清除之后布局自然很乱。为了应付这点,JVM不得不维持一个内存的空闲列表。
复制算法:
复制算法是将内存划分为两个空间,一个是活动区间 另一个是空闲区间。在任意时间点,所有分配的对象都分配在活动区间。当有效内存耗尽,JVM暂停程序运行,开启复制算法GC线程。GC线程会将活动区间所有对象复制到空闲区间,且严格按照内存地址排列。同时GC线程将更新存活对象的内存引用地址 指向新的内存地址
复制算法弥补了标记清除算法中内部布局混乱的缺点,但它的缺点也很明显
1.浪费了一半的内存
2.复制对象的时间,在对象成活率很高时 ,将不容忽视
标记-整理算法
分为标记和整理
标记:和标记清除算法一样,均遍历GCRoots,将存活的对象标记
整理:整理所有存活对象,且按照内存地址依次排列。然后将末端地址以后的内存全部回收。
所以:标记-整理算法 不仅弥补标记-清除算法中 内存区域分散的特点,也消除了复制算法中 浪费一半内存的代价。
同时它的缺点也是效率不高。不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
于是有了终极算法-分代收集算法
是针对对象的不同特性,而使用适合的算法,这里面并没有实际上的新算法产生。
内存中对象大致分为三种:夭折对象、老不死对象和 不灭对象
由于不灭对象的生命周期过长,因此分代搜集算法就是针对的JAVA堆而设计的,也就是针对夭折对象和老不死对象。
Java堆的对象回收:对于夭折对象和老不死对象。
夭折对象:这类对象朝生夕灭,存活时间短。因为复制算法要求对象成活率不能太高,所以最适合复制算法。一般的,使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的
(年轻代分为三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured))
针对老不死对象的特性,显然不再适合使用复制算法,因为它的存活率太高。因此一般针对老不死对象只能采用标记/整理或者标记/清除算法。
Java堆分成了两部分 新生代。年老代。
方法区的对象回收(不灭对象),方法区也被称为永久代
Java堆是GC回收关注的主要对象,对于不灭对象的回收已不属于分代搜集算法的内容。
这一部分区域的GC与年老代采用相似的方法,二者都是只能使用标记/清除和标记/整理算法。
回收的时机
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC),它们所针对的区域如下。
普通GC(minor GC):只针对新生代区域的GC。
全局GC(major GC or Full GC):针对年老代的GC,偶尔伴随对新生代的GC以及对永久代的GC。
由于年老代与永久代相对来说GC效果不好,而且二者的内存使用增长速度也慢,因此一般情况下,需要经过好几次普通GC,才会触发一次全局GC。
3.2.垃圾搜集器的分类
大致分为以下三类。
串行搜集器(serial collector):它只有一条GC线程,它在运行的时候需要暂停用户程序(stop the world)。
并行搜集器(parallel collector):它有多条GC线程,且它也需要暂停用户程序(stop the world)。
并发搜集器(concurrent collector):它有一条或多条GC线程,且它需要在部分阶段暂停用户程序(stop the world),部分阶段与用户程序并发执行。
:为什么Full GC的次数越来越多?
A:因此内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的垃圾回收
串行收集器只适用于小数据量的情况默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动的时候加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。
吞吐量优先的并行收集器
如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学计算和后台处理等。
响应时间优先的并发收集器
如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
hotspot中的垃圾搜集器
在hotspotJVM中,每一个种类的垃圾搜集器都有对应的实现,如下。
串行搜集器的实现:serial(用于新生代,采用复制算法)、serial old(用于年老代,采用标记/整理算法)
并行搜集器的实现:ParNew(用于新生代,采用复制算法)、Parallel Scavenge(用于新生代,采用复制算法)、Parallel old(用于年老代,采用标记/整理算法)
并发搜集器的实现:concurrent m7ark sweep[CMS](用于年老代,采用标记/清除算法)
可以看到,上面每一种垃圾搜集器都是针对不同内存区域所设计的,因为它们采用的算法不同,凡是用于新生代的都是使用的复制算法,而用于年老代的都是使用的标记/清除或者标记/整理算法。
类加载器
负责从classpath所在的目录加载class文件到内存中。类加载器在加载class文件的时候,仍然使用的IO流技术。
Java中使用ClassLoader类来表示类加载器对象。
JDK中存在三个类加载器,他们都是ClassLoader的子类
.类加载器组织结构
bootstrap class loader:JVM中的根加载器,C编写。负责加载jdk目录 jre下lib目录中的所有jar包
ExtClassLoader 称为扩展类加载器,它负责加载jdk目录中的jre目录中ext目录下的所有jar包。
APPClassLoader 称为应用程序类加载器,主要负责加载项目中所有java程序,及导入的第三方jar包。
父类委托机制:
当我们在程序需要使用某个类的时候,这时应该是先由APPClassLoader 加载,但是这个加载器它不加载,会先让它的父类加载器(ExtClassLoader )去加载,而ExtClassLoader 也不加载,会让bootstrap class loader加载,这时bootstrap class loader会在自己加载的范围中找有没有需要加载的那个类。如果找到了就加载,如果没有找到,这时会告诉子加载器,让子加载器去加载。
为何采用此机制
采用双亲委派模型使得java类随着类加载器一起具备了带有优先级的层次关系。如用户自己写了个java.lang.Object的类,并且放在程序的classpath中,那么系统中将出现多个不同的Object类,应用程序会变得一片混乱。
如何自定义一个类加载器
定义一个类继承ClassLoader
重写findClass方法
使用IO流读取指定路径中的class文件
使用输出流ByteArrayOutputStream,把数据写到内存的数组中
获取流中维护的数据的所有数据 (defineClass)
调用findClass方法,得到clazz,在使用clazz的newIntance()方法得到对象