JVM学习

类加载器

加载,连接与初始化

在java代码中,类型的加载,连接与初始化过程都是在程序运行期间完成的。
        1.加载:类加载器并不需要等到某个类被”首次主动使用“时再加载它。JVM规范允许类加载器在预料某个类将要被使用时就预先加载他,如果预先加载过程中遇到。class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误);如果这个类一直没有被程序主动使用,那类加载器就不会报告错误。
        2.连接:验证——>将加载到内存中的类的二进制数据合并到虚拟机的运行时环境中去,检查数据正确性;准备——>为类的静态变量分配内存,并且设置默认的初始值。解析——>把类中的符号引用变成直接引用。
        3.初始化:执行类中初始化语句,为类的静态变量赋予初始值。(声明处初始化和静态代码块中初始化)初始化会按照代码的顺序执行。

java虚拟机与程序的生命周期,java虚拟机在下面四种情况将结束生命周期:
        1.执行了System.exit()方法;
        2.程序正常执行结束;
        3.程序执行过程中遇到了错误或错误异常终止;
        4.操作系统出错。

java程序对类的使用方式:
        一、主动使用:
                1,创建类的实例;
                2,访问每个类或者接口的静态变量,或对静态变量赋值;
                3,调用类的静态方法;
                4,反射;
                5初始化一个类的子类(接口不需要,初始化接口的子接口父接口不会初始化,当真正用到父接口这时候才会被初始化);
                6,jvm启动时被标明为启动类;
                7,动态语言支持)   
        二、被动使用。

        所有的java虚拟机实现必须在每个类或接口被java程序“首次主动使用”时才初始化他们。当一个类调用另一个类的静态final修饰的可以确定的常量时,会将这个常量存入自己类的常量池中,只需调用常量池中的数据即可,并不需要初始化另一类。但是如果另一类的静态final修饰的常量不能确定时,那么其值不会被放到调用类的常量池中,这时程序运行时,还会主动使用这个常量所在的类,显然还会导致这个类被初始化。                                                                          当一个类中创建一个另一类的数组,被创建泛型的那个类不会被初始化。例如:在非Test1类中使用Test1 test1 = new Test1();Test1会被初始化。但是如果时数组Test1[] test1 = new Test1[2];那么Test1不会被初始化。集合亦是如此。数组中的Class对象运行并不是由classloader加载的,是由jvm根据需要动态生成的,调用数组的class得到getClassLoader与数组中任意一个对象的getClassLoader是一样的。如果数组中元素类型是个原生类型,那这个数组类是没有classloader,调用getClassLoader返回为null。

当用子类去调用父类的静态常量或者静态方法时,只表示对父类的主动使用,而不是对子类的主动使用,子类并不会初始化。

               de30f888d4114b93031f16ffb7f19bd0ea3.jpg 

-XX:+TraceClassLoading 用于类的加载信息并打印出来。-XX:+TraceClassUnloading用于类的卸载信息并打印出来。

两种类加载器:

        java虚拟机自带的类加载器:根类加载器(bootstrap) 扩展类加载器(Extension) 系统类加载器(System)。
        用户自己定义的类加载器:用户自己定制类的加载方式(java.lang.ClassLoader的子类)。

类加载器的运行机制

        jdk1.2开始采用的是父亲委托机制,除了虚拟机自带的根类加载器之外,其余的类加载器有且只有一个父加载器。当java程序请求加载器loader1在在Sample类时,loader1首先会委托自己的父加载器去加载Sample类,若父加载器能加载,就让父加载器完成任务,否则才让加载器本身加载Sample类(一般我们自己不会定义类加载器,所有的类基本上是App ClassLoader帮我们加载)。 如果用自己定义的classloader去加载一个类,该类中还有其他类需要加载的话,定义的classloader会把其他的类交给父加载类执行,一级级递上去,如果父加载类无法加载,才会让定义的classloader执行,如果还无法加载,那就报NoClassDefFoundError错误。

                                 a208b0c5605ee8635fec6e8508b1d725018.jpg                      082825fa53ed88d6fbb8afb57a75379481e.jpg

为什么要采用父亲委托机制:
        1.可以确保java核心库的类型安全:所有java应用都至少会引用java.lang.Object类,也就是说在运行期间,java.lang.Object这个类会被加载到java虚拟机中;如果这个加载过程是由java应用自己的类加载器所完成的,那么很可能就会在jvm中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容的,相互不可见的(正是命名空间发挥作用的)。借助双亲委托机制,java核心类库中的类的加载工作都是由启动类加载器来统一完成的,从而确保了java应用所使用的都是同一个版本的java核心库,他们之间是相互兼容的。
        2.可以确保java核心类库所提供的类不会被自定义的类所取代。
        3.不同的类加载器可以为相同的名称(binary name)的类创建额外的命名空间,相同名称的类可以并存在java虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的,这就是相当于在java虚拟机内部创建了一个又一个相互隔离的java类空间,这类技术在很多框架中都得到了很多实际应用。

