文章目录
java虚拟机
1. 内存区域
Tips:
- 编译:.java --> .class
- JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。
暖色调:线程共享区域
冷色调:线程隔离的数据区域
分代策略
堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。给堆内存分代是为了提高对象内存分配和垃圾回收的效率。
试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率,这简直太可怕了。
有了内存分代,情况就不同了,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代(JDK1.8 之后,变成了元空间)中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行垃圾回收,还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率,这些都是内存分代带来的好处。
Eden、 S1 和 S2
堆中内存使用分代策略。主要是:
- 新生代
- Eden区:存放新创建的对象;
- Survivor区,又分为S1,S2;这是考虑新生代的对象新建、删除很频繁,所以考虑了标记复制的垃圾回收的策略;
- 老年代
- 新创建的对象,绝大多数都会存储在
Eden
中; - 当
Eden
满了(达到一定比例)不能创建新对象,则触发垃圾回收(Minor GC
),将无用对象清理掉,然后剩余对象复制到某个Survivor
中,如S1
,同时清空Eden区; - 当
Eden
区再次满了,会将S1
中的不能清空的对象存到另外一个Survivor
中,如S2
, 同时将S1
区中的不能清空的对象,也复制到S2
中,保证Eden
和S1
,均被清空; - 重复多次(默认15次)
Survivor
中没有被清理的对象,则会复制到老年代Old
区中; - 当
Old
区满了,则会触发一个一次完整地垃圾回收(Full GC
),之前新生代的垃圾回收称为(Minor GC
)
新生代的垃圾回收:Minor GC
老年代的垃圾回收:Full GC
JMM
Java Memory Model
前后两张图不是一个概念上的对内存的区分。
2. 常量池
-
Class常量池:
- 在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用 。 字符串常量池:
- 在每个虚拟机中只有一份,存放的是字符串常量的引用值 。 运行时常量池:
- 是在 类加载完成之后,将每个 class常量池 中的符号引用值转存到 运行时常量池 中。类在 解析阶段,将 符号引用替换成 直接引用 ,与 字符串常量池中的引用值保持一致。
- 每个class都有一个运行时常量池。
2.1 Class常量池
常量池(Constant Pool),也叫 Class常量池(Class ConstantC Pool)、静态常量池。
.java文件被编译成 .class文件。
2.1.1 Class文件结构
.class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池(Constant Pool),用于存放编译器生成的各种字面量( Literal )和 符号引用(Symbolic References)。
常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(如下所示),代表当前这个常量属于哪种常量类型。
序号 | 常量池中数据项类型 | 类型标志 | 类型描述 |
---|---|---|---|
1 | CONSTANT_Utf8 | 1 | UTF-8 编码的Unicode字符串 |
2 | CONSTANT_Integer | 3 | int 类型字面值 |
3 | CONSTANT_Float | 4 | float 类型字面值 |
4 | CONSTANT_Long | 5 | long 类型字面值 |
5 | CONSTANT_Double | 6 | double 类型字面值 |
6 | CONSTANT_Class | 7 | 对一个类或接口的符号引用 |
7 | CONSTANT_String | 8 | String 类型字面值 |
8 | CONSTANT_Fieldref | 9 | 对一个字段的符号引用 |
9 | CONSTANT_Methodref | 10 | 对一个类中声明的方法的符号引用 |
10 | CONSTANT_InterfaceMethodref | 11 | 对一个接口中声明的方法的符号引用 |
11 | CONSTANT_NameAndType | 12 | 对一个字段 或 方法的部分符号引用 |
每种不同类型的常量类型具有不同的结构。
2.2 运行时常量池
2.3 字符串常量池(字符串池、String Pool、全局字符串池)
是在类加载完成,经过验证,准备阶段之后,在 堆 中生成字符串对象实例,然后将该字符串对象实例的引用值 存到 String Pool 中。
String Pool 中存的是 引用!!!而不是具体的实例对象,具体的实例对象是在 堆 中开辟的一块空间存放的。
在 HotSpot VM 中通过是一个 StringTable
类实现String Pool的功能; StringTable
是一个哈希表,里面存的是 字符串常量 的 引用 而不是 字符串常量本身。换句话说,只要某些字符串只要被StringTable
锁引用,就等同于被赋予了 字符串常量 的身份。
Tip:这个StringTable在每个 HotSpot VM 的实例只有一份,被所有的类共享。
2.3.1 证明:字符串常量池存放在堆中:
public static void main(String[] sure) throws InterruptedException {
String a = "abccad";
List<String> list = new ArrayList<>();
for(int i =0;i<Integer.MAX_VALUE; i++)
{
String str = a + a;
a = str;
list.add(str.intern());
}
}
2.3.2 new String() 创建了几个对象
情况一:
String str = "计算机";
这行代码会直接在字符串常量池中创建一个字符串对象,然后将栈中的str变量指向它。
情况二:
String str = "计算机";
String str2 = "计算机";
如果我们再创建一个“str2”,其值也是“计算机”的话就会直接将栈中的“str2”变量也指向字符串常量池中已存在的“计算机”对象。
避免重复创建对象,字符串常量池存在的原因也是如此。
情况三:
String str = new String("计算机");
情况四:
String str = new String("计算机");
String str2 = new String("计算机");
2.3.3 intern()方法
当intern()方法被调用的时候,如果字符串常量池中已经存在这个字符串对象了,就返回常量池中该字符串对象的地址;如果字符串常量池中不存在,就在常量池中创建一个指向该对象堆中实例的引用,并返回这个引用地址。
String str = new StringBuilder("计算机").append("软件").toString();
System.out.println(str.intern()==str); // true
String str = new StringBuilder("ja").append("va").toString();
System.out.println(str.intern() == str); // false
java对象并不是我们创建的,因为“java”是java语言中的关键字,它是字符串常量池中默认存在。
!!! !!! !!! !!! !!! !!! !!! !!!
我们在前面有如上说法,但是在本小节中,为了方便理解都是直接在字符串常量池中直接画出各个字符串,但是其实他们应该是这样的:
图解jdk1.8中的intern()方法,包教包会
Java字符串池(String Pool)深度解析
class常量池、字符串常量池和运行时常量池的区别
3. 对象创建
- 类 是否存在方法区的常量池中;不存在,则执行类加载过程
- new:内存分配
- 初始化零值
- 必要的设置(哪个类的实例、如何查询元数据信息)
- < init >() : 初始化(构造)
3.1. 类加载
-
加载
- .class文件加载到内存中 连接
- 验证:格式、语义、字符引用的检查
- 准备:类变量初始化 0
- 解析:字符引用 转换为 直接引用 初始化
- 类变量赋值,static方法块操作
经过以上三个步骤之后,会在内存中生成一个对应的Class对象。
另外,最初的加载操作之后,加载和连接可能会交替进行。
符号引用 :
以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。 在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。 比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用:
简单的说就是:某个主体(类、方法…)在实际内存中的地址。但具体实现时候有不同的方式;如下:
- 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
- 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
- 一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。()
3.1.1 类加载器
类加载器: 是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。
-
java中存在3种类型的类加载器:
- 引导类加载器;
- 扩展类加载器;
- 系统类加载器。
- 三者是的关系是:引导类加载器是扩展类加载器的父类,扩展类加载器是系统类加载器的父类。
- BootstrapLoader,jre/bin中
- extensionLoader jre/bin/ext中
- AppLoader,用户自定义
构建自定义加载器:
继承 ClassLoader
并重写 loadClass()
方法。
向上检查父类加载器是否已经加载目标类(4321),向下尝试加载目标类(1234)
3.1.2 双亲委派机制
双亲委派机制:
简单的说就是,自底向上检查类是否加载,自顶向下对类进行加载。
另一方面,双亲委派模型指的是类加载器之间的层次关系。该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合关系来实现,而不是通过继承。
双亲委派机制
双亲委派模型对于保证 Java 程序的稳定运作很重要,但它的实现却非常的简单,实现双亲委派的代码都集中在 java.lang.ClassLoader
的 loadClass()
方法中。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1. 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;
}
}
能不能自己写个类叫java.lang.System?
答案: 通常不可以,但可以采取另类方法达到这个需求。
解释: 为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。
但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器加载一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载
3.2. 内存分配
为new的对象分配内存空间。
方法:
指针碰撞
空闲列表
并发问题:
CAS
TLAD?
3.3 零值设置
初始化对象中的字段全为零值。
保证了对象不赋予初值可直接使用
3.4. 设置对象头
- 对象头(Header)
- 运行时候对象数据
- 类型指针
- 实例数据(Instance Data)
各种类型的字段
- 对齐填充(Padding)(8字节对齐)
不一定存在,只是占位符。任何对象的起始地址必须是8字节的整数倍。
3.4.1 对象头的内容
。。。
与多线程相关的 Mark Word;
类对象信息
3.5 执行init方法
依照程序猿的意图进行初始化:构造方法、语句块
顺序大致如下:
- 父类变量初始化
- 父类语句块
- 父类构造函数
- 子类变量初始化
- 子类语句块
- 子类构造函数
4. 垃圾回收
5. volatile
-
可见性
-
防止指令重排
防止指令重排使用单例模式来讲解
5.1 单例模式中的 volatile
public class LazyMan{
// 1. 私有化构造函数
private LazyMan(){}
// 2. 静态对象
private static LazyMan lman;
//------------------------------------------------------------------------------------------
// 3.1 单线程之下获取实例方法
public static LazyMan getInstance(){
// 4. 判空
if(lman == null)
{
lman = new LazyMan();
}
return lman;
}
//------------------------------------------------------------------------------------------
// 3.2 获取实例方法
public static LazyMan getInstance_multi_thread(){
// 4. 双重检测锁 DCL懒汉式 : Notice 3
if(lman == null){
synchronized(LazyMan.class){// 5. 加锁,保证进去之后没有其他线程进来
if(lman == null){//
lman = new LazyMan(); // 不是原子性的操作 : Notice 1
//> 1. 分配内存空间
//> 2. 执行构造方法
//> 3. 将对象指向分配的空间
}
}
}
return lman; // 123是一般的顺序,但是当进行指令重排之后,顺序可能是132
// 那么就存在一种情况 Thread1:拿到锁,已经执行了 1、3步骤。
// 这时候Thread 2 调用该方法发现 lman != null,则直接返回了。
// 但其实Thread1只是进行了1、3步骤还没有完成2步骤。所以Thread 2拿了一个未被初始化的lman。
// 怎么解决??? ---> 防止其指令重排
}
//------------------------------------------------------------------------------------------
// 3.3 获取实例方法
// 2. 静态对象
private volatile static LazyMan lman; // 使用volatile进行修饰 : Notice 2
public static LazyMan getInstance_multi_thread(){
// 4. 双重检测锁 DCL懒汉式
if(lman == null){ // 4.1 减少线程对同步锁的竞争
synchronized(LazyMan.class){// 5. 加锁,保证进去之后没有其他线程进来
if(lman == null){// 保证单例
lman = new LazyMan(); // 不是原子性的操作
}
}
}
return lman;
} // 仍然不安全,因为存在反射
Notice 1:
Object obj = new Object();
// 不是原子性操作
- 分配内存空间
- 执行构造方法
- 将对象指向分配的空间
期望的顺序是:123; 但是JVM为了高效,有的时候会进行指令重排,所以可能的顺序是 132
Notice 2:
volatile的优势:
- 可见性(直接使用主内存(这儿的知识点是:主内存和工作内存))
- 防止指令重排
这儿主要讲 volatile 的作用,更多单例模式的信息见我的另一篇文章 Java单例模式