JAVA虚拟机
java堆
被所有线程共享的内存区域,主要用于存放对象实例,为对象分配内存就是把一块确定大小的内存从堆内存中划分出来。
1、指针碰撞法: 已分配的内存和空闲内存在一个指针的两侧,需要分配内存时只需要指针往空闲的内存移动与对象大小相等的区域,
2、空闲列表法: 因为大多数情况内存很混乱,JVM一般都是维护一个列表,上面记录了空闲内存的信息,分配操作完成后分配对象实例,并更新记录。
创建对象时要考虑多线程的问题
1、采用CAS操作保证操作的原子性。
2、把内存分配的操作按照线程划分。
java栈
栈是线程私有的,每个线程对应一个Java栈,每个线程在创建时会创建一个对应的栈帧。
栈帧负责存储局部变量表(存放方法参数和局部变量)、操作数栈、动态链接和方法返回地址等信息。
每个方法的调用过程,相当于栈帧的入栈和出栈过程。
方法区
方法区也是线程共享的,用于存放已被虚拟机加载的类信息、常量、静态变量和即时编辑器编译后的代码数据。
运行时常量池也是方法区的一部分,用于存放编译期间生成的各种符号引用
程序计数器
指令计数器是线程私有的,每个线程都有独立的指令计数器,记录了虚拟机正在进行的字节码指令的地址,分支、循环、跳转、异常
等操作都依赖于程序计数器
对象的组成
对象头、实例数据、对齐填充
对象头:存储了hashcode、GC年龄、锁状态标志、类元数据(可知道是哪个类的实例)等信息
实例数据:代码定义的各种字段,包括从父类中继承的
对齐填充:当对象大小不满足8的倍数时进行空位填充
Java GC
为了高效的垃圾回收,Java虚拟机把堆内存分为新生代(Young),老年代(Old),和永久代(Permenent)。
新生代
新生代由Eden和Survivor(S0,S1)构成,大小通过-Xmn参数指定,Eden和Survivor内存大小为8:1
Eden: 大多数情况下,对象在Eden中分配,当Eden没有足够空间时,会触发一次Minor GC。
Survivor: 新生代和老年代的缓冲区域。
什么时候发生GC
当新生代发生Minor GC时,会将存活的对象移动到S0内存区域,并清空Eden区域。
再次发生Minor GC时,会将Eden和S0存活的对象移动到S1区域。
存活对象会反复在S0和S1之间移动,对象每发生一次移动,对象的GC年龄就会增加。
当GC年龄超过默认阈值15时,会将对象移动到老年代。
老年代
老年代的空间大小是-Xmx和-Xmn(新生代大小)两个参数之差,用于存放几次Minor GC之后依旧存活的对象。
当老年代的空间不足时,会触发Major GC/Full GC,速度比一般的Minor GC慢十倍以上。
永久代
在JDK8之前,永久代中保存类的元数据信息、运行时常量池、已确定的符号引用和虚方法表等被保存在永久代中,一旦类的元数据超过了永久代大小,就会抛出OOM异常。
什么时候触发Young GC和Full GC?
Young GC
新生代Eden的内存不够时,就会触发Young GC。
Full GC
1、老年代空间不足
创建一个大对象、大数组时,Eden区不足以分配这么大的空间时,会尝试在老年区分配,如果老年区也不够就会触发Full GC。
2、Young GC晋升到老年代的对象平均总大小大于老年代的剩余空间
晋升时会判断是否大于,大于的话就不会执行Young GC,转而执行一次Full GC。
3、永久代空间不足
加载的类、调用的方法较多时直到空余空间不多时会触发Full GC。
4、调用System.gc()
移除永久代原因?
调优困难,大小很难确定,数据可能随着Full GC发生移动。
在移除后数据存储在元数据空间中,可以避免永久代的内存溢出问题,不过监控会耗费内存。
PS:JDK8之后,字符串常量池只保存字符串的引用
如何判断对象是否存活?
GC发生之前,需要确定堆内存那些对象是存活的,一般有两种:引用计数法,可达性分析法
引用计数法:
在对象上添加一个引用计数器,每当有一个对象引用他时计数器+1,当使用完对象时,计数器-1,计数器值为0的对象表示不可能再被使用。但是不能解决对象之间互相引用的问题。
可达性分析法:
通过称为GC roots的对象作为起点,从这些节点向下搜索,搜索路径称为引用链,当一个对象没有任何引用链时,意味着可以被回收。
如何判定一个对象是否可回收
至少经过两次标记过程:
1、如果对象到GC Roots没有引用链,则进行第一次标记。
2、如果对象重写了finalize方法,且还未执行过,那么此对象会被插入到F-Queue队列中,由一个Finalizer线程去触发其finalize方法
如果finalize方法中使得对象与引用链上任何一个对象建立起联系,那么就不会回收并移出队列。