2、类的初始化
1)类什么时候才被初始化:
- 创建类的实例,也就是new一个对象。
- 访问某个类或接口的静态变量,或者对该静态变量赋值。
- 调用类的静态方法。
- 反射(Class.forName(“com.lyj.load”))。
- 初始化一个类的子类(会首先初始化子类的父类)。
- JVM启动时标明的启动类,即文件名和类名相同的那个类。
2)类的初始化顺序
- 如果这个类还没有被加载和链接,那先进行加载和链接
- 假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
- 加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
- 总的来说,初始化顺序依次是:
(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;
如果有父类,则顺序是:父类的静态变量 –> 父类的静态代码块 –> 子类的静态变量 –> 子类的静态代码块 –> 父类的非静态变量 –> 父类的非静态代码块 –> 父类的构造方法 –> 子类的非静态变量 –> 子类的非静态代码块 –> 子类的构造方法。
3、类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的java.lang.Class对象,用来封装类在方法区类的对象。如:
类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。加载类的方式有以下几种:
- 从本地系统直接加载。
- 通过网络下载.class文件。
- 从zip,jar等归档文件中加载.class文件。
- 从专有数据库中提取.class文件。
- 将Java源文件动态编译为.class文件(服务器)。
4、加载器
JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
加载器介绍:
1)BootstrapClassLoader(启动类加载器):
负责加载JAVA_HOME中jre/lib/rt.jar里所有的class,加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar。
2)ExtensionClassLoader(标准扩展类加载器):
负责加载java平台中扩展功能的一些jar包,包括JAVAHOME中jre/lib/rt.jar里所有的class,加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar。
2)ExtensionClassLoader(标准扩展类加载器):负责加载java平台中扩展功能的一些jar包,包括JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。载System.getProperty(“java.ext.dirs”)所指定的路径或jar。
3)AppClassLoader(系统类加载器):
负责加载classpath中指定的jar包及目录中class。
4)CustomClassLoader(自定义加载器):
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。
类加载器的顺序
- 加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
- 在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。
- Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null。
5、类加载器之双亲委派模型
- 所谓的双亲委派模型指除了启动类加载器以外,其余的加载器都有自己的父类加载器,而在工作的时候,如果一个类加载器收到加载请求,他不会马上加载类,而是将这个请求向上传递给他的父加载器,看父加载器能不能加载这个类,加载的原则就是优先父加载器加载,如果父加载器加载不了,自己才能加载。
- 因为有了双亲委派模型的存在,类似Object类重复多次的问题就不会存在了,因为经过层层传递,加载请求最终都会被Bootstrap ClassLoader所响应。加载的Object对象也会只有一个。并且面对同一JVM进程多版本共存的问题,只要自定义一个不向上传递加载请求的加载器就好啦。
垃圾回收机制
Java内存区域划分
我们先来看看Java的内存区域划分情况,如下图所示:
私有内存区的区域名和相应的特性如下表所示:
虚拟机栈中的局部变量表里面存放了三个信息:
- 各种基本数据类型(boolean、byte、char、short、int、float、long、double)。
- 对象引用(reference)。
- returnAddress地址。
这个returnAddress和程序计数器有什么区别?前者是指示JVM的指令执行到了哪一行,后者是指你的代码执行到哪一行。
共享内存区(接下来主要讲jdk1.7)的区域名和相应的特性如下表所示:
哪些内存需要回收?
私有内存区伴随着线程的产生而产生,一旦线程中止,私有内存区也会自动消除,因此我们在本文中讨论的内存回收主要是针对共享内存区。
Java堆
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC (但非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
新生代:刚刚新建的对象在Eden中,经历一次Minor GC, Eden中的存活对象就被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC, Eden和S0中的存活对象会被复制送入第二块survivor space S1。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就被送到老年代中。
为什么新生代内存需要有两个Sruvivor区:
先不去想为什么有两个Survivor区,第一个问题是,设置Survivor区的意义在哪里?
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。你也许会问,执行时间长有什么坏处?频发的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连接会因为超时发生连接错误了。那我们来想想在没有Survivor的情况下,有没有什么解决办法,可以避免上述情况:
显而易见,没有Survivor的话,上述两种解决方案都不能从根本上解决问题。我们可以得到第一条结论:Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
设置两个Survivor区最大的好处就是解决了碎片化,下面我们来分析一下。为什么一个Survivor区不行?
第一部分中,我们知道了必须设置Survivor区。假设现在只有一个survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
那么,顺理成章的,应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。
参考文章:https://blog.csdn.net/antony9118/article/details/51425581
老年代:如果某个对象经历了几次垃圾回收之后还存活,就会被存放到老年代中。老年代的空间一般比新生代大。
这个流程如下图所示:
什么时候回收?
Java并没有给我们提供明确的代码来标注一块内存并将其回收。或许你会说,我们可以将相关对象设为null或者用System.gc()。然而,后者将会严重影响代码的性能,因为每一次显示调用system.gc()都会停止所有响应,去检查内存中是否有可回收的对象,这会对程序的正常运行造成极大威胁。
另外,调用该方法并不能保障JVM立即进行垃圾回收,仅仅是通知JVM要进行垃圾回收了,具体回收与否完全由JVM决定。
生存还是死亡
可达性算法:这个算法的基本思路是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
二次标记:在可达性分析算法中被判断是对象不可达时不一定会被垃圾回收机制回收,因为要真正宣告一个对象的死亡,必须经历两次标记的过程。
如果发现对象不可达时,将会进行第一次标记,此时如果该对象调用了finalize()方法,那么这个对象会被放置在一个叫F-Queue的队列之中,如果在此队列中该对象没有成功拯救自己(拯救自己的方法是该对象有没有被重新引用),
那么GC就会对F-Queue队列中的对象进行小规模的第二次标记,一旦被第二次标记的对象,将会被移除队列并等待被GC回收,所以finalize()方法是对象逃脱死亡命运的最后一次机会。
在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
GC的算法
引用计数法(Reference Counting):
给对象添加一个引用计数器,每过一个引用计数器值就+1,少一个引用就-1。当它的引用变为0时,该对象就不能再被使用。它的实现简单,但是不能解决互相循环引用的问题。
优点:
- 及时回收无效内存,实时性高。
- 垃圾回收过程中无需挂起。
- 没有全局扫描,性能高。
缺点:
- 对象创建时需要更新引用计数器,耗费一部分时间。
- 浪费CPU资源,计数器统计需要实时进行。
- 无法解决循环引用问题,即使对象无效仍不会被回收。
标记-清除(Mark-Sweep)算法:
分为两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象(后续的垃圾回收算法都是基于此算法进行改进的)。
缺点:效率问题,标记和清除两个过程的效率都不高;空间问题,会产生很多碎片。
复制算法:
将可用内存按容量划分为大小相等的两块,每次只用其中一块。当这一块用完了,就将还存活的对象复制到另外一块上面,然后把原始空间全部回收。高效、简单。
缺点:将内存缩小为原来的一半。
标记-整理(Mark-Compat)算法
标记过程与标记-清除算法过程一样,但后面不是简单的清除,而是让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
分代收集(Generational Collection)算法
新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代中,其存活率较高、没有额外空间对它进行分配担保,就应该使用“标记-整理”或“标记-清除”算法进行回收。
增量回收GC和并行回收GC这里就不做具体介绍了,有兴趣的朋友可以自行了解一下。
垃圾收集器
Serial收集器:单线程收集器,表示在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。"Stop The World"。
ParNew收集器:实际就是Serial收集器的多线程版本。
- 并发(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并行(Concurrent):指用户线程与垃圾收集线程同时执行,用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
Parallel Scavenge收集器:该收集器比较关注吞吐量(Throughout)(CPU用于用户代码的时间与CPU总消耗时间的比值),保证吞吐量在一个可控的范围内。
CMS(Concurrent Mark Sweep)收集器:CMS收集器是一种以获取最短回收停顿时间为目标的垃圾收集器,是基于“标记——清除”算法实现的。
其回收过程主要分为四个步骤:
- 初始标记:标记一下GC Roots能直接关联到的对象,速度很快。
- 并发标记:进行GC Roots Tracing的过程,也就是标记不可达的对象,相对耗时。
- 重新标记:修正并发标记期间因用户程序继续运作导致的标记变动,速度比较快。
- 并发清除:对标记的对象进行统一回收处理,比较耗时。
由于初始标记和重新标记速度比较快,其它工作线程停顿的时间几乎可以忽略不计,所以CMS的内存回收过程是与用户线程一起并发执行的。初始标记和重新标记两个步骤需要Stop the world;并发标记和并发清除两个步骤可与用户线程并发执行。“Stop the world”意思是垃圾收集器在进行垃圾回收时,会暂停其它所有工作线程,直到垃圾收集结束为止。
CMS的缺点:
- 对CPU资源非常敏感;也就是说当CMS开启垃圾收集线程进行垃圾回收时,会占用部分用户线程,如果在CPU资源紧张的情况下,会导致用户程序的工作效率下降。
- 无法处理浮动垃圾导致又一次FULL GC的产生;由于CMS并发回收垃圾时用户线程同时也在运行,伴随用户线程的运行自然会有新的垃圾产生,这部分垃圾出现在标记过程之后,CMS无法在当次收集过程中进行回收,只能在下一次GC时在进行清除。所以在CMS运行期间要确保内存中有足够的预留空间用来存放用户线程的产生的浮动垃圾,不允许像其它收集器一样等到老年代区完全填满了之后再进行收集;那么当内存预留的空间不足时就会产生又一次的FULL GC来释放内存空间,由于是通过Serial Old收集器进行老年代的垃圾收集,所以导致停顿的时间变长了(系统有一个阈值来触发CMS收集器的启动,这个阈值不允许太高,太高反而导致性能降低)。
- 标记——清除算法会产生内存碎片;如果产生过多的内存碎片时,当系统虚拟机想要再分配大对象时,会找不到一块足够大的连续内存空间进行存储,不得不又一次触发FULL GC。
G1(Garbage First)收集器:G1收集器是一款成熟的商用的垃圾收集器,是基于“标记——整理”算法实现的。
其回收过程主要分为四个步骤:
- 初始标记:标记一下GC Roots能直接关联到的对象,速度很快。
- 并发标记:进行GC Roots Tracing的过程,也就是标记不可达的对象,相对耗时。
- 最终标记:修正并发标记期间因用户程序继续运作导致的标记变动,速度比较快。
- 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
G1收集器的特点:
- 并发与并行:机型垃圾收集时可以与用户线程并发运行。
- 分代收集:能根据对象的存活时间采取不同的收集算法进行垃圾回收。
- 不会产生内存碎片:基于标记——整理算法和复制算法保证不会产生内存空间碎片。
- 可预测的停顿:G1除了追求低停顿时间外,还能建立可预测的停顿时间模型,便于用户的实时监控。
CMS收集器与G1收集器的区别:
- CMS采用标记——清除算法会产生空间碎片,G1采用标记——整理算法不会产生空间碎片。
- G1可以建立可预测的停顿时间模型,而CMS则不能。
JDK 1.8 JVM的变化
1、为什么取消方法区
- 它在启动时固定大小,很难进行调优,并且FullGC时会移动类元信息。
- 类及方法的信息等比较难确定大小,因此对永久代的大小指定比较困难。
- 在某些场景下,如果动态加载类过多,容易造成Perm区的OOM。
- 字符串存在方法区中,容易出现性能问题和内存溢出。
- 永久代GC垃圾回收效率偏低。
2、JDK 1.8里Perm区中的所有内容中字符串常量移至堆内存,其他内容如类元信息、字段、静态属性、方法、常量等都移动到元空间内。
3、元空间
元空间(MetaSpace)不在堆内存上,而是直接占用的本地内存。因此元空间的大小仅受本地内存限制
也可通过参数来设定元空间的大小:
- -XX:MetaSpaceSize 初始元空间大小
- -XX:MaxMetaSpaceSize 最大元空间大小
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。
元空间的特点:
- 每个加载器有专门的存储空间。
- 不会单独回收某个类。
- 元空间里的对象的位置是固定的。
- 如果发现某个加载器不再存货了,会把相关的空间整个回收。
性能优化:
- 减少new对象。每次new对象之后,都要开辟新的内存空间。这些对象不被引用之后,还要回收掉。因此,如果最大限度地合理重用对象,或者使用基本数据类型替代对象,都有助于节省内存。
- 多使用局部变量,减少使用静态变量。局部变量被创建在栈中,存取速度快。静态变量则是存储在堆内存中。
- 避免使用finalize,该方法会给GC增添很大的负担。
- 如果是单线程,尽量使用非多线程安全的,因为线程安全来自于同步机制,同步机制会降低性能。例如,单线程程序,能使用HashMap,就不要使用HashTabl。同理,尽量减少使用synchronized。
- 用移位符号替代乘除号。比如:a*8应该写作a<<3。
- 对于经常反复使用的对象使用缓存。
- 尽量使用基本类型而不是包装类型,尽量使用一维数组而不是二维数组。
- 尽量使用final修饰符,final表示不可修改,访问效率高。
- 单线程下(或者是针对于局部变量),字符串尽量使用StringBuilder,比StringBuffer要快。
- 尽量使用StringBuffer来连接字符串。这里需要注意的是,StringBuffer的默认缓存容量是16个字符,如果超过16,append方法调用私有的expandCapacity()方法,来保证足够的缓存容量。因此,如果可以预设StringBuffer的容量,避免append再去扩展容量。
java自动装箱拆箱总结
当基本类型包装类与基本类型值进行==运算时,包装类会自动拆箱。即比较的是基本类型值。
具体实现上,是调用了Integer.intValue()方法实现拆箱。
int a = 1; Integer b = 1; Integer c = new Integer(1); System.out.println(a == b); //true System.out.println(a == c); //true System.out.println(c == b); //false Integer a = 1; 会调用这个 Integer a = Integer.valueOf(1); Integer已经默认创建了数值【-128到127】的Integer常量池 Integer a = -128; Integer b = -128; System.out.println(a == b); //true Integer a = 128; Integer b = 128; System.out.println(a == b); //false Java的数学计算是在内存栈里操作的 c1 + c2 会进行拆箱,比较还是基本类型 int a = 0; Integer b1 = 1000; Integer c1 = new Integer(1000); Integer b2 = 0; Integer c2 = new Integer(0); System.out.println(b1 == b1 + b2); //true System.out.println(c1 == c1 + c2); //true System.out.println(b1 == b1 + a); //true System.out.println(c1 == c1 + a); //true
以上这些,答案总结并非标准,仅供参考,如果有错误或者更好的见解,欢迎留言讨论,往期公众号整理的一些面试题看这里:Java面试题内容聚合