java代码是如何运行起来的
-
编译成jar或者war包
-
启动一个jvm进程运行程序
-
jvm使用类加载器去加载使用到的类,一般来说main方法是入口
-
字节码执行器执行加载到内存中的类
运行时的内存区域
-
程序计数器,较小的内存空间,当前线程执行的字节码的行号指示器,线程私有
-
虚拟机栈,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,一个栈帧包含局部变量表、操作数栈、动态链接、方法出口等信息。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程,栈里面存放着各种基本数据类型和对象的引用,线程私有
- 栈桢大小缺省为1M
- –Xss调整大小,例如-Xss256k
- 栈帧中的变量针对的是方法级别的
-
本地方法栈,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不再为其在虚拟机栈中创建栈帧,jvm只是简单地动态链接并直接调用native方法、线程私有
-
堆,是需要重点关注的区域,因为涉及到内存的分配(new、反射等)与回收(回收算法、收集器等),字符串常量池存放于堆中,线程共享
- -Xms,堆的最小值
- -Xmx,堆的最大值,-Xmx256m
- -Xmn,新生代的大小
- -XX:NewSize,新生代最小值
- -XX:MaxNewSize,新生代最大值
-
方法区、用于存储已经被虚拟机加载的类信息、静态变量(static变量)等数据,jdk1.8之后移动到元空间,不再受虚拟机内存影响,只接本机总内存限制,线程共享
- -XX:MaxMetaspaceSize=3M
线程私有的部分,在编译期间就已经确定所需内存大小,例如虚拟机栈默认1M,直接分配1M空间,运行时可能会抛栈溢出异常
一段程序是如何在jvm中存储的
如下代码再jvm中的执行时的一个存储形态
```
public class Kafka {
public static void main(String[] args) {
ReplicaManager replicaManager = new ReplicaManager();
replicaManager.loadReplicaFromDisk();
}
}
class ReplicaManager{
public void loadReplicaFromDisk(){
boolean hasFinishedLoad = false;
if(isLocalDataCorrupt()){
}
}
public boolean isLocalDataCorrupt(){
boolean isCorrupt = false;
Object obj = new Object();
//代码执行到这里被暂停
obj.hashCode();
return isCorrupt;
}
}
```
一个方法执行完毕之后会怎样
- 一旦某个方法执行完毕会将对应的方法栈帧弹出当前线程的虚拟机栈
- 栈内存会立即释放
- 基本数据类型的局部变量占用的内存空间会立即释放
- 引用类型的变量引用会立即释放,堆中的对应对象就没有了相关的引用
直接内存
- 不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域
- NIO这块区域会被频繁使用,在java中可以用DirectByteBuffer对象直接引用并操作
- 这块内存区域不受虚拟机堆大小限制,但受本机总内存限制,可以通过-XX:MaxDirectMemorySize来设置(默认和堆内存最大值一致),所以也会出现OOM
操作系统在I/O过程中,需要把数据从用户态拷贝到内核态,然后再输出到I/O设备,所以从java堆内存输出到I/O设备需要经历两次拷贝,而直接内存在native堆上,所以只需要一次拷贝,java的堆属于用户态,直接内存属于内核态
栈上分配
- 对于线程私有的对象,将它打散分配在栈上,而不分配在堆上,好处是对象跟着方法调用自行销毁,不需要进行垃圾回收,可以提高性能
- 栈上分配需求条件
- 逃逸分析,判断对象的作用域是否逃逸出方法体,一般是通过返回值逃逸
- jvm以server(linux服务器模式)的模式运行,server模式才能进行逃逸分析,jvm的运行模式还有mix(自适应匹配)/client(win桌面程序)
- 启动栈上分配
- -Xmx10m和-Xms10m,堆的大小
- -XX:+DoEscapeAnalysis,启用逃逸分析(默认打开)
- -XX:+PrintGC,打印GC日志
- -XX:+EliminateAllocations,标量替换(默认打开),如果一个对象中包含几个属性,那么就直接将这几个属性打散,分配在栈上面
- -XX:-UseTLAB,关闭本地线程分配缓冲,为避免每个线程在申请分配堆中资源冲突,事先在堆里面为每一个线程分配一块私有内存,用完了之后,又重新分配,申请的时候是线程私有的,但是申请的对象是线程共享的
对象的分配
- 执行类加载
- 虚拟机为新生对象分配内存
- 如果堆内存是整理过的,空闲的内存和用过的内存中间有一个指针,作为分界点的指示器,分配只需要将指针往空闲空间移动对象大小相等的距离,这叫做指针碰撞
- 如果堆内存不是连续的,虚拟机会有一个表记录哪些内存块是可用的,在分配的时候从从列表中找出一块足够大的空间划分给对象实例,并更新列表,称为空闲列表分配
- 多个线程一起分配的时候,处理线程安全一般有三种方式
- 加锁,做同步处理
- 采用CAS配上失败重试机制
- Thread Local Allocation Buffer,TLAB
- 分配完成后,虚拟机需要设置变量的默认值(int=0,boolean=false等)
- 虚拟机需要对对象进行必要的设置,对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中
- 上面就相当于执行完了虚拟机的操作,接下来就是程序员对对象的一些初始化,赋值等操作
对象的内存布局
- 对象头,包括两部分信息
- 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
- 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- 实例数据,对象的实例数据
- 对齐填充,并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用,因为HotSpot VM要求对象的大小必须是8字节的整数倍
对象的访问定位
- 句柄访问对象
- 定义,java堆划出一块内存作为句柄池,引用中存储对象的句柄地址,句柄中包含对象实例数据、类型数据的地址信息
- 优点,引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不需要改动引用ref本身
- 直接指针访问对象(sun、hotspot)
- 定义,ref中直接存储的就是对象的实例数据,但是类型数据跟句柄访问方式一样
- 优点,速度快,相比于句柄访问少了一次指针定位的开销时间
内存溢出
- 定义,内存溢出时内存空间不足导致的
- 栈溢出,一般很难出现
- 抛出java.lang.StackOverflowError异常,考虑出现了无限递归,栈也有最大深度的
- 方法的执行要打包成栈帧,所以递归天生比拥有同样功能的循环慢,也就是说非递归的算法更好
- 堆溢出
- 参数-Xms5m -Xmx5m -XX:+PrintGC
- 抛出java.lang.OutOfMemoryError: GC overhead limit exceeded,是不停的在分配对象
- 抛出java.lang.OutOfMemoryError: Java heap space,是分配了巨型对象
内存泄漏
- 定义,应该释放的对象没有释放,多见于自己使用容器保存元素的情况下
- 示例ArrayList--->remove
public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, umMoved); // clear to let GC do its work //前面将元素移动之后,需要将最后一个无用的数组值设置为null //如果不清除掉,这个数据就持有这个对象,gc就不会回收 elementData[--size] = null; return oldValue; }自己写的数组容器类似的,一定要注意这种问题