调用类的加载器去加载类仅仅只是对类的一个加载,并不会对类进行初始化。比如:

systemClassLoader.loadClass("com.yaoxiaojian.jvm.MyTest11In");//这样不会对MyTest11In类进行初始化,仅仅会加载。
Class.forName("com.yaoxiaojian.jvm.MyTest11In");//这样会对类初始化,反射也是7中主动使用之一。

        如果自己定义一个classloader去加载classpath目录下面某个类,其实根据加载器的双亲委托机制,也是将这个类交给定义的classloader的父类去加载,一般是appclassloader加载这个类。想让自己定义的classloader去加载某个类,一般不要把类放在appclassloader加载的哪个目录下。当在同一个命名空间内(也就是同一个classloader去加载),一个类最多被加载一次。
        命名空间——每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
        重要说明:子加载器所加载的类能够访问父加载器所加载得类;父加载器所加载的类无法访问到子加载器所加载的类。
                同一个命名空间内的类是相互可见的。子加载器的命名空间包含了父加载器的命名空间。因此子加载器加载的类能看见父加载器加载的类。例:appclassloader加载器加载的类能看见bootstrap classloader加载器加载的类。由父加载器加载的类不能看见子加载器加载的类,如果两个加载器之间没有直接或者间接的父子关系,那么他们各自加载的类相互不可见。

bootstrap classloader 和 extension classloader 和 app classloader 又是谁来加载的?
        bootstrap会加载java.lang.ClassLoader以及其他的java平台类,当jvm启动时,一块特殊的机器码会运行,它会加extension 和 app  classloader,这块特殊的机器码叫做启动类加载器。(启动类加载器不是java类,是特定于平台的机器指令(c++),负责开chen启整个加载过程) 每个类都会使用自己的类加载器(即加载自身的类加载器)来加载其他类(指的是所依赖的类),如果classX引用了classY,那么classX的类加载器就会去加载classY(前提是classY尚未被加载)。

线程类中的上下文加载器(context classloader)

        线程上下文类加载器是从jdk1.2开始引入的,类Thread中的getContextClassLoader()和setContextClassLoader()进行设置的,如果没有setContextclassLoader()进行设置的话,线程将继承其父线程的上下文的加载器。java应用运行时的初始线程的上下文加载器是系统类加载器。线程中运行的代码可以通过该类加载器来加载类与资源。
        在java的jdk中有大量的SPI实现,由于这些实现是父classloader可以使用当前线程Thread.currentThread().getContextClassLoader()所制定的classloader加载的类,这就改变了父classloader不能使用子classloader或者其他没有直接父子关系的classloader加载的类的情况,即改变了双亲委托模型。
        线程上下文类加载器就是当前线程的Current ClassLoader。
        在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载,但是对于SPI来说,有些接口是java核心库提供的,而java核心库是由启动类加载器来加载的,而这些接口的实现却来自不同的jar包(厂商提供),java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI要求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载 。
        当高层提供了统一的接口去让底层实现,同时又要在高层加载(或实例化)低层的类时,就必须要通过线程上下文类加载器帮助高层的classloader找到并且加载该类。  

类的卸载——只有自己定义的classloader去加载的类才能被卸载。当代表类的class对象不再被引用,即不可触及时,class对象就会结束生命周期,该类在方法区内的数据也会被卸载,从而结束生命周期。

字节码

        1:使用javap -verbose命令分析一个字节码文件时,将会分析该字节码文件的魔数,版本号,常量池,类信息构造方法,类中的方法信息,类变量与成员变量等信息。
        2:魔数:所有的.class字节码文件的前4个字节码都是魔数,魔数值为固定值:0xCAFEBABE;
        3:魔数之后的4个字节为版本信息,前两个字节表示minor version(次版本号)。后两个字节表示major version(主版本号);
        4:常量池(constant pool):紧接着主版本号之后得就是常量池入口。一个java类中定义的很多信息都是由常量池来维护和描述的。可以将常量池看作是class文件的资源仓库,比如说java类中定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用。字面量如文本字符串,java中申明为final的常量值等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。
        5:常量池的总体结构:java类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分共同构成的。常量池数量紧跟在主版本号后面,占据2个字节;常量池数组则紧跟在常量池数量之后。常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型,结构都是不同的,长度当然也就不同;但是,每一种元素的第一中元素都是一个u1类型,该字节是个标志位,占据1个字节。jvm在解析常量池时,会根据这个u1类型来获取元素的具体类型。值得注意的是,常量池数组中元素的个数=常量池数-1(其中0暂时不使用)。目的是满足某些常量池索引值的数据在特定情况下需要表达【不引用任何一个常量池】的含义:根本原因在于。索引位0也是一个常量,只不过他不位于常量表中,这个常量就对应null值;所以,常量池的索引从1开始而非0开始。

