java虚拟机相关(对象创建、垃圾回收、类加载、双亲委派模型、锁优化)

一.对象创建过程

  1.虚拟机遇到new指令时,检查new后边的类符号,是否在常量池中能找到,然后检查这个类是否执行了类加载过程,没有的话就先执行类加载,

  2.在类加载以后就确定了要分配的内存,然后根据堆的结构(碰撞指针、空闲链表)来分配内存,分配时还要考虑并发的问题,可能两个线程的

两个对象分配到一个内存了,解决是采用cas加失败重试,或者使用TLAB(ThreadLocal),在创建线程时给线程在堆中分配一小块空间,等空间

用完了以后,再使用CAS加失败重试分配内存。

  3.最后将哈希码、对象属于哪个类的实例、对象的GC分代年龄等信息存到对象头中。到这一步,虚拟机创建对象完毕,接下来要执行构造函数,初始化等过程。

二.对象实例的组成

  对象头

    1.存储对象运行时所需数据:对象锁状态、哈希码、GC分代年龄

    2.类型指针,通过这个找到对象属于哪个实例。

  对象内容

  对齐填充

    有的要求对象大小是8字节的整数倍。

三.对象的使用

  表面上是引用,但是引用有两种类型:指针  句柄。

  1.指针 

  引用直接是对象在堆中的地址,好处是访问快,坏处是对象在堆中地址总是变(由于垃圾回收机制,换代),就要修改引用的值。

  2.句柄

  引用指向堆中的句柄表,通过句柄表存着对象的地址,和对象所属类的地址(方法区),好处不用变引用的值,坏处效率没指针高。

四.判断对象是否存活的方法

  引用计数法

    对象维护一个整型,有个引用加一,少个引用减一,为零的时候就死亡了。不行,因为存在循环引用,两个对象都只有对方引用。

  可达性分析算法

五.引用类型

  有这样一类对象,当内存紧张时他们不能存活,内存宽裕时,就可以留着(比如缓存功能),所以就有了引用类型,抛弃了要么有引用就一直有要么没引用就要被回收的两种极端。

  强引用:正常使用的引用类型。

  软引用:指向一些有用但非必须的对象,当要发生内存溢出异常前,把这些回收了。

  弱引用:更弱一些,逃避这次垃圾回收,下次就没了。

  虚引用:照样回收,唯一作用回收时发个通知。

六.回收过程

  可达性算法发现,没人引用某个对象,就会把它标记一下,然后判断finalize()方法是否有(一个对象只能执行一次),需要的话就把这些对象放到f-queue队列中,再标记一下,然后

虚拟机创建一个线程执行他们(不保证成功结束,因为有死循环会影响到其他对象),这时候要是在finalize中能和引用链建立上链接(让一个引用链对象引用它)就逃脱了,没有的话就被回收了。

七.GC回收机制

  现在大部分虚拟机都是将堆区分代进行回收管理的。

  新生代:包括一个eden区,两个survivor区。采用复制算法,minor GC。

  老年代:old区。标记-整理算法 major GC。

  fullGC全清理

  垃圾回收过程:当向eden区存入对象失败时,就根据可达性分析算法判断,eden区和survivorfrom区存活的对象,复制到survivorTo区,该过程叫做minorGC,很常见不一定等eden满才使用。

  等挺过执行minorGC超过15次后,将依然存活者的对象存到老年代,老年代实际上提供一种担保机制,正常98%的对象都是朝生夕死,所以新生代区域设置为8:1:1,足矣了。大对象(数组)或者

存活时间长的对象直接存到老年代,等老年代满的时候触发FullGC清理老年代。

  在进行minorGC前会判断老年代能否装下新生代对象,不能的话jiupanduanyigeflag允许担保失败不,要是允许就判断以前每次续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,可以进进行,不可以就fullGC。

  可达性算法:

六类加载过程

  步骤:加载、验证、准备、解析、初始化、使用、卸载。2.3.4步又叫连接

       执行初始化的时机

    1.new对象或者调用静态代码时

    2.通过反射创建对象时

    3.执行子类对象的创建过程时,要执行父类初始化

    4.虚拟机启动初始化main所属类的初始化。

  加载:通过类的全限定类名将类转换为二进制字节流,生成class对象。

  验证:判断字节流文件信息是否符合要求,是否会危害到java虚拟机本身。

  准备:将类变量分配内存(方法区),并清空为零。

  解析:设置常量池的类引用

  初始化:执行程序的初始化代码。

