一. 类的生命周期:加载→连接→初始化→使用→销毁
连接 包括:
①验证(确保被加载的类的正确性)
②准备(为静态变量分配内存,并将其初始化为默认值(指0,false,null等),同时将static final值转换为具体的常量
③初始化:为静态变量执行赋值语句(static,非final),执行静态块
PS:初始化只有在对类主动使用时才会执行,包括以下:
①new
②访问静态域的时候
③反射(如Class.forName)
④初始化某个类时,其父类也会初始化
⑤运行main方法时,该类加载
类加载器:
①Bootstarp ClassLoader(启动类加载器,由C++实现的类,无法获取实例)
负责加载JDK下/lib的类库(如java.*等关键类,是最底层的Class Loader)
②Extension ClassLoader(拓展类加载器
负责加载JDK下 /lib/ext目录,即额外的类库
③Application ClassLoader(应用程序类加载器负责加载用户类路径的类,即ClassPath
④其他自定义的类加载器 User Class Loader
PS:这里的加载器并不是通过继承组成的关系,而是组合。 ②③④都继承自java.lang.ClassLoader
question1:为什么类加载机制是双亲委派模型(即优先加载父类加载器的类)
ans:双亲委派模型:当一个Class loader收到了加载类的请求,它不会立刻加载,而是先交给父类加载器加载,但父类加载器找不到那个类,才传递回给子类加载器。即优先在Bootstarp中加载,然后是Extension,然后就才是Application。
这样做的好处:
①只有1个classloader加载了类,防止内存中会出现同样的字节码
②增强了安全性,避免用户自己定义了java预定义的类并成功覆盖。比如用户自定义了一个java.lang.Object的类,这时候它会优先从Bootstrap中加载,而Bootstrap中加载的是java预定义的Object类,而不是用户自定义的Object类,这样就避免了用户的Object类覆盖。如果是先从Application Class loader中加载,那么就是先加载用户自定义的Object类,而Java预定义的Object类被覆盖掉。
二. JVM的内存结构:
JDK1.6: Heap + PermGen(Method Area)永久区,方法区 + Stack + Program Counter Register
同时:①Stack = JVM Stack + Native Method Stack
Stack用于描述Java方法执行的内存模型,每个方法执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表,操作栈,动态链接,方法出口,基本类型和对象的引用等等。每一个方法从被调用直至执行完成的过程,就对应一个栈帧在JVM Stack中从入栈到出栈的过程。
(Native同理,只是作用与本地方法)
PS:如果线程请求的栈深度大于JVM所允许的深度,抛出StackOverflowError。
如果JVM Stack可以动态拓展,拓展时无法申请到足够的内存,抛出OutOfMemoryError
②Heap = Young Generation年轻代 + Old Generation 老年代
Young = Eden + From Survivor + To Survivor
通过各种参数可以控制各区域的内存大小,进而达到JVM调优效果
-Xms, -Xmx:设置Heap的最小,最大空间的大小
-XX:NewSize, -XX:MaxNewSize: 设置年轻代最小,最大空间的大小
-XX:PermSize, -XX:MaxPermSize: 设置永久区最小, 最大空间的大小
-Xss:设置每个JVM Stack的大小
(PS:Perm是永久区,要设置老年代只能通过 Heap - Young来间接控制)
PS:在Heap区中,有可能抛出OutOfMemoryError
③Program Counter Register
较小的内存空间,是当前字节码的行号指示器(用于分支,跳转等待)
如果执行的是Java方法,记录正在执行的JVM字节码的指令地址。
如果是Native方法,那么为空(Undefined)
(此区域是JVM唯一无OutOfMemoryError的区域,因为一般不会在这里抛异常)
④PermGen(Method Area)方法区,永久区
用于存储已经被JVM加载的类信息,常量,静态变量,JNI编译后的代码·
(会抛出OutOfMemoryError)
PS:Heap 和Method Area在线程间是会共享资源的,而Stack和Counter是线程私有的。
JDK各版本的区别: (1.6 ~ 1.8)
JDK1.6中,各种常量池是放在方法区中的
JDK1.7中,常量池放在Heap中,此时:Heap = Young + Old + 各种常量池
(PS:关于常量池,这里还有一个TODO,即intern那中问题,看《JVM入门到放弃》)
JDK1.8:常量池依然在Heap中,但永久区/方法区被移除,改为使用metaSpace(元空间)
question2:移除永久区PermGen的原因
①永久区一直只存在于HotSpot JVM,而JRockit VM没有永久区,为了融合两个JVM而做出的调整,无须继续配置永久区
②永久区内存经常不够用,或者发生内存泄漏,抛出OutOfMemoryError:PermGen
③永久区会为GC带来不必要的复杂度,并且回收效率偏低
question3:元空间metaSpace和永久区的区别?
元空间并不在VM中,而是使用本地内存,默认情况下,仅受本地内存的限制。(可以通过配置参数来控制meta Space,默认情况下,最大空间没有限制,即内存的上限)。配置参数:
-XX:MetaspaceSize, -XX:MaxMetaspaceSize 初始空间大小,最大空间大小
-XX:MinMetaspaceFreeRadio, -XX:MaxMetaspaceFreeRadio:
在GC之后,最小/最大的Metaspace剩余空间容量的百分比
question4:常量池为什么要移动到Heap
因为方法区的回收比较困难,会导致过多严重的bug,所以从1.7开始就把常量池移动到Heap,为后续移除PermGen做出准备(在JDK1.8已把PermGen移除)
三. GC算法:
①标记-清除 Mark-Sweep
缺点:效率不高,且会产生大量的内存碎片
②复制算法 Copying
缺点:会使内存缩小一半,不能直接用在Old,因为持续复制长生存期的对象会导致效率降低
③标记-压缩 Mark-Compact (在①的基础上,清除完之后还对碎片进行压缩)
④分代收集Generational Collection 即年轻代用 复制算法,老年代用标记清除/压缩
垃圾回收器:
①Serial,串行收集器。最古老,最稳定,效率高。但会产生较长的停顿
②ParNew,即Serial的多线程版本
③Parallel,于ParNew类似,但更关注系统的吞吐量
④CMS(Concurrent Mark Sweep)
重视服务的响应速度,希望系统停顿时间最短,但会产生大量的空间碎片,降低吞吐量。
⑤G1(Garbage-First)
满足了GC停顿时间要求,同时又具备高吞吐量的特征
(收集器之间可以组合使用,如Young使用A,Old使用B)
MinorGC:对年轻代进行GC。特点:频繁,回收速度快
MajorGC:对老年代进行GC。
FullGC:全堆范围的GC
question5:什么情况下会出现内存溢出/泄漏?
虽说Java有GC,无须我们手动释放资源,但在运行时还是可能出现对象可达,但不会被使用的情况,这时候就会导致内存泄漏。例子:
内存溢出的可能原因:
①内存泄漏导致Stack内存不断增大,引发OutOfMemory(Stack处)
②大量jar,class文件加载,装载类的空间不足,溢出 (Class Loader处)
③操作大量对象,导致Heap空间不足,溢出(Heap处)
④nio直接操作内存,内存过大导致溢出(内存处)
(同理应该MetaSpace溢出也是一样的)
解决方法:查看是否有内存泄漏,设置参数增大空间,代码是否存在死循环生成过多对象
question6:JVM从Young到Old的晋升条件是什么
①对象在Eden出生,经过一个MinorGC还存活,就被Survivor容纳,在From和to中复制交换。如果经过[MaxTenuringThreshold](默认是15)次交换还存活,进入Old
②如果对象大小等于Eden的二分之一,直接分到Old。如果Old也分配不下,做一次MajorGC
(如果小于Eden的二分之一,但没有足够的空间,进行MinorGC)
③MajorGC之后,如果Survivor仍然放不下,则放到Old
④动态年龄判断。如果大于等于某年龄的对象超过survivor空间的一半,则这些对象全部都直接进入Old(无须达到MaxTenuringThreshold)