运行时常量池
- 每一个运行时常量池都在java虚拟机的方法区中分配。
例如在Java中字符串的创建会在常量池(方法区中StringTable:HashSet)中进行:
public class Changliang {
public static void main(String[] args) {
// s1与s2是相等的,为字节码常亮
String s1 = "abc";
String s2 = "abc";
// s3创建在堆内存中
String s3 = new String("abc");
// intern方法可以将对象变为运行时常量
// intern是一个native方法
System.out.println(s1 == s3.intern()); // true
}
}
- 直接内存**:jdk1.4中增加了NIO,可以分配堆外内存(系统内存替代用户内存),提高了性能。
对象的创建
一个对象创建的过程为:
如何在堆中给对象分配内存
- 两种方式:指针碰撞和空闲列表。我们具体使用的哪一种,就要看我们虚拟机中使用的是什么垃圾回收机制了,如果有压缩整理,可以使用指针碰撞的分配方式。
- 指针碰撞:假设Java堆中内存是绝对规整的,所有用过的内存度放一边,空闲的内存放另一边,中间放着一个指针作为分界点的指示器,所分配内存就仅仅是把哪个指针向空闲空间那边挪动一段与对象大小相等的举例,这种分配方案就叫指针碰撞
- 空闲列表:有一个列表,其中记录中哪些内存块有用,在分配的时候从列表中找到一块足够大的空间划分给对象实例,然后更新列表中的记录,这就叫做空闲列表。
线程安全性问题 - 在两个线程同时创建对象时,可能会造成空间分配的冲突,解决方案有:线程同步(但执行效率过低)或给每一个线程单独分配一个堆区域TLAB
Thread Local Allocation Buffer(本地线程分配缓冲)。
对象的结构
Header(对象头)
自身运行时数据(32位~64位 MarkWord):哈希值、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳
类型指针(什么类的实例) - InstanceData**:数据实例,即对象的有效信息,相同宽度(如long和double)的字段被分配在一起,父类属性在子类属性之前。
- Padding:占位符填充内存
对象的访问定位 - 对象的访问定位有两种方式:句柄访问和直接指针访问
- 句柄访问:Java堆中会划分出一块内存来作为句柄池,引用变量中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。一个句柄又包含了两个地址,一个对象实例数据,一个是对象类型数据(这个在方法区中,因为类字节码文件就放在方法区中)。
- 直接指针访问:引用变量中存储的就直接是对象地址了,在堆中不会分句柄池,直接指向了对象的地址,对象中包含了对象类型数据的地址。HotSpot采用直接定位
垃圾回收
-
对于一般Java程序员开发的过程中,不需要考虑垃圾回收。
-
如何判定对象为垃圾对象;
-
1.引用计数法
-
2.可达性分析法
-
如何回收垃圾对象;
-
1.回收策略(标记清除、复制、标记整理、分带收集算法)
-
2.常见的垃圾回收器(Serial、Parnew、Cms、G1)
-
何时回收垃圾对象。
判定垃圾对象
引用计数算法
- 在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就加1,当引用失效的时候(变量记为null),计数器的值就减1。但Java虚拟机中没有使用这种算法,这是由于如果堆内的对象之间相互引用,就始终不会发生计数器-1,那么就不会回收。
测试:两个对象相互引用
public class Count {
private Object instance;
/*给大家推荐一个免费的学习交流君样:826021115 */
public Count() {
// 占据20M内存
byte[] m = new byte[20 * 1024 *1024];
}
public static void main(String[] args) {
Count c1 = new Count();
Count c2 = new Count();
c1.instance = c2;
c2.instance = c1;
// 断掉引用
c1 = null;
c2 = null;
/*给大家推荐一个免费的学习交流君样:826021115 */
//垃圾回收
System.gc();
}
}
- 打印垃圾回收简易信息的参数:-verbose:gc 打印详细:-verbose:gc -XX:+PrintGCDetails 输出:[GC
(System.gc()) [PSYoungGen: 22476K->680K(38400K)]
42956K->21168K(125952K), 0.0008355 secs]可以看出对象被回收,因此Java不使用引用计数算法。
可达性分析法
-
此算法的核心思想:通过一系列称为“GC
Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC
Roots没有任何的引用链相连时(从GC Roots)到这个对象不可达)时,证明此对象不可用。
可作为GC Roots的对象: -
虚拟机栈
-
方法区的类属性所引用的对象
-
方法区中常量所引用的对象
-
本地方法栈中引用的对象
垃圾回收算法
标记清除算法 -
先标记出要回收的对象(一般使用可达性分析算法),再去清除,但会有效率问题和空间问题:标记的空间被清除后,会造成我的内存中出现越来越多的不连续空间,当要分配一个大对象的时候,在进行寻址的要花费很多时间,可能会再一次触发垃圾回收。
复制算法
堆:
-
新生代
Eden 伊甸园
Survivor 存活期
Tenured Gen 老年区 -
老年代
-
复制算法**是将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,浪费较大。复制算法的执行过程如下图所示:
-
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。