垃圾回收机制
说到回收,就不得不说jvm的内存分配
jvm在给一个对象分配内存的时,会优先考虑将对象分配到eden区,如果是对象较大的情况下会直接将较大的对象放入老年代,在eden区的对象,在minorGC后还存活会进入survivor区,在survivor区里存活时间较长的对象会进入老年代里。
所以说一般说的垃圾回收有两种情况,一种是发生在新生代的minorGC,一种是发生在老年代的fullGC。
minorGC是发生在新生代的,这里主要使用的是复制算法,把eden区里的存活对象发在survivor区里,每次只是用其中的一个survivor区,每次minorGC把eden区和其中一个survivor区里的存活对象放在另外一个survivor对象。这个时候如果survivor区内存不足可以使用老年代做担保。
fullGC是发生在老年代的,在老年代的主要使用标记清除或者标记整理算法。
java的垃圾回收器比如cms和G1回收器
cms垃圾回收器是是并发的标记清除回收器。主要的优点是对停顿时间短,停顿短的原因是因为cms在标记清理的时候会让用户线程和GC线程交替运行,减少GC线程独占cpu的时间,这样虽然会导致收集过程变长,但也使得gc操作的用户的影响看上去不是那么明显。
主要会经过一个初始标记标记gcroot直接指向的对象,然后是线程的并发标记,在最后的清除前还有在最终标记一次,已防止在第一次标记之后的对象分配的变动。cms垃圾回收器的主要缺点是对cpu资源比较敏感,还有由于是使用的标记清楚算法,所以说会参数大量的空间碎片(碎片多就可能产生找不到足够大的空间分配大对象,也许会出现老年代还有很大的空间,由于碎片的缘故不得不进行一次fullGC操作)。还有就是无法处理浮动的垃圾,cms在并发清理阶段用户线程还是在运行的,程序的运行必然会又新的垃圾产生,这个时候产生的来及无法在本次的垃圾回收过程回收掉,只能留在下一次GC操作处理。(stop the world会发生在初始标记和重新标记的时候)
G1回收器是一个面向服务器的垃圾回收器,可以充分的利用服务器多核的优势缩短停顿时间比如其他收集器原本可能需要停顿java线程区执行gc线程,G1也可以利用并发的方式让用户线程继续运行。
G1回收器是把内存分为很多个heap区域,对这些区域局部,比如针对一个会多个区域使用复制算法,对整体的heap来看使用的是编辑整理算法。
G1回收器同样保留了分代的机制,与以往的分代不同的是,G1的分代不要求heap是一个连续的,只是在逻辑层面上的分代。
G1最厉害的一点是能简历可预测的停顿时间模型,G1跟踪各个区域里的垃圾堆积的价值大小,比如收回一个区域所获得的空间会很大,就认为这个区域的价值很大。在后台维护这样一个优先刘表,保证G1收集器在有限的时间里获取尽可能高的收集效率。
jvm的内存划分
首先内存里有一个程序计数器,这一部分是一个比较小的内存空间,这部分是不会出现内存溢出的内存区域,主要用来存放当前线程所执行的字节码指示器,字节码解释器工作时通过这个计数器来选区下一条需要执行的指令,也是一些异常处理,线程恢复功能的具体实现不可缺少的。
然后除去这一部分,jvm内存主要分为两个部分,就是堆和栈。
栈主要有虚拟机栈和本地方法栈,他们的作用很相似,只是一个是为用户的方法服务的,一个是为本地的native方法服务的。栈里面有局部变量表,可以存放各种基本数据类型和对象的引用。栈和程序计数器一样都是线程私有的,一般随着线程创建产生,随着线程死亡消失。
堆是jvm内存管理的主要地区,这里存放的是所有对象的实例。具体再分可以分为新生代老年代和永久代。永久带也就是常说的方法区。方法区里存放的是虚拟机加载的类信息,常量静态变量等数据。比如方法区里会划分一块运行时常量池,一般用来存放编译时的各种字面量比如创建一个字符串。
因为现在的垃圾收集器用的都是分代手机算法,所以新生代具体还可以再分为eden区和两个survivor区。
对象在内存中的分配适合内存分布的情况与垃圾回收算法是分不开的。
一种是使用一个指针指示当前的空间哪部分是可用的那部分不可用,另一种是维护一个空闲列表。这两种的方法的选择要看当前的有没有内存空间碎片,有没有碎片是不同的回收机制来决定的。
方法区与 Java 堆一样,是线程共享的区域,是用于存储已被虚拟机加载的类信息、常量、静态变量、编译后的代码这些数据。
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就会一直存在,方法区满的时候或者内存不够用的时候也会有一个类的回收。
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息,这里是用于存放编译期生成的各种字面量和符号引用。
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池,在更新的版本更是独立于堆的存在。
java一个对象的创建过程
一个对象的创建就是一个对象new出来的过程,它首先是检查该对象的类是否被加载,如果没有就执行一系列的类加载过程,已经加载的话就开始为新生的对象分配内存,分配的方式有两种,指针碰撞和空闲列表。这个时候其实还要考虑一个内存并发分配的问题。jvm在这里一般使用的是CAS加失败重试或者是预先为线程在Eden区分配一块内存,用完时在考虑CAS加失败重试。
分配内存结束后,jvm需要对分配到的内存空间都初始化为零值,这样java代码中很多实例字段不用赋初始值就可以直接使用了。
初始化零值后会创建对象头,对象头里会存放这个对象的类的信息,对象的哈希码,GC分代年龄等信息。还有当前是否启用偏向所,对象头也会有不同设置。
都执行玩后,对象开始执行程序员想要的一些初始化操作一个对象就创建完成了。
所以说一个对象可以分为三个部分,一个是对象头,实例数据还有个对其补充。有对齐补充的目的就是jvm的管理系统要求对象的大小是8字节的倍数。
java类加载过程
首先java类是判断什么时候时候需要加载:
一种情况是,java用new关键字去创建对象,发现对象对应的类未加载的时候需要加载;一种是用java的反射包对类发射调用的时候,类没有加载回去先加载对应的类;还有当初始化一个类的时候,如果发现其父类没有被初始化就会先初始化它的父类。程序运行的main入口对应的类也会被直接加载。
java虚拟机中类加载主要有五个过程,加载、验证、准备、解析和初始化。
加载的过程就是 通过要加载的类的完全限定名,产生一个代表该类型的二进制数据流,然后去解析这个二进制数据流为方法区内的运行时数据结构,最后创建一个表示该类型的java.lang.Class类的实例,作为方法区这个类的各种数据的访问入口。
验证是为了保证Class文件的字节流符合虚拟机的要求,并且不会危害虚拟机自身的安全。主要验证的是class文件文件格式是不是当前虚拟机能处理的,然后验证class里的语法是不是规范的,再查找代码里面没有有可能对虚拟机有危险的语句。还有类似代码里的修饰符是不是有什么限制。
准备阶段是给一些类常量分配内存空间,并把这些常量初始化成0值。
解析阶段主要做的是吧符号引用转化为直接引用。
初始化阶段真正开始执行类中定义的java程序代码。
java类加载器
启动类加载器(Bootstrap ClassLoader):
这个类加载器负责将存放在\lib目录中的类库加载到虚拟机内存中。
扩展类加载器(Extension ClassLoader):
它负责加载开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader):
它负责加载用户所指定的类库,开发者可以直接使用这个类加载器
双亲委派模型
双亲委派模型(Pattern Delegation Model),要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。这里父子关系通常是子类通过组合关系而不是继承关系来复用父加载器的代码。
双亲委派模型的工作过程: 如果一个类加载器收到了类加载的请求,先把这个请求委派给父类加载器去完成(所以所有的加载请求最终都应该传送到顶层的启动类加载器中),只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。
好处:
例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
如果没有双亲委派模型,而是由各个类加载器自己加载自己的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,就会造成类之间继承的混乱,危害jvm。