一.对象创建过程
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锁是使用操作系统的信号量机制来实现的,需要切换内核态,并且使用锁会导致线程间切换,非常消耗时间,所以对锁进行优化。