jvm学习
类加载子系统
类加载的过程
- 加载(Loading)
- 链接(linking)
- 验证(verify)
- 准备(prepare)
- 解析(resolve)
- 初始化(initialization)
类加载器的种类
-
启动类类加载器(引导类类加载器,bootstrap classloader)
- 使用c/c++实现。嵌套在jvm内部
- 用来加载java的核心库(JAVA_HOME/jre/lib/rt.jar)
- 并不继承自java.lang.classload,没有附加载器
- 处于安全考虑,bootstrap加载器致家长包名为java/javax/sun等开头的类
-
扩展类加载器(extension classloader)
- java语言编写
- 派生于classloader类
- 父加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从jdk的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在该目录下,也会有扩展类加载器加载。
-
应用程序类加载器(系统类加载器 appclassloader)
- java语言编写
- 父类为扩展类加载器
-
用户自定义类加载器
- 隔离加载类
- 修改类加载的方法
- 扩展加载源
- 防止源码泄露
-
自定义类加载器步骤
- 继承抽象类java.lang.classloader
- 1.2以前重写loadclass方法 1.2以后自定义类加载器的逻辑编写在findclass方法中
- 在编写自定义类加载器的时候没有复杂的需求可以直接继承urlclassloader
双亲委派机制
java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将class文件加载到内存生成class对象,而且加载某个类的class文件时,java虚拟机采用的是双亲委派机制,即把请求交由父类处理,他是一种任务委派模式。
-
原理
- 如果一个类加载器收到类要加载的请求,他并不会自己先去加载而是把这个请求委托给父类的加载器去执行。
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终会到达顶层的启动类加载器。
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试去加载,这就是双亲委派模式。
-
优势
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
- 自定义类:java.lang.string
- 自定义类:java.lang.shkStart
-
沙箱安全机制
自定义string类,但是在加载自定义string类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会优先加载jdk自带的文件(rt.jar包中的java\lang\string),报错信息中说没有main方法。就是因为加载的是rt.jar中的string类。这样就可以保证对java核心源代码的保护,这就是沙箱安全机制。
运行时数据区
程序寄存器
用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
-
使用pc寄存器存储字节码指令有什么用呢?为什么使用pc寄存器记录当前线程的执行地址呢?
一个cpu解决多个线程。cpu需要不停的切换各个线程,这个时候切换回来以后,就得知道从哪开始继续执行。
jvm的字节码解释器就需要通过改变pc寄存器的值来明确下一条应该执行什么样的字节码指令。
-
pc寄存器为什么会被设定为线程私有?
我们都只到所谓的多线程在一个特定的时间内只会执行其中某一个线程的方法,cpu会不停的切换任务,这样必然导致经常中断或恢复,如何保证分毫无差?为了能够记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每个线程分配一个pc寄存器。这样一来各个线程之间就可以独立计算而不会出险相互干扰的情况。
虚拟机栈
虚拟机不光是堆和栈。==栈是运行时单位,而堆是存储档位。==即:栈解决的是程序运行问题,即程序如何执行。堆解决的是数据的存储问题,即数据怎么放,放在哪。
- java虚拟机栈是什么?
早期也叫java栈。每个线程在创建是都会创建一个虚拟机栈,其内部保存一个个栈真,对应着一次次的java方法调用。
- 生命周期?
生命周期和线程一样。
- 作用
主管java程序的运行,其保存方法的局部变量(基本数据类型(八种)、引用数据类型(对象)),部分结果,饼参与方法的调用和返回。局部变量vs成员变量
-
栈的特点(优点)
- 栈是快速有效的分配方式,访问速度仅次于程序计数器。
- jvm直接对java栈的操作只有两个
- 每个方法执行,伴随这进栈(入栈、压栈)
- 执行结束后的出栈工作
-
对于栈来说不存在垃圾回收问题
-
如何设置栈的大小?
-Xss1024 单位:k m;
-
栈中存储什么?
- 每个线程都有自己的栈,栈中数据都是以栈真的格式存在的
- 在这个线程上正在执行的每个方法都对应一个栈真
- 栈真是一个内存分区,是一个数据集。维系着方法执行过程中的各种数据信息。
-
每个栈帧中存储的东西?
- 局部变量表
- 操作数栈
- 动态链表
- 方法放回地址
- 一些附加信息
-
局部变量表
- 局部变量表也被成为局部变量数组或本地变量表
- 定义为一个数字数组,主要存储方法参数和定义在方法体内的局部变量,这些数据包括各类接班数据类型、对象引用、以及returnaddress类型。
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
- 局部变量表所需要的容量大小是在编译期确定下来的,并保存在方法的的code属性的maximun local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
-
操作数栈
栈可以使用数组或者列表实现。先进后出。
- 没一个独立的栈帧中除了包含一个局部变量表之外还包含一个后进先出的操作数栈。也可以称之为表达式栈。
- 操作数占,在方法执行过程中根据字节码指令往栈中写入数据或提取数据,即入栈/出栈。
-
栈顶缓存技术
将栈顶元素全部缓存在物理cpu的寄存器中,以此来降低对聂村的读写次数。提升执行引擎的执行效率。
-
动态链接(和方法返回地址、一些附加信息统称帧数据区)
- 每一个栈帧内部都包含一个指向运行时常量池中改栈帧所属方法的引用。包含这个应用的目的是为了支持当前方法的代码能够实现动态链接。
- 在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如描述一个方法调用另外的其他方法时。就是通过常量池中指向方法的符号引用来标识的,那么动态链接的作用就是为了将这些符号引用转化为调用方法的直接引用。
-
为什么需要常量池?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。
-
方法调用
- 非虚方法:如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法是非虚方法
- 静态方法、死有余方法、final方法、实例构造器、父类方法都是非虚方法
- 其他方法都是虚方法
-
虚拟机中提供方法调用的指令
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用方法、私有及父类方法,揭西县阶段确定唯一方法版本
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
- invokedynamic:动态解析出需要调用的方法,然后执行
-
方法返回地址
-
存放调用该方法的pc寄存器地址。
-
-个方法结束由两种方式
- 正常执行完成
- 出现未处理的异常,非正常退出
-
无论那种方式退出,在方法退出后都返回到该方法调用的位置。方法正常退出时,调用者的pc寄存器的指作为返回地址。即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
-
-
一些附加信息
栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
-
栈相关面试题
-
举例栈溢出的情况?(StackOverFlowError)
- 通过-Xss设置栈的大小
-
调整栈的大小能否保证不出现溢出?
有可能。理论上来说只能让溢出出现的时间更晚一些。
-
分配的栈内存越多越好?
不是 栈内存空间过大 会挤占其他区域的空间
-
垃圾回收会涉及到栈空间吗?
不会,垃圾回收只会涉及到方法区和堆。
-
方法中定义的局部变量是否线程安全?
根据情况来说 如果该变量在方法内部消亡了则是线程安全的。不是内部产生的或者内部产生返回到外部的则是线程不安全的。
-
本地方法接口
-
什么事本地方法?
简单来说,一个native方法就是一个java调用非java代码的接口。一个native方法是这样一个java方法:该方法的实现有非java语言实现,比如c。这个特征并非java所特有,很多其他的编程语言都有这一机制,比如在c++中,你可以用extern"c"告知c++编译器去调用一个c的函数。
在定义一个native method时,并不提供实现提类似于一个java的接口,因为他的实现体是有非java语言在外面实现的。
本地接口的作用是融合不同的编程语言为java所用,他的初衷是融合c/c++程序。
-
为什么要使用native方法?
java使用起来非常方便。然而有些层次的任务用java实现起来很不容易或者我们队程序的效率很在意时问题就来了。
-
与外界环境交互
有时java应用需要于java外面的环境交互,这是本地方法存在的主要原因。你可以想想java需要于一些底层系统,如操作系统和某些硬件交换信息时的情况。本地方法正是这种交流机制:他为我们提供一个非常简洁的借口,而且我们无需去了解java应用之外的繁琐细节。
-
于操作系统交互。
通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至jvm的一些部分就是用c写的。
-
suns java
sun的解释器使用c实现的。这是非他能像一些普通的c一样与外部交互。
-
堆
堆空间概述
-
一个jvm实例(一个应用程序启动就会创建一个jvm实例)只存在一个堆内存,堆也是java内存管理的核心区域。
-
java堆区在jvm启动的时候被创建,其空间大小也就被确定了。是jvm管理的最大一块内存空间。(堆内存大小是可以调节的)
-
《java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上他应该被视为连续的。
-
所有java线程共享java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)
-
几乎所有的对象实例以及数组都应当在运行时分配在堆上。
-
数组和对象永远不会存储在栈上因为栈帧保存引用,这个引用只想对象或者数组在堆中的位置。
-
在方法结束后,堆中的对象不会立马被移除,仅仅在垃圾回收的时候才会被移除
-
堆,是gc执行垃圾回收的重点区域。
-
java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
-
java 8及之后可以分为三部分:新生区(伊甸园区、新生者1区、新生者0区)+养老区+元空间
堆空间大小设置
-
设置堆空间大小 -xms 10m -xmx10m -XX:+PrintGCDetails
-
“-Xms”表示堆空间的起始内存,等价于-XX:InitialHeapSize -X是jvm的参数 ms meroy size
-
"-Xmx"则用以表示堆内存的最大内存等价于-XX:MaxHeapSize
-
-
值得是新生代和老年代的大小 不包括元空间。
-
如何查看内存大小?
- jsp/jstat -gc 进程id
- -XX:+PrintGCDetails打印jvm详情
年轻代和老年代
-
存储在jvm中的java对象可以划分为两类
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡非常瞬速
- 另外一类对象的生命周期却非常长,在某些极端的情况可能和jvm的生命周期一样长。
-
配置新生代和老年代在堆结构的占比
- 默认 -XX:NewRatio = 2,表示新生代占1.老年代占2,新生代占整个堆得三分之一
- 可以修改-XX:newRatio=4,表示新生代占去,老年代占4,新生代占整个堆得1/5.
- -XX:SurvivorRation:设置新生代中Eden区域与survivor区的比例
- -XX:-UserAdaptiveSizePolicy:关闭自适应的内存分配策略。
- -Xmn设置新生代的大小
-
垃圾回收器
-
部分收集:不是完整收集整个java堆得垃圾收集。其中分为一下几种:
-
新生代收集(minor GC/Yong GC):只是新生代的垃圾收集
-
老年代收集(major GC/Old GC):只是老年代的垃圾收集
- 目前只有CMS GC会单独收集老年代的行为
- 注意,很多时候Major GC会和Full GC混淆使用。需要具体分辨是老年代回收还是整堆回收。
-
混合回收(mIxed GC):收集整个新生代以及部分老年代的垃圾
- 目前只有G1 gc会有这种行为
-
-
整堆收集(Full GC):收集整个java堆和方法区的垃圾收集
-
-
堆空间参数
- -XX:+PrintFlagsInitial:查看所有参数的默认值
- -XX:+PrintFlagsFinal:查看所有参数的最终值(可能会存在修改不是默认的)
- -Xms:出事堆空间的内存(默认是物理内存的1/64)
- -Xmx:最大堆空间大小(默认是物理内存的1/4)
- -Xmn:设置新生代大小初始化和最大值
- -XX:NewRatio:设置新生代老年代在堆内存的占比
- -XX:SurvivorRatio:设置新生代中Eden区和S0/S1空间的占比
- -XX:MaxTenuringThreshold:设置新生代垃圾回收的最大年龄
- -XX:+PrintGCDetails:输出详细的GC日志
- -XX:+PrintGC/-verbose:gc:打印gc简要信息
- -XX:handlePromotionFailure:是否设置空间分配担保
-
堆是分配对象存储的唯一选择吗?
- 使用逃逸分析,编译器可以堆代码做如下优化:
- 栈上分配。将堆分配转化为栈分配。如果一个程序在子程序中被分配。要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
- 同步忽略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分或全部可以不存储在内存,而是存储在cpu寄存器中。
- 使用逃逸分析,编译器可以堆代码做如下优化:
方法区
-
内存分配策略
-
优先分配到eden
-
大对象直接分配到老年代:尽量避免程序中出现大量的大对象。
-
长期存活的对象分配到老年代
-
动态对象年龄判断:如果s区中相同年龄的所有对象大小的总和大于s空间的一般,年龄大于或等于该年龄的对象可以直接进入老年区无需达到MaxTenuringThreshold中的年龄要求。
-
空间分配担保 -XX:handlepromotionfailure
-
-
为什么有TLAB(Thread Local Allocation Buffer)
- 堆区是线程共享区域,任何线程都可以访问到堆里的共享数据
- 犹豫对象实例的创建在jvm中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
- 为避免多个线程操作一个地址,需要使用加锁等机制,进而影响分配速度。
-
方法区的内部结构
- 类型信息(类、枚举、注解、接口等信息)
- 字符串常量(常量、静态变量、即时编译后的代码缓存等)
-
运行时常量池
- 运行时常量池是方法区的一部分
- 常量池是字节码文件的一部分。用于存放编译器生成的各种字面量与符号引用。这部分内容将在类加载后存放到方法区的运行时常量池。
- 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
-
方法区的演变
- jdk1.6及以前用永久代,静态变量存放在永久代上
- Jdk1.7有永久代,但已经逐步去永久代,字符串常量池、静态变量移除,保存在堆中
- jdk1.8无永久代,类信息、字段、方法。常量保存在本地内存的元空间,但字符串常量池。静态变量仍在堆。
-
为什么永久去除永久代?
- 为永久代设置空间大小是很难确定的。元空间使用的是本地内存不会出现内存溢出。
- 对永久代调优是很困难的
-
方法取的垃圾收集主要回收哪些内容?
- 常量池中废弃的常量
- 不再使用的类型
-
对象创建的过程(字节码角度)
- 判断对象对应的类是否加载、链接、初始化:虚拟机收到一条new的智力高,首先去检查这个指令的参数能否在metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载解析和初始化。如果没有则进行类加载,并生成对应的class类对象。
- 为对象分配内存:首先要计算该对象占用的空间的大小,然后在堆中划分一块内存给对象,如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。
- 处理并发问题
- 初始化分配到的空间:属性的初始化,0值初始化。
- 设置对象的对象头
- 执行init方法进行初始化
-
对象头里边保存的是什么东西?
- 运行时元数据
- 哈希值
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程id
- 偏向时间戳
- 类型指针
- 指向类元数据,确定该对象所属的类型。
- 运行时元数据
执行引擎
什么是执行引擎
- 执行引擎是java虚拟机核心组成部分
- “虚拟机"是一个相对于"物理机的概念”,这两种机器都有执行代码的能力,其区别是物理机的执行引擎是建立在处理器、缓存、指令集和操作系统层面的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不收物理条件制约地定制指令集和执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
什么是解释器?什么是jit编译器?
解释器:当java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件的内容"翻译"为对应平台的本地机器指令执行。
==JIT(Just In Time Compiler)编译器:==就是虚拟机将源码直接编译成和本地机器平台相关的机器语言。
执行引擎的执行模式参数
-
-Xint 纯解释器
-
-Xcomp编译器
-
-Xmixed混合模式解释器+编译器
StringTable
string的基本特性
-
string:字符串,使用一堆""引起来表示
-
string生命是final的,不可被继承
-
string实现了序列化接口,表示字符串是支持序列化的 实现了comparable表示string是支持比较的
-
string在jdk8及以前内部定义了final char[]value用于存储字符串数据,jdk9改成byte[]
-
intern方法如果字符串常量池中没有对应的data的字符串的话,则在常量池中生成。
string的内存分配
-
string类型的常量池主要使用办法有两种
- 直接使用双引号生命出来的string对象会直接存储在常量池中
- 如果不是用双引号声明的string对象,可以使用string提供的intern()方法。
-
字符串拼接
- 如果是两个字符拼接用的还是字符串常量池里的字符串。
- 如果有变量的话创建的过程如下:
- 创建一个stringBuilder实例:StringBuilder s = new StringBuilder();
- s.append(“a”);
- s.append(“b”);
- s.tostring ->约等于new string(“ab”);
垃圾回收
什么是垃圾
- 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾
- 如果不及时堆内存中的垃圾进行清理,那么,这些垃圾对象所占用的内存空间会一直保存到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出
垃圾回收相关算法
-
垃圾标记阶段(对象是否存活)算法之引用计数算法
- 在jvm中如何标识一个对象已经死亡呢?一个对象不被任何存活的对象继续引用,可以宣判为已经死亡。
- 判断对象存活的两种方式:引用计数算法和可达性分析算法。
- 引用计数算法:对每个对象保存一个==整形的引用计数器熟悉。用于记录对象被引用的情况。
- 优点:实现简单,垃圾对象便于辨识,判断效率搞,回收没有延迟性。
- 缺点:
- 需要单独的字段存储计数器,这样的做法增加了存储空间的开销
- 每次赋值都需要更新计数器,伴随着加法和减法增加了时间开销
- 引用计数器有一个严重的问题,无法处理循环引用的问题
-
垃圾标记阶段算法之引用可达性分析
- 虚拟机栈中引用的对象:各个线程被调用的方法中使用的参数、局部变量等。
-
本地发明合法栈中引用的对象
-
方发渠中类静态属性引用的对象:java类的引用类型静态变量
-
方法区中常量引用的对象:字符串常量池里的引用
-
所有同步锁持有的对象
-
java虚拟机内部的引用
-
反映java虚拟机内部情况的jmxbean、jvmti中注册的回调、代码缓存等、