当 JVM
收到一个 new
指令时,会检查指令中的参数在常量池是否有这个符号的引用,还会检查该类是否已经被加载过了,若是没有的话则要进行一次类加载。git
接着就是分配内存了,一般有两种方式:github
使用指针碰撞的前提是堆内存是彻底工整的,用过的内存和没用的内存各在一边每次分配的时候只须要将指针向空闲内存一方移动一段和内存大小相等区域便可。算法
当堆中已经使用的内存和未使用的内存互相交错时,指针碰撞的方式就行不通了,这时就须要采用空闲列表的方式。虚拟机会维护一个空闲的列表,用于记录哪些内存是能够进行分配的,分配时直接从可用内存中直接分配便可。数组
堆中的内存是否工整是有垃圾收集器来决定的,若是带有压缩功能的垃圾收集器就是采用指针碰撞的方式来进行内存分配的。缓存
分配内存时也会出现并发问题:并发
这样能够在建立对象的时候使用 CAS
这样的乐观锁来保证。线程
也能够将内存分配安排在每一个线程独有的空间进行,每一个线程首先在堆内存中分配一小块内存,称为本地分配缓存(TLAB : Thread Local Allocation Buffer
)。指针
分配内存时,只须要在本身的分配缓存中分配便可,因为这个内存区域是线程私有的,因此不会出现并发问题。code
可使用 -XX:+/-UseTLAB
参数来设定 JVM 是否开启 TLAB
。对象
内存分配以后须要对该对象进行设置,如对象头。对象头的一些应用能够查看 Synchronize 关键字原理。
一个对象被建立以后天然是为了使用,在 Java 中是经过栈来引用堆内存中的对象来进行操做的。
对于咱们经常使用的 HotSpot
虚拟机来讲,这样引用关系是经过直接指针来关联的。
如图:
这样的好处就是:在 Java 里进行频繁的对象访问能够提高访问速度(相对于使用句柄池来讲)。
简单的来讲对象都是在堆内存中分配的,往细一点看则是优先在 Eden
区分配。
这里就涉及到堆内存的划分了,为了方便垃圾回收,JVM 将对内存分为新生代和老年代。
而新生代中又会划分为 Eden
区,from Survivor、to Survivor
区。
其中 Eden
和 Survivor
区的比例默认是 8:1:1
,固然也支持参数调整 -XX:SurvivorRatio=8
。
当在 Eden
区分配内存不足时,则会发生 minorGC
,因为 Java
对象多数是朝生夕灭的特性,因此 minorGC
一般会比较频繁,效率也比较高。
当发生 minorGC
时,JVM 会根据复制算法将存活的对象拷贝到另外一个未使用的 Survivor
区,若是 Survivor
区内存不足时,则会使用分配担保策略将对象移动到老年代中。
谈到 minorGC
时,就不得不提到 fullGC(majorGC)
,这是指发生在老年代的 GC
,不管是效率仍是速度都比 minorGC
慢的多,回收时还会发生 stop the world
使程序发生停顿,因此应当尽可能避免发生 fullGC
。
也有一些状况会致使对象直接在老年代分配,好比当分配一个大对象时(大的数组,很长的字符串),因为 Eden
区没有足够大的连续空间来分配时,会致使提早触发一次 GC
,因此尽可能别频繁的建立大对象。
所以 JVM
会根据一个阈值来判断大于该阈值对象直接分配到老年代,这样能够避免在新生代频繁的发生 GC
。
对于一些在新生代的老对象 JVM
也会根据某种机制移动到老年代中。
JVM 是根据记录对象年龄的方式来判断该对象是否应该移动到老年代,根据新生代的复制算法,当一个对象被移动到 Survivor
区以后 JVM 就给该对象的年龄记为1,每当熬过一次 minorGC
后对象的年龄就 +1 ,直到达到阈值(默认为15)就移动到老年代中。
可使用
-XX:MaxTenuringThreshold=15
来配置这个阈值。
虽然说这些内容略显枯燥,但当应用发生不正常的 GC
时,能够方便更快的定位问题。
最近在总结一些 Java 相关的知识点,感兴趣的朋友能够一块儿维护。