类加载器:执行类的加载步骤,通过代码使用类的全限定类名找到类,将其转换成二进制字节流。

双亲委派模型

  该模型指的是类加载器的继承体系(不是真的继承是组合实现的),接下来介绍下类加载器

启动类加载器:虚拟机自带(C++编写),可以加载<java_HOME>/lib目录下的类库加载到虚拟机中

扩展类加载器:<java_HOME>/lib/ext目录下的类库

应用程序类加载器:加载用户类路径下的类库,又叫系统类加载器。

其他用户自己编写的类加载器:都是组合的方式继承应用程序类加载器。

类加载过程:当一个类加载收到加载请求时,他会委托父类加载器处理,父类矣如此,直到最顶层说,这个路径下的我加载不了,子类才会尝试加载。

采用双亲委派模式的好处:一个类被他的加载器和本身唯一标识,如果不用这种模型,那么两个加载器加载某个类,就会出现同样的类不一致问题,双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。

双亲委派模型的破坏:在双亲委派模型引入之前的jdk1.2前就有了自定义类加载器,这时候用户就会重写loadClass()进行类加载,而在引入模型后,该方法会调用父类加载器,不能被覆盖,所以,就

新增了一个findclass方法呼吁在这个方法中重写。

2.在实现jdbc时:原来是class.forName()

然后再manager管理driver,但后来,

         // 1.加载数据访问驱动
        Class.forName("com.mysql.jdbc.Driver"); //2.连接到数据"库"上去 Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } } }
这是之前,Class.forName()其实触发了静态代码块,然后向DriverManager中注册了一个mysql的Driver实现。
但后来
 Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

可以看到这里直接获取连接,省去了上面的Class.forName()注册过程。
现在,我们分析下看使用了这种spi服务的模式原本的过程是怎样的:

  • 第一,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
  • 第二,加载这个类,这里肯定只能用class.forName("com.mysql.jdbc.Driver")来加载

好了,问题来了,Class.forName()加载用的是调用者的Classloader,这个调用者DriverManager是在rt.jar中的,ClassLoader是启动类加载器,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。

线程上下文类加载器可以通过Thread.setContextClassLoaser()方法设置,如果不特殊设置会从父类继承,一般默认使用的是应用程序类加载器

很明显,线程上下文类加载器让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派模型的原则


锁的优化:因为java锁是使用操作系统的信号量机制来实现的,需要切换内核态,并且使用锁会导致线程间切换,非常消耗时间,所以对锁进行优化。
  优化锁的获取方式:自适应自旋锁:得不到锁就要阻塞等待,很浪费时间,并且经过研究发现大部分线程占用锁的时间会很短,所以在获取某共享数据时,我们可以在一定时间内循环判断锁,不用阻塞,并且根据这个变量上次自选的时间,自选成功率高,那就自选久一点,这就叫自适应。
  锁消除:在同步块中不会出现对共享变量的多个访问,虚拟机就会把锁直接消除掉,比如stringbuffer。append()方法,内部就是同步块,而在方法内部调用这个方法就会自动消除。
  锁粗化:锁的粒度大多数情况下是越细越好,但是要是只锁一个循环里的某条语句,则每次进行循环都要加锁解锁,就会将锁粗化,上个例子不消除粗化也可以。
  轻量级锁:判断对象头中的锁标志位要是01就是没有被锁,创建一个锁记录存储对象头中的信息,然后用cas操作将对象头mark word更新为指向锁记录的指针,如果更新失败,就判断哪个指针指的要是自己的线程就直接进入同步块执行,否则膨胀为重量级锁,更新成功也进去执行。解锁状态就使用cas把锁记录和对象头内容交换,失败的话就说明有人动过了,就要唤醒线程。
  偏向锁:虚拟机设置开启偏向锁参数后,线程第一次获取锁对象后,将对象头的锁标志设为”01“偏向模式,并将线程id记录到都对象头内,这样该线程以后每次都可以直接访问锁对象。如果有其他线程访问的话,根据锁对象当前是否被使用,如果没有被使用,将偏向擦除,如果被使用转换为轻量级锁,重复上例操作。


猜你喜欢

转载自www.cnblogs.com/gmzqjn/p/11761026.html