内存空间 

                                       5c49a5933b5f633bdde73c0a386da4b420d.jpg

虚拟机栈:是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack  Frame),用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
程序计数器:(Program Counter):一块较小的内存空间,是当前线程所执行的字节码的信号指示器,每一条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。这个内存时唯一一个在虚拟机中没有任何outofmemoryError情况的区域。
本地方法栈:本地方法栈和虚拟机栈作用类似,区别是Java stack为执行java方法服务,而本地方法栈则为native方法服务。如果一个jvm实现使用C-linkage模型来支持native调用,那么该栈将会是一个c栈,但是hotstop jvm直接就把本地方发栈和虚拟机栈合二为一了。
堆(heap):jvm管理的最大一块内存空间。被线程共享的一块内存区域,创建对象和数组都保存在java堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代jvm采用的分代收集算法,因此java堆从GC的角度还可以细分为:新生代(Eden区  from survivor区 to survivor区 )和老年代
方法区(Method Area)/永久代:存储元信息。永久代(Permenent Generation),从JDK1.8开始,已经彻底废弃永久代,使用元空间(meta space,元空间并不在虚拟机中而是在本地内存)。用于存储被jvm加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,hotspot jvm把GC分代收集扩展至方法区,即使用java的堆的永久代来实现方法区,这样hotspot jvm就能像java堆一样管理这部分内存而不必为方法区开发专门的内存管理器。
运行时常量池:方法区的一部分。用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池。

jvm运行时的内存

        java堆从GC的角度还可以细分为Eden区 from survivor区  to survivor区和老年区。如图:

                   41e38158ee5f8986c28994e99f1e9e62c38.jpg

新生代:是用来存放新生的对象。一般占用堆的1/3空间。由于频繁创建对象,所以新生代频繁触发GC进行垃圾回收。新生代又分为Eden   ServivorFrom  ServivorTo三个区。
        Eden区:java新对象的出生地(如果创建该对象占用的内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
        ServivorFrom区:上一次GC的幸存者,作为这一次GC的被扫描者(也就是图中的S0区域)。
        ServivorTo区:保留了一次MinorGC过程中的幸存者(也即是图中的S1区域)。
老年代:主要存放应用程序中生命周期常得内存对象。
永久代:指内存的永久保存区域,主要放class和meta(元数据)的信息,class在被加载的时候被放入永久区域,它和存放实例的区域不同,GC不会在主程序运行期间对永久代区域进行清理。所以这也导致了永久代区域会随着class的增多而膨胀,最终抛出OOM异常。

GC垃圾回收算法

        引用计数法:一个对象如果没有任何与之关联的引用,即他们的引用为0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
        可达性分析:通过一系列的“GC  roots”对象作为起点搜索。如果在“GC  roots”和一个对象之间没有可达路径,则称该对象是不可达的,不可达对象变为可回收对象至少姚经过两次标记过程。两次标记后仍然时可回收对象,则将面临回收。
        标记清除算法:最基础的垃圾回收算法,分为标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的的对象所占用的空间。缺点:内存碎片化严重,大对象找不到可利用空间的问题。
        复制算法:将内存空间平分成两块,每次只使用其中一块,当这块内存满后将还存活的对象复制到另一边,把已使用的内存清掉。这算法解决了标记清除算法的缺点。
        分代收集算法:
目前大部分jvm所采用的方法,核心是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代和新生代。老生代每次垃圾回收只有少量对象需要被回收,新生代的特点是每次垃圾回收都有大量垃圾需要被回收,因此根据不同的区域选择不同的算法。(目前采用较多的是新生代采用copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少。一般将新生代划分为一块较大的Eden空间和两块较小的From和to空间),每次使用Eden空间和其中一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块survivor空间中。
                1. JAVA虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储class类, 常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。 
                2. 对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目 前存放对象的那一块),少数情况会直接分配到老生代。 
                3. 当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。 
                4. 如果To Space无法足够存储某个对象,则将这个对象存储到老生代。 
                5. 在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。 
                6. 当对象在Survivor区躲过一次GC 后,其年龄就会+1。默认情况下年龄到达15 的对象会被 移到老生代中。 

 

转载于:https://my.oschina.net/yaojianpeng/blog/3060236

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值