JVM
java虚拟机,运行在操作系统之上,使用c,和汇编语言写的。
程序计数器
程序计数器是一块很小的内存区域,是当前的线程所执行字节码的行号指示器,每个线程都有一个独立的程序计数器,线程私有,互不影响。字节码解释器工作时,通过改变计数器的值,来选取下一条字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
程序计数器,是唯一不会出现内存溢出的区域,他的生命周期随着线程的创建而创建,线程的结束而死亡
java 虚拟机栈
线程私有,描述的是java方法执行的内存模型,每次方法的调用都是通过栈传递的。
**虚拟机栈存放 局部变量表(8大基本类型)和对象引用(对象的位置) **
Java 虚拟机栈会出现两种错误:StackOverFlowError( Java 虚拟机栈的内存大小不允许动态扩展) 和 OutOfMemoryError(Java 虚拟机栈的内存大小可以动态扩展)
扩展:那么方法/函数如何调用?
Java 栈可以类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。
Java 方法有两种返回方式:
return 语句。
抛出异常。
不管哪种返回方式都会导致栈帧被弹出
本地方法栈
虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
堆
静态变量,常量,类信息(构造方法,接口定义),运行时的常量池,编译后的代码在方法区,但是实例变量和数组和字符串常量池存在堆内存中,和方法区无关。
java虚拟机所管理的内存中最大的一块,是垃圾回收的主要区域,也被称为GC堆,jdk7之前堆内存被分为3部分:
新生代,老年代,永久代。划分的目的 是为了更好地回收内存,和更快的分配内存。
jdk8后永久代被元空间取代,元空间使用直接内存。
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置
方法区(非堆) 也被成为永久代
方法区与内存一样,是线程共享的区域。 静态变量,常量,类信息(构造方法,接口定义),运行时的常量池,编译后的代码在方法区
垃圾回收在此区域是比较少出现,但并不是数据进去方法区就永久存在了。
jdk8以后,永久代被彻底移除,有元空间取代,元空间使用的是直接内存。
整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
运行时常量池:
运行时常量池是方法区得一部分,字符串常量池在堆,运行时常量池在方法区,不过方法区的实现由永久代变为元空间。
对象的创建
对象的定位和访问
通过栈上的 reference 数据来操作堆上的具体对象.
-
句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
-
直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销.
String
总结 :
对于基本数据类型来说,==比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。
在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
一般来说,我们要尽量避免通过 new 的方式创建字符串。使用双引号声明的 String 对象( String s1 = “java” )更利于让编译器有机会优化我们的代码,同时也更易于阅读。
被 final 关键字修改之后的 String 会被编译器当做常量来处理,编译器程序编译期就可以确定它的值,其效果就相当于访问常量
String s1 = new String(“abc”);这句话创建了几个字符串对象?
会创建1个或两个字符串
如果字符串常量池中已存在字符串常量“abc”,则会在堆中创建一个字符串常量“abc”
如果字符串常量池中不存在字符串常量“abc”,则会现在字符串常量池中创建,然后在堆中创建一个字符串常量“abc”
jvm 垃圾回收详解
类加载器
类加载过程: 加载->连接->初始化, 连接又分为3步,验证->准备->解析
所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存
类加载器总结
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader:
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。
- ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
- AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类
双亲委派模型
类加载器收到类加载的请求,委托父类加载器去加载,一直向上委托,直到启动类加载器
启动类加载器能加载就结束,否则通知子类加载器去加载。