本人是某双非一本大学的大四学生,目前在复习准备秋招,希望这些自己整理的内容对大家有用。这篇文章内容关于JVM,但GC的内容还没整理完,所以这里先不贴出GC相关,之后再专门发一篇文章
JVM
1.Java内存区域
1.1.说一下Java的主要组成部分及其作用
-
JVM包含两个子系统和两个组件:两个子系统分别是类加载子系统和执行引擎,两个组件时运行时数据区和本地接口。
- 类加载子系统:根据给定的全限定类名,来装载class文件到方法区
- 执行引擎:包括即时编译器和解释器
- 本地接口:与一些其他语言交互的接口
- 运行时数据区:也是我们常说的JVM内存
-
作用:首先通过前端编译器把.java文件编译成.class的二进制字节码文件,再由类加载子系统把.class文件加载到JVM内存的方法区中,而字节码指令并不能直接交给底层操作系统去执行,因此需要执行引擎把字节码文件翻译成机器指令,再交给CPU去执行,而这个过程需要利用本地接口与其他语言交互来实现整个程序的功能。
1.2说一下运行时数据区
-
程序计数器:
- 作用:字节码指令通过改变程序计数器的值来依次读取字节码指令,从而实现流程的控制。多线程情况下,程序计数器用于记录当前线程执行的位置,当切换回来的时候能够知道该线程之前运行到哪了。
- 底层是通过寄存器实现,不存在内存溢出
-
虚拟机栈:
-
作用:线程私有,用于内含局部变量表,操作数栈,方法出口。
-
局部变量表:在编译时,大小就确定,局部变量表的容量以变量槽slot为基本单位,long和double占2个槽,以前一个作为索引,slot可重用
-
操作数栈:用于保存计算过程的中间结果【这里还有栈顶缓存技术,将栈顶元素缓存到cpu寄存器中减少与内存交互次数】
通过-Xss可以指定栈内存的大小。
越大越好吗?如果划太大,线程数就会减少,一般用默认即可。
栈中变量是否安全?看是否线程共享
虚拟机栈可能有两种错误:
- 虚拟机栈内存大小不允许动态扩展,StackOverFlow:
- 栈帧过多:一个方法不停地调用自己
- 栈帧过大:比如转Json字符串时,两个类循环引用,用transient解决
- 虚拟机栈的内存大小可动态扩展,若在扩展时无法申请到足够内存空间则OutOfMemory。
- 虚拟机栈内存大小不允许动态扩展,StackOverFlow:
-
-
本地方法栈:
-
作用:为本地方法的运行提供内存
常见的本地方法有:Object的clone() notify() wait() hashCode()
-
-
堆:
-
最大的区域,线程共享。几乎所有的对象实例都在这里分配内存,虚拟机启动时创建。【TLAB(ThreadLocalAllocBuffer):线程私有缓冲区,在Eden区,默认开启】
之所以说“几乎”,是因为有逃逸分析和标量替换,如果某些对象引用没有逃逸出方法,那么可能标量替换后站上分配。
常用的堆内存诊断工具:jps查看进程、jmap查看堆内存实时占用情况、jconsole连续监控
如果垃圾回收后,内存占用仍然很高?jvisualvm生成堆转储Dump文件,分析占用内存的大对象
-
-
方法区:
-
用于存储已被类加载器加载的类的元信息【变量、方法修饰符等等】。
-
元空间默认大小
《Java 虚拟机规范》规定了有方法区这个概念及其作用,永久代和元空间是对其具体的实现,在
JDK7的时候,将字符串常量池、静态变量等移出【静态变量移到了Class对象末】
JDK8的时候,废弃了永久代,在本地内存实现了元空间,并把原来永久代中留存的内容移到元空间
类被类加载子系统加载之后,它的常量池信息就被放入方法区中,称为运行时常量池,此时符号引用会变为真实引用。
-
为什么要把字符串常量池移到堆中?
首先字符串常量池是在JDK7被移到堆中的,永久代的GC效率很低,只有FullGC才会回收永久代,GC效率低,转到堆中,minorGC就会回收StringTable
-
为什么要把方法区移到直接内存,也可以说为什么要改成元空间?
永久代大小难以设置,设置小了,若加载类多了OOM,也可能导致频繁GC;设置大了又浪费。干脆直接移到直接内存。
-
-
1.3堆和虚拟机栈的区别
- 堆中,给对象分配物理地址不连续,因此性能也会慢一些【在GC时考虑到不连续分配,也就有了各种算法】
- 栈中,顾名思义使用的是数据结构中的栈,物理地址的分配是连续的,因此性能比堆快,同时遵循先进后出的原则。
- 因为堆中物理地址分配不连续,因此分配内存要在运行期确定。因为虚拟机栈中物理地址分配是连续的,因此在编译器就确定了。
2.对象相关内容
2.1.对象创建过程
2.1.1.类加载检查
- 检查要创建的类是否已经完成了类加载的过程,也就是加载链接初始化。如果没有的话就要先进行类加载
2.1.2.分配内存
-
类加载检查通过后,在堆中为对象分配内存,此处要明确创建对象所需要的空间在类加载完成之后已经可以确定,此时知识把一块固定大小的内存从堆内存中划分出来。而分配内存的方式又有“指针碰撞”和“维护空闲列表”两种方式,具体要看垃圾回收器的垃圾回收算法是否具有压缩整理的功能。
扩展:内存分配存在并发问题,开发过程中创建对象很频繁,必须保证线程安全。采取以下两种方式:
- TLAB(ThreadLocalAllocBuffer):为每个线程预先在Eden分配一块内存,分配对象时,首先在TLAB分配,当不足以分配时,再采用以下策略
- CAS+失败重试:虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性;
2.1.3.初始化零值
- 为对象的字段初始化零值,保证可以不赋初始值就直接使用。
2.1.4.设置对象头
2.1.5.执行<init>
方法
- 按照程序员的意愿对对象进行初始化,对象的创建才算完成。
2.2.对象内存布局和对象头详情
-
此处以64位JVM虚拟机为准。查看对象头的方法,通过jol-core依赖。
-
Java虚拟机中,对象在内存的布局分为三部分:对象头,对象实例数据,对齐填充
-
对象头:包括三部分MarkWord、KlassPoint、数组长度。
- MarkWord具体看JUC篇
- KlassPoint:具体看类加载过程
- 数组长度:只有对象是数组对象时才有,4字节。
-
实例数据
-
对齐填充:Java对象默认是按照8字节对齐,提高对象的访问效率,用空间换时间。
-
指针压缩:
-
出现的原因:采用8字节(64位)存储真实内存地址比之前4字节(32位)多了开销,堆空间能放的对象变少了,使得GC更频繁。同时也降低了CPU缓存的命中率。
-
原理:KlassPoint不存储真实的64位地址,而是存一个映射地址编号,4个字节可以表示2^32个地址,每个地址对应8bytes的内存块。这时用4个字节就可以表示32GB的内存地址。
此处也就说明了当JVM堆内存大于32GC时,开启指针压缩会失效,接着会使得GC频率变高,造成时间空间浪费。
-
-
2.3.对象的访问定位
- 建立对象就是为了使用对象,我们通过栈上的引用数据来访问、操作堆上的具体对象,而对象的访问方式有两种:
- 句柄:在Java堆内存中划分出一块内存区域作为句柄池,引用存储对象的句柄,句柄中包含对象的实例数据和类型数据各自的具体地址信息。
- 直接指针:引用直接存储对象的地址。
- 这两种方式各有好处,采用句柄访问的方式,当实例对象被移动时只会改变句柄中的对象指针,而引用本身不需要修改;使用直接指针访问的方式最大的好处就是速度快,节省了一次指针定位的开销。
3.类加载子系统
3.1.类加载过程
3.1.1.加载
-
把类的字节码加载进方法区,然后创建对应C++对象instanceKlass来描述Java的类。【此处具体根据我们要创建的对象的类型区分:1.如果是Reference类型,就用instanceRefKlass对象表示;2.如果不是Reference,如果是Class类就通过instanceMirrorKlass对象表示,ClassLoader类或者其子类就用instaneClassLoaderKlass对象表示,普通类就是用instanceKlass对象表示】。解析Class文件的相关信息大多都会通过InstanceKlass等对象的属性保存起来。在构造函数中还需会将除header外的字初始化为NULL_WORD,将此类代表的Java类所创建出来的Java对象的大小初始化为0,后续会在parseClassFile()方法中更新这个值。
-
由于任何一个Java类都有一个Class对象来表示,所以在创建了表示普通Java类的InstanceKlass对象后,还需要创建对应的InstanceOop对象(代表Class对象)。如果java.lang.Class类还没有被解析,则将相关信息暂时存储到数组中,后续在类解析后会做处理,处理逻辑和当前类处理java.lang.Class类被加载时的逻辑基本一致。
-
创建出表示了java.lang.Class对象的oop实例后,设置到InstanceKlass实例的
_java_mirror
属性中,同时也设置oop的_klass
属性。如果当前类表示数组,那么在java_lang_Class::create_mirror()方法中还会设置表示数组的ArrayKlass对象的_component_mirror
属性,同时也会设置表示当前数组的Class对象的_array_klass
属性。HSDB的使用: 1. java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB 2. jps-->attach to hotspot process 3. tools-->findObjectByQuery【select d from top.jiakaic.bytecode.Dog d】
如图是java对象的内容:
如图是instanceKlass的内容:
3.1.2.链接(linking)
-
验证(verifo):验证类的字节码文件是否符合JVM规范,同时还有安全性检验等…【此处不通过会有ClassFormatError】
-
准备(prepare):为static变量分配空间,设置默认值【在JDK7前存在instanceKlass末,JDK7开始存在mirror末】
此处要注意,如果是static final修饰的基本类型数据或者String类型,会在此处直接赋值
-
解析():将常量池中的符号引用转化为直接引用【在方法区中建立虚方法表】
3.1.3.初始化(initialization)
-
初始化即调用()V,是线程安全且懒惰的。【虚拟机会保证该方法在多线程下被同步加锁】
()V称为类构造器方法,是javac编译时收集类中所有静态变量的赋值动作和静态代码块中的语句得来的,具体顺序按代码中的出现顺序。
-
初始化发生的时机:
- 首先明确main方法所在的类,总会首先初始化
- 首次访问类的静态方法或静态变量时
- 子类初始化时,如果父类还没初始化,会触发父类初始化
- 子类访问父类的静态变量时,会触发父类初始化
- Class.forName()
- new会导致初始化
-
不会导致初始化的情况:
- 访问类的静态常量:也就是static final修饰的
- 类对象.class不会触发初始化
- 创建该类的数据不会导致这个类初始化
- 类加载器的loadClass也不会导致初始化
- Class.forName(Class clazz,boolean initialize)的第二个参数为false时
3.2.类加载器
- BootstrapClassLoader:负责加载(JAVA_HOME/jre/lib/rt,jar、resources.jar或sun.boot.class.path下),通过C/C++实现,ExtClassLoader和AppClassLoader也是通过它加载的。
- ExtensionClassLoader:加载jre/lib/ext下【打包jar包放进ext目录下也会被其加载】
- ApplicationClassLoader:加载环境变量classpath或java.class.path指定路径下
3.3.双亲委派机制
3.3.1. 双亲委派流程
-
Java虚拟机对class文件采用按需加载,双亲委派的模式加载。
-
具体细节:ClassLoader类中的loadClass()
- 首先会调用findLoadedClass(name),检查本类加载器缓存中是否加载过
- 如果没有加载过,就找父类
- 父类不为空,让父类调loadClass(name, false)
- 父类为空,调findBootstrapClassOrNull(name);委派BootstrapClassLoader
- 如果继续往下执行,仍没有加载,调用findClass(name);每个类加载有各自的加载路径,去找
//此处是ClassLoader类中 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
3.3.2.双亲委派模型的好处
- 避免类重复加载,保护核心API不被篡改。
3.3.3.自定义类加载器
- 自定义类加载器需要创建一个类继承于ClassLoader,如果我们不想打破双亲委派模型,可以重写findClass(),比较简单;如果我们要打破双亲委派模型,可以重写loadClass()。
3.3.4.JDBC破坏双亲委派的例子
- DriverManager的类加载器是BootstrapClassLoader