熟悉Java核心基础知识,对集合、线程等都有了解,能运用模块化、面向对象的方式编程。
1.Java八种基本数据类型
Java的数据类型分为两大类:①基本数据类型 ②引用数据类型
2.面向对象三大特性
封装、继承、多态。
简要介绍一下/谈一下你的理解:
封装:把对象的状态信息(属性)隐藏在内部,不允许外部对象直接访问对象的内部信息
封装的主要目的是隔离复杂度,保护对象的内部状态不被外部随意修改,只通过对象提供的接口进行访问。
在Java中,我们通常通过以下步骤实现封装:
- 使用访问修饰符:使用访问修饰符来定义类的属性和方法。通常,private以防止外部直接访问。通过public来提供对属性的访问和修改。
- 使用类:我们将相关的属性和方法定义在一个类中,形成一个独立的对象。这个对象可以包含数据和对数据的操作。
- 提供接口:我们提供公共的接口(方法)供外部访问和修改对象的内部状态。这些接口通常被设计为符合对象的行为和职责。
继承
继承是一种重要的面向对象编程特性,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。通过继承,子类可以重用父类的代码,从而提高了代码的重用性和可维护性。
继承是通过关键字extends来实现的。子类可以继承父类的非私有属性和方法,并可以添加新的属性和方法或覆盖父类中的方法
继承的作用:
- 代码重用:继承允许子类重用父类的代码,从而避免了大量重复代码的编写。通过继承,子类可以自动获得父类中的属性和方法,无需重新编写。这大大减少了代码的冗余,提高了代码的可维护性。
- 扩展性:继承允许子类在继承父类功能的基础上添加新的功能。子类可以通过添加新的属性和方法或覆盖父类中的方法来实现自己的功能扩展。这使得子类能够更加灵活地满足特定的需求。
- 层次结构:继承可以形成类的层次结构,使得类之间的关系更加清晰。通过继承,我们可以将具有相似属性和方法的类组织在一起,形成一个类族。这有助于我们更好地理解和使用这些类。
多态:多种形态,就是去完成某个行为,当不同的对象去完成时会产生不同的状态。
多态主要体现在方法重载和方法重写两个方面。
- 方法重载是 通过在同一个类中定义多个同名但参数列表不同的方法来实现多态。
- 方法重写则是子类可以定义一个与父类同名同参数的方法来实现多态。
- 接口实现: 通过实现接口并在实现类中定义相应的方法来体现多态。接口是一种完全抽象的类,而实现类则需要提供具体的方法实现。
1.重写和重载区别
重写:是子类对父类方法重写,要求方法名、参数列表、返回值类型与父类相同,修饰符访问权限要大于等于父类,抛出的异常小于等于父类。如果父类方法修饰符为private,那么子类就不能重写该方法。
重载:是同一类中,不同的函数使用相同 的函数名,要求方法名相同,参数类型个数顺序、返回值类型有不同。
2.==和 equals 的区别
==:对基本数据类型比较值是否相等;对引用数据类型比较地址是否相同。
equals:默认比较地址是否相同,对String、Integer、Date等对equals进行重写,可以比较内容是否相同。
3.接口和抽象类的区别
实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
类可以实现很多个接口;但是只能继承一个抽象类。
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写),
而接口中不能包含普通方法, 子类必须重写所有的抽象方法.
构造函数:抽象类可以有构造函数;接口不能有。
main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方 法。
访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符
4.String、StringBuffer、StringBuilder区别
可变性:
String类中他使用的是字符数组保存字符串,所以String对象他是不可变的
StringBuffer、StringBuilder这两种是可变的
线程安全性:
String的对象他是不可变的,可就可以理解为是常量,线程是安全的。
StringBuffer对方法中加了同步锁或者对调用方法加了同步锁,所以线程是安全的。
StringBuilder并没有对方法进行加同步锁,所以是非线程安全的
性能:
每次对String类型进行改变的时候,都会生成一个String对象,这时候空指针会指向新的String对象。
操作少量的字符串 用String; 单线程下需要操作大量数据 用StringBuilder;多线程下需要操作大量数据 用StringBuffer
5.java几种修饰符的区别
访问修饰符:public、protected、private、default
1.public:公共;使用对象:类、接口、变量、方法。使用场景:当你希望某个类的功能被广泛使用时,应将其设置为 public。
2.default:默认;使用对象:类、接口、变量、方法。使用场景:该类的实现细节仅在包内部可见时
3.private:私有;使用对象:成员变量,成员函数。使用场景:方法、变量或构造函数仅作为类的内部实现的一部分时
注释:只在本类中有效;不能修饰类;
4.protected:保护;使用对象:变量、方法。使用场景:某个类的成员可以被子类继承,但又不希望被其他不相关的类访问时。
注意:不能修饰类(外部类)。
注释:保护权限,不同包之间只有继承了的子类才能访问;
总结:public 提供了最大的灵活性,protected 允许一定程度的继承访问,默认访问级别限制了包内访问,而 private 则提供了最强的封装。
非访问修饰符:final、static、abstract、synchronized、volatile
abstract:表示该类或方法是抽象的,不能被实例化或调用,只能被继承或实现。
final:表示该类、方法或变量不能被继承、重写或重新赋值。
static:表示该方法或变量是类级别的,可以通过类名直接访问,不需要创建实例。
synchronized:表示该方法是同步的,多个线程不能同时访问该方法
volatile:表示该变量是易变的,每次读取变量时都会从主存中获取最新值,每次修改变量时都会立即写入主存。
6.final、static修饰符的区别
final和static的区别:
很多时候会容易把static和final关键字混淆,static作用于成员变量用来表示只保存一份副本,也就是在内存中只有一个,静态变量被所有的对象所共享,在内存中只有一个副本,而final的作用是用来保证变量不可变。
final
(1)final修饰的成员变量,一旦有了初始值就不能被重新赋值。由于成员变量具有默认值,用final关键字修饰后 不会再给默认值,必须手动赋值,否则会报错。
(2)修饰类和方法,不能被继承和重写。
static
(1)static是静态的意思,可用来修饰 成员方法、成员变量。static修饰后的变量、方法,可以被类的所有对象共享。
(2)static不能修饰类,被static修饰的方法可以直接使用类名.方法名
(3)静态方法只能访问静态成员。不能直接访问实例成员
(4)静态方法中是不可以出现this关键字的。this指当前对象,静态方法中不用声明实例对象
静态方法和实例方法有什么不同
1.调用方式:
外部调用示例方法,使用对象.方法名
;外部调用静态方法既可以对象.方法名
也可以类名.方法名
。也就是说,调用静态方法无需创建对象 。
2.访问类成员是否存在限制:
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员。
因为:静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在
,需要通过类的实例对象去访问。在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
5.String 对象的创建
JDK7之后:
(1)有两种方式创建String对象:字面量赋值、new关键字
- 字面值创建:JVM首先检查字符串常量池中是否已经存在该字符串,如果存在 则直接返回字符串对象的引用,否则就创建一个新的字符串对象并放入字符串常量池中,最终将该对象的引用赋值给变量str。【字符串常量池中不会存储相同内容的字符串】
- 通过new关键字创建字符串对象:会先检查字符串常量池中是否有相同的字符串,如果有则拷贝一份放到堆中,然后返回堆中地址;如果没有 就先在字符串常量池中创建"abc"这个字符串,而后再复制一份放到堆中 并把堆地址返回给str。【字符串常量池和堆内存都会有这个对象】
- 方式一效率比方式二高。
(2) String str1="abc"和String str2=new String(“abc”)区别
- String str=“abc"创建了几个对象? 0个 或 1个。如果字符串常量池中没有"abc”,则在常量池中创建"abc" 并让str引用指向该对象(1个);如果字符串常量池中有"abc",则一个都不创建 直接返回地址值给str(0个)
- String str=new String(“abc”)创建了几个对象? 1个 或 2个。如果字符串常量池中没有"abc",则在字符串常量池和堆内存中各创建一个对象,返回堆地址(2个);如果常量池中有"abc",则只在堆中创建对象并返回地址值给str(1个)。【new相当于在堆中新建了value值,每new一个对象就会在堆中新建,地址值也因此不同,堆中的value存储着指向常量池的引用地址】
(3)字符串拼接操作
- 常量 与 常量 的拼接结果在 常量池,原理是 编译期 优化;常量池 中不会存在相同内容的常量
- 只要其中一个是变量,结果就在堆中
- 变量拼接的原理 是StringBuilder
- 如果拼接的结果调用 intern() 方法,则主动将常量池中 还没有的字符串对象放入池中,并返回地址
(4)String类型变量+常量
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true——常量折叠,字符串常量池中同一个
System.out.println(str4 == str5);//false——两个引用在程序编译期是无法确定的所以无法常量折叠
(1)对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。
对于 String str3 = “str” + “ing”; 编译器会给你优化成 String str3 = “string”; —— 这就是常量折叠。
只有编译器在程序编译期就可以确定值的常量才可以:
- 基本数据类型( byte、boolean、short、char、int、float、long、double)以及字符串常量。
- final 修饰的基本数据类型和
- 字符串变量字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
(2)引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
String str4 = str1 + str2
实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。
不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。
String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++) {
s += arr[i];
}
System.out.println(s);
上述代码:StringBuilder 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder 对象。
String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder();
for (String value : arr) {
s.append(value);
}
System.out.println(s);
上述代码:如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了。
6.内部类
将一个类的定义放在里另一个类的内部,这就是内部类。
广义上我们将内部类分为四种:成员内部类、静态内部类、局部(方法)内部类、匿名内部类。
内部类基本概念:如果A类需要直接访问B类中的成员,而B类又需要建立A类的对象。这时,为了方便设计和访问,直接将A类定义在B类中。就可以了。A类就称为内部类。内部类可以直接访问外部类中的成员。而外部类想要访问内部类,必须要建立内部类的对象。
class Outer {
int num = 4;
class Inner {
void show() {
System.out.println("inner show run " + num);
}
}
public void method() {
Inner in = new Inner();// 创建内部类的对象。
in.show();// 调用内部类的方法。
}
}
可以使用内部类继承某个具体的或抽象的类,间接解决类无法多继承引起的一系列问题。
匿名内部类
匿名内部类:是没有名字的内部类。就是内部类的简化形式。一般只用一次就可以用这种形式。
原本创建子类或者实现类去继承父类和实现接口,才能重写其中的方法。但是有时候子类和实现类只使用了一次。这个时候可以使用匿名内部类,不用去写子类和实现类,起到简化代码的作用。
【也就是说可以直接实现接口方法,和重写父类方法】
匿名内部类的格式:父类/接口 对象 = new 父类/接口(){ 重写父类/接口中的方法 };
这样做就把子类继承父类,重写父类中的方法,创建子类对象,合成了一步完成,减少了其中创建子类的过程。
或者将实现类实现接口,重写接口中的方法,创建实现类对象,合成了一步完成,减少了其中创建实现类的过程
Collections类
Collections类是java提供的一个操作List,Set和Map等集合的工具类。Collections类中提供了一系列操作集合的静态方法,使用这些方法可以实现对集合元素的排序、查询、修改等操作。
在这个包下java.util.Collections
直接使用:
Collections.reverse(list);//反转
Collections.shuffle(list);//随机打乱
Collections.sort(list);//升序
Collections.sort(list, new Comparator<Object>() {//可以根据指定的Comparator指定的顺序对指定的List集合进行升序排序。
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof Integer && o2 instanceof Integer)
return (Integer)o2 - (Integer)o1;
return -1;
}
});
Collections.swap(list, 0, list.size() - 1);//指定0与最后一个元素交换
int max = (int) Collections.max(list);//集合中的最大值
int min = (int) Collections.min(list);//集合中的最小值
int times_0 = Collections.frequency(list, 11);//指定元素在指定集合中一共出现的次数
Collections.copy(list2, list);//将指定的旧集合中的元素拷贝到指定的新集合中。当新集合的长度小于旧集合时,抛出下标越界异常
Collections.replaceAll(list, "Cyan", "Ice")//将集合中指定的旧值全部替换为指定的新值
7.Java集合类有哪些
主要集合类包括:
- List:有序集合,允许元素重复。主要实现类有ArrayList、LinkedList和Vector等。
- ArrayList:基于动态数组实现,适合频繁的随机访问
- LinkedList:基于双向链表实现,适合频繁插入和删除
- Vector:与ArrayList类似同样维护一个数组, 但通过加synchronized来保证线程安全
- Set:无序集合,不允许元素重复。主要实现类有HashSet、LinkedHashSet、TreeSet。
- HashSet:基于哈希表实现,不保证集合的迭代顺序
- LinkedHashSet:继承自HashSet,通过链表维护元素插入顺序
- TreeSet:基于红黑树实现,可以对集合中的元素进行排序
- Map:键值对集合,一个键最多只能映射到一个值。主要实现类有HashMap、LinkHashMap、TreeMap。
- HashMap:基于哈希表实现,不保证映射顺序
- LinkHashMap:继承自HashMap,通过链表维护键值对的插入顺序
- TreeMap:基于红黑树实现,可以对键进行排序
- Queue:元素有序,可重复,按特定的排队规则来确定先后顺序。
Java 集合框架概览:
8.HashMap底层数据结构
HashMap核心知识,扰动函数、负载因子、扩容链表拆分
HashMap 的底层数据结构是哈希表(散列表),具体实现为数组+链表(JDK1.8前)或数组+链表+红黑树(JDK1.8及以后)
主要特点:
- 数组:用于存储桶(Bucket)的数组,每个桶要么存储一个元素(键值对),要么存储一个指向链表头结点的引用(哈希冲突时)
- 链表:用于解决哈希冲突,的那个多个键值对银蛇到数组的同一位置时,会形成一个链表。
- 红黑树:联保长度超过某个阈值(默认为8),链表会转换为红黑树以提高查找效率。红黑树是自平衡的二叉查找树,能保持较低的高度,从而减少查找时间。
工作流程:
9.List和Set的区别,以及底层数据结构实现
List中的元素是有序、可重复的。
List接口有三个实现类:LinkedList基于链表实现,链表内存是散列的,增删快,查找慢;ArrayList基于数组实现,非线程安全,效率高,增删慢,查找快;Vector也是基于数组实现,但使用synchronized保证线程安全,效率低,增删慢,查找慢;
Set中的元素是无序的不可重复的。
Set接口有三个实现类:HashSet底层是基于HashMap实现,使用该方式时需要重写 equals()和 hash Code()方法;LinkedHashSet继承于 HashSet,通过来链表来维护键值对的插入顺序;TreeMap底层基于红黑树实现,可以对键进行排序。
10.为什么arraylist检索快增删慢
1.检索快:
- 直接通过索引访问,因为数组内存地址联系,通过首地址+(元素长度*下标)就能计算出内存地址
- 时间复杂度低,查找的时间复杂度为O(1)
2.增删慢
- 需要移动元素,尤其是操作发生在列表开头或中间位置时,后续元素需要前移或后移,导致时间复杂度高。
- 当ArrayList容量不足时,需要扩容。扩容涉及创建新数组,然后间原数组复制到新数组中,时间复杂度O(n)且会导致内存碎片化。
3.删除的额外开销
删除元素使,还需要处理数组末尾的null元素???,以及可能的数组缩容
11.红黑树和链表的查询效率对比
BST(二叉搜索树),AVL(平衡二叉树)、RBT(红黑树)的区别
红黑树是自平衡二叉搜索树,通过一些列旋转和重新着色操作维护树的平衡性。
平衡性有一组规则来保证高度近似O(logn),每个节点要么是红要么是黑,根节点是黑色,叶子结点是黑色(NIL节点、空节点)。
因此红黑树的查询时间复杂度为O(logn)
/
链表是线性数据结构,由一系列节点组成,每个节点包含数据和指向下一个节点的指针。链表中每个元素通过指针链接,不支持索引访问元素,每次查询需要从头结点开始循序遍历,查询的时间复杂度O(logn)。
12.多线程下想用hashmap怎么办
多线程下使用HashMap可能会导致数据不一致问题,因为HashMap不是线程安全的。
(1)使用Java集合提供的Collection.synchronizedMap方法,使其成为一个线程安全的Map。
每次只能有一个线程访问Map,可能个会限制并发性。
(2)ConcurrentHashMap提供更高级的并发级别。内部使用分段锁(分段加锁,减少锁的竞争),使得多个线程可以同时读写不同的数据段,从而提高性能。
(3)使用读写锁来控制对Map的访问。这种锁允许多个读操作同时进行但写操作互斥。
常见多线程的使用方式(创建线程的方式)
1.继承Thread类
继承java.lang.Thread类并重写run()方法,可以创建一个新线程。
创建该类的实例并调用start()方法来启动线程。
优点:简单直观
缺点:Java不支持多重继承,如果类已经继承了另一个类,则无法再继承Thread类
class Grape extends Thread { /**当某个类继承了Thread类后,就可以当作线程使用*/
@Override
public void run() {
}
}
public class Thread_Demo1 {
public static void main(String[] args) {
//创建线程对象
Grape grape = new Grape();
//启动线程!
grape.start();
}
}
2.实现Runnable接口
实现java.lang.Runnable接口并重写run()方法。
将任务实例传递给Thread类的构造器来创建线程对象,调用start()方法启动线程。
优点:避免java单继承的限制;线程与任务Runnable实例分离,便于任务的传递和共享。
缺点:相比继承Thread类 稍显复杂
class Fruit {}
class Apple extends Fruit implements Runnable {
@Override
public void run() {
System.out.println("线程是:" + Thread.currentThread().getName());
}
}
public class Thread_Demo2 {
public static void main(String[] args) throws InterruptedException {
Apple apple = new Apple();
//实例传递给Thread类的构造器来创建线程对象
Thread thread = new Thread(apple);
thread.start();
}
}
3.使用Callable和Future
Callable接口类似于Runable,但它可以返回一个结果并可以抛出异常。Future接口用于表示异步计算的结果。
FutureTask是Future和Runnable之间的桥梁,它实现了Runnable并封装了Callable。
优点:可以返回执行结果和抛出异常。
缺点:需要处理Future的结果,可能会阻塞调用线程。
4.使用线程池ExecutorService
线程池是一种基于池化技术的多线程管理工具,可以重用线程,减少线程创建和销毁的开销,提高系统的相应速度和吞吐量。
优点:提高了资源利用率和系统相应速度;便于管理和控制线程的生命周期。
缺点:需要合理配置线程池的参数,比如线程池大小、队列类型等以避免资源耗尽或性能瓶颈。
例1:
首先是MyCallable类,继承Callbale接口,重写call()函数
之后是主函数,使用了线程池,以及利用Future接口的get()方法来获取返回值(Future接口概念:异步接收,ExecutorService .submit()的返回结果,其提供的get()方法异步等待call()的返回值)
,这个案例可以清楚地看到,get()的阻塞线程的效果。
package Threads;
// Commons IO是针对开发IO流功能的工具类库
// FileUtils文件工具,复制url到文件
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
// 线程创建方式三:实现callable接口
/*
callable的好处
1.可以定义返回值
2.可以抛出异常
*/
public class MyCallbale implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("开始执行");
int sum = 0;
for(int i = 0; i <= 100; i++){
//这里加这个等待的时间是为了,更明显的看出get的阻塞效果,除此之外没有别的含义。
Thread.sleep(50);
sum += i;
}
System.out.println("执行结束了");
return sum;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//定义任务
MyCallbale task = new MyCallbale();
//定义线程池
ExecutorService es = Executors.newSingleThreadExecutor();
//提交任务,这里future会异步接收es正在执行的task的返回值
Future<Integer> future = es.submit(task);
//关闭线程池
es.shutdown();
//获取返回值,这里的get是阻塞式的等待future的返回值的
Integer sum = future.get();
System.out.println(sum);
}
例2:
- 将两个任务分别创建出来,一个计算1-50的和,另一个计算51-100的和
- 创建线程池,因为只有两个任务,所以创建固定线程数量的线程池
- 将两个任务提交到线程池,并利用Future接口的get方法接收返回的结果
import java.util.concurrent.*;
public class TestFuture {
public static void main(String[] args) throws Exception{
//匿名内部类
Callable<Integer> thread1 = new Callable<Integer>(){
@Override
public Integer call() throws Exception{
System.out.println(Thread.currentThread().getName() + "正在执行1-50的和");
int sum = 0;
for(int i = 1; i < 50; i++ ){
sum += i;
}
System.out.println(Thread.currentThread().getName() + "执行完成");
return sum;
}
};
//匿名内部类
Callable<Integer> thread2 = new Callable<Integer>(){
@Override
public Integer call() throws Exception{
System.out.println(Thread.currentThread().getName() + "正在执行50-100的和");
int sum = 0;
for(int i = 50; i < 101; i++ ){
sum += i;
}
System.out.println(Thread.currentThread().getName() + "执行完成");
return sum;
}
};
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Integer> sum1 = es.submit(thread1);
Future<Integer> sum2 = es.submit(thread2);
Integer total = sum1.get() + sum2.get();
System.out.println(total);
}
}
JDK 1.8运行时数据区域:
线程私有的:【它们的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。】
- 程序计数器
- 虚拟机栈( VM Stack)
- 本地方法栈
线程共享的:
- 堆(内含字符串常量池)
- 方法区
- 直接内存 (非运行时数据区的一部分)
堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
方法区
方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。在不同的虚拟机实现上,方法区的实现是不同的。
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。
方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据
。
字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
直接内存
直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
虚拟机对象创建
1.类加载检查
- 虚拟机遇到一条 new 指令时,首先去检查指令的参数是否能在常量池中定位到这符号引用,并且检查引用代表的类是否被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
分配方式有 “指针碰撞
” 和 “空闲列表
” 两种,选择哪种分配方式由 Java 堆是否规整决定
。
内存分配的两种方式 (补充内容,需要掌握):
- 指针碰撞:
- 适用场合:堆内存规整(即没有内存碎片)的情况下。
- 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
- 使用该分配方式的 GC 收集器:Serial, ParNew
- 空闲列表:
- 适用场合:堆内存不规整的情况下。
- 原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
- 使用该分配方式的 GC 收集器:CMS
内存分配并发问题(补充内容,需要掌握)
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
. CAS+失败重试: CAS 是如果冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
. TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
3.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用
,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
将这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
5.执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始
, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法
,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
对象的访问定位
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 引用数据
来操作堆上的具体对象。
对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针
。
直接指针:
reference中存储的直接就是对象地址