精华内容
下载资源
问答
  • 基本类型的变量如果是临时变量,只要定义了,就会分配内存空间,不管是否被赋值;如果是作为对象的属性出现,只要该对象不实例化,就不会分配内存空间。... 2)保存类的实例,即堆区对象的引用(指针) 3)也可以用来
    基本类型的变量如果是临时变量,只要定义了,就会分配内存空间,不管是否被赋值;如果是作为对象的属性出现,只要该对象不实例化,就不会分配内存空间。

    一个完整的Java程序运行过程会涉及以下内存区域:
    1、寄存器:JVM内部虚拟寄存器,存取速度非常快,程序不可控制。
    2、 栈:保存局部变量的值,包括:
    1)用来保存基本数据类型的值;
    2)保存类的实例,即堆区对象的引用(指针)
    3)也可以用来保存加载方法时的帧
    3、堆:用来存放动态产生的数据,比如new出来的对象。注意创建出来的对象只包含属于各自的成员变量,并不包括成员方法。因为同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法,并不是每创建一个对象就把成员方法复制一次。

    4、常量池:JVM为每个已加载的类型维护一个常量池,常量池就是这个类型用到的常量的一个有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用(1)。池中的数据和数组一样通过索引访问。由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。常量池存在于堆中。

    5、代码段:用来存放从硬盘上读取的源程序代码。
    6、数据段:用来存放static定义的静态成员

    注意:
    1.一个Java文件,只要有main入口方法,我们就认为这是一个Java程序,可以单独编译运行。
    2.无论是普通类型的变量还是引用类型的变量(俗称实例),都可以作为局部变量,他们都可以出现在栈中。只不过普通类型的变量在栈中直接保存它所对应的值,而引用类型的变量保存的是一个指向堆区的指针,通过这个指针,就可以找到这个实例在堆区对应的对象。因此,普通类型变量只在栈区占用一块内存,而引用类型变量要在栈区和堆区各占一块内存。
    展开全文
  • 列表变量是一个引用,还好学过C语言,所以要注意,对于引用的修改就是用指针操作,会直接修改原本的列表对象,只有部分方法是不修改的.list()用来转换列表,另外列表还有列表生成式,是非常灵活的写法,也是很python的写法....

    列表及方法

    列表是python内置的数据结构或者说是类型之一,可以包含各种数据类型作为元素.有点类似于C语言里的链表.

    列表变量是一个引用,还好学过C语言,所以要注意,对于引用的修改就是用指针操作,会直接修改原本的列表对象,只有部分方法是不修改的.

    list()用来转换列表,另外列表还有列表生成式,是非常灵活的写法,也是很python的写法.

    除了正常的切片,索引,通过赋值修改某个位置的元素,in 等内置方法,还有list类的方法:

    append(p_object)

    列表尾部追加一个元素.修改对象.返回None.

    clear()

    删除列表里的所有元素,返回空列表,直接修改对象,返回None.

    copy()

    赋给一个新变量则得到当前列表的复制,是一个新列表,引用和原来的列表不同.注意这个方法得到的是浅拷贝

    count(value)

    返回value在列表元素中出现的整型次数,不修改原列表

    extend(iterable)

    将一个可迭代对象iterable里的元素增加到列表里.例如b.extend(range(10,21)),本身返回None

    index(value, start=None, stop=None)

    查找值返回索引整型,如果没有会报ValueError

    insert(index, p_object)

    在指定的索引位置插入元素,该索引位置上的原来元素及后边元素向后移动

    pop(index=None)

    弹出某一个索引的元素,返回这个元素,同时将这个元素从原列表里删除,如果不指定索引,则默认弹出最后一个.如果列表为空则报IndexError

    remove(value)

    从列表里去掉查到到的第一个value,如果找不到value,会报ValueError.如果成功去除,返回None.Python删除列表内的元素还可以用del li[index]或者del 切片的方法

    reverse()

    就地倒转列表顺序,直接修改原列表

    sort(key=None, reverse=False)

    就地排序,直接修改原列表,key可以用一个函数或者lambda表达式来做判断,reverse默认是升序,如果是True则降序.内置函数有一个sorted()函数也可以用来排序,但是sorted函数不会修改原列表

    直接赋值 浅拷贝 深拷贝

    直接赋值:其实就是对象的引用(别名)。

    浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象。

    深拷贝(deepcopy):如果需要深拷贝,导入copy模块的deepcopy方法,完全拷贝了父对象及其子对象。

    元组

    元组的类是tuple,元组一般用来传递一组固定结构的数据,可以方便的拆解到变量里使用.

    元组其实是对列表的二次加工,主要的区别就是元素不可被修改,也不能增加或者删除元素.

    用tuple()来生成元组,tuple(iterable)将迭代器的内容拿到一个元组里.

    注意,写元组的时候,一般要在最后一个元组后边加一个逗号,就可以很清楚的看到是元组.这是惯例.

    元组支持索引和切片,切片得到的结果还是一个元组.元组也是一个可迭代对象.

    字符串和列表,元组都可以转换.元组也支持.join方法.

    元组的一级元素不能够被修改,但是一级元素如果是引用,则可以修改元素的内容.就类似于元组是一排玻璃杯,这些玻璃杯不能更换,但是玻璃杯里装多少水,是可以修改的.

    count(self, value)

    count方法,返回次数

    index(value, start=None, stop=None)

    查找值返回索引整型,如果没有会报ValueError

    字典

    字典就是python语言的哈希表,拥有键值对,键key必须是一个可以hash的值,所以列表不能够作为key,字典也不能够作为key,因为这二者都会产生变化.值则没有限制,可以是任何对象.

    字典支持for 循环,但是默认是循环key,in 方法的默认也是判断key.字典是无序的,字典值的索引就是键.

    字典的方法:

    clear()

    返回None,清除整个字典

    copy()

    复制字典,注意这个也是浅复制

    fromkeys(*args, **kwargs)

    这个因为有@staticmethod,这是一个静态方法,也就是通过类名调用的方法,这个方法的内容是用给定的克迭代对象和对应的值来产生一个字典,在下边有示例

    get(k, d=None)

    字典可以直接用key来获得值,但是如果key不存在的话会报错.这时候可以用get方法来取得k对应的值,如果键中没有k,则会返回d,d默认是None,可以自行指定.get函数通常用在结合键值是否在字典内和需要返回一个特殊东西时候的场合

    items()

    返回这个字典里所有的键值对,类似一个集合.很python的写法就是用两个变量来同时接收键和值

    keys()

    返回字典里的所有的key构成的一个类似集合的对象,对迭代器的操作都可以用在返回对象上

    pop(k, d=None)

    按照键k对应的值从字典里弹出.如果找不到k,则返回d,如果d没有指定,则返回KeyError

    popitem()

    从字典里弹出一个键值对,无法控制先弹哪一个,返回的是一个键值对构成的元组.如果字典已经为空,则返回KeyError错误

    setdefault(k, d=None)

    将字典里k的值设置为d.但是如果k已经存在,则不做任何修改.两种情况都返回k对应的值.

    update(E=None, **F)

    更新列表,返回None.更新的机制是可以用k=v来当参数,也可以传一个字典,如果键已经存在,则更新值,否则将键值对更新到字典内.

    values()

    返回字典里所有值构成的一个类似于元组的东西.可以使用迭代器操作.

    用静态方法生成字典

    # 用dict类的静态方法生成字典

    v = dict.fromkeys([1, 2, 3, 4, 5, ], 'test')

    print(v)

    # 结果:

    # {1: 'test', 2: 'test', 3: 'test', 4: 'test', 5: 'test'}

    enumerate的用法

    # enumerate(iterable[, start]),前边是一个可迭代对象,后边是指定开始的索引.

    # enumerate返回一个生成器,给原来迭代器中的每个对象,返回一个(索引,对象)的元组.

    v = dict.fromkeys([1, 2, 3, 4, 5, ], 'test')

    for i in enumerate(v.items(), 99):

    print(type(i), i)

    # 结果:

    # (99, (1, 'test'))

    # (100, (2, 'test'))

    # (101, (3, 'test'))

    # (102, (4, 'test'))

    # (103, (5, 'test'))

    练习

    # 有一个列表 nums = [2,7,11,15,1,8,7],找到列表中任意两个元素相加等于9的元素集合,如[(0,1),(4,5)]

    nums = [2, 7, 11, 15, 1, 8, 7]

    idx = 0

    ls = []

    for i in nums:

    if idx + 1 == len(nums):

    break

    else:

    for j in range(idx + 1, len(nums)):

    if i + nums[j] == 9:

    ls.append((i, nums[j]))

    idx += 1

    print(ls)

    *集合:

    由不同的元素组成,即集合内不能有重复的元素.集合内的元素是无序排列,而且是可哈希的值,也就是不可变类型.这些元素也可以作为字典的key.

    集合的定义,用大括号{}

    直接定义集合用大括号 set_a = {1,2,3,4,5} 集合在生成的过程中会自动去除重复元素.集合无序所以也不支持索引.

    还可以采用set函数生成集合,如果是列表或者元组,会将其中每个元素拆开,如果是字符串,会将每个字符拆开.

    set函数如果对字典使用,只会生成包含键的集合,对列表使用,则列表的元素不能有不可哈希的元素.

    集合的内置方法:

    add(item)

    向集合内增加一个元素,add只接受一个参数

    clear()

    清空集合

    copy()

    这个也是浅复制

    pop()

    弹出集合中的一个元素,如果集合为空,返回KeyError.这个是随机删除.

    remove(item)

    指定删除,参数必须是集合内的元素,如果不存在,返回KeyError

    discard(item)

    指定删除,参数必须是集合内的元素,如果不存在,不做任何事情,也不报错.返回None

    集合的逻辑关系运算

    上边的是单体集合操作的方法,集合主要的用途是进行逻辑判断:

    集合的逻辑运算符有:

    in

    not in

    ==

    !=

    >,>=

    |,|= 合集

    &,&= 交集

    -,-= 差集

    ,=交叉补集

    上边的这些运算,集合也都有方法,详细如下:

    intersection(anotherset)

    求自身和另外一个集合anotherset的交集,符号是&

    union(anotherset)

    求自身和另外一个集合的并集,符号是|

    difference(anotherset)

    求差集,存在于原集合但不存在参数集合的结果.注意差集交换集合的顺序,结果是不同的.符号是-

    symmetric_difference(anotherset)

    交叉补集,首先把两者合并到一起,然后减去两者共有的部分,相当于并集-交集.符号是^

    difference_update(anotherset)

    把属于anotherset里的元素直接从当前集合中去掉.这些带有update的方法都是直接更新调用方法的集合.那些不带update的方法,如果要接受更改之后的集合,需要用另外一个参数去接收,或者用|=这种符号.

    intersection_update(anotherset)

    取交集,然后赋给调用方法的变量,即操作完毕之后,原集合变为交集.

    isdisjoint(s)

    如果两个集合没有交集,返回True,否则返回False

    issubset(s)

    如果s包含当前集合,则返回True,否则返回False.就是看当前集合是否是s的子集,相当于<=

    issuperset(s)

    如果s是当前集合的子集,返回True,否则返回False.相当于>=

    symmetric_difference_update(s)

    update系列方法

    update(s)

    单独的update方法就是用并集更新调用方法的集合.没有union_update方法.另外update可以传可迭代对象,可以将可迭代对象拆解后更新多个值进当前集合.而add只能添加一个值,给add传可迭代对象,会将可迭代对象作为单个元素传进去.

    集合关系运算例子

    # 集合的关系运算

    a = {1, 2, 3, 4, 5, 'a', 'b', 'c'}

    b = {3, 4, 5, 'b', 'c', 'd', 'e'}

    c = {'f','z',32,421}

    d = {'f'}

    print(a.intersection(b))

    print(a | b)

    print(a.union(b))

    print(a-b)

    print(a.difference(b))

    print(b-a)

    print(b.difference(a))

    print(a^b)

    print(a.symmetric_difference(b))

    集合还可以用frozenset()定义不可变集合.这个集合不可修改,除了没有增删改的方法之外,其他方法和关系运算与普通列表一致.IDE里也可以通过ctrl查看frozenset类.

    展开全文
  • 一段简单的遍历中, List.forEach中的临时变量不能用来赋值,因为它是个临时的复制, 求大佬解答下代码出错在那里 首先是一个简单的类: <code class="language-java">public class ImportDataBean { private ...
  • 01一维数组的定义1、一般形式类型符 数组名[常量表达式]2、数组名的命名规则和变量名相同,遵循...02一维数组的引用1、引用形式数组名[下标]2、在定义数组并对其中各元素赋值后,就可以引用数组中的元素。3、应...

    cb56db2afbfbcc734106fc16a75d7772.png

    01一维数组的定义

    1、一般形式

    类型符 数组名[常量表达式]

    2、数组名的命名规则和变量名相同,遵循标识符命名规则。

    3、在定义数组时,需要指定数组中元素的个数,方括号中的常量表达式用来表示元素的个数,即数组长度。

    4、常量表达式中可以包括常量和符号常量,不能包括变量。

    5、例子

    int a[10];

    02一维数组的引用

    1、引用形式

    数组名[下标]

    2、在定义数组并对其中各元素赋值后,就可以引用数组中的元素。

    3、应该注意的是,只能引用数组元素而不能一次整体调用整个数组全部元素的值。

    4、例子

    a[0],就是数组a中序号为0的元素,它和一个简单变量的地位和作用相似。

    03一维数组的初始化

    1、为了使程序简洁,常在定义数组的同时,给各数组元素赋值,这称为数组的初始化。

    2、在定义数组时对全部数组元素赋初值。

    例子:

    int a[10]={0,1,2,3,4,5,6,7,8,9};

    3、可以只给数组中的一部分元素赋值。

    例子:

    int a[10]={0,1,2,3};

    4、可以使一个数组中全部元素值为0。

    例子:

    int a[10]={0,0,0,0,0,0,0,0,0,0};或者int a[10]={0};

    5、如果在定义数值型数组时,指定了数组的长度并对之初始化,凡未被“初始化列表”指定初始化的数组元素,系统会自动把它们初始化为0。

    6、如果是字符型数组,则初始化为'0',如果是指针型数组,则初始化为null,即空指针。

    C语言 | 大写字母A转换为小写amp.weixin.qq.com
    83e7a45442b0e36da47a65e91517ad03.png
    展开全文
  • 这个语句声明的是一个指向对象的引用,名为"s",可以指向类型为String的任何对象,目前指向"Hello world!"这个String类型的对象。这就是真正发生的事情。我们并没有声明一个String对象,我们只是声明了一个只能指向...
  • 一、对象已死吗?...它的做法是为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。 如果有一个引用,被赋值为某...

    一、对象已死吗?

    在堆里面有着几乎所有的对象实例,垃圾收集器在进行回收之前,首先确定这些对象之中哪些还“存活”,哪些已“死去”(即不可能再被任何途径使用的对象)。

    1、引用计数法

    引用计数法(reference counting)。它的做法是为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。

    如果有一个引用,被赋值为某一对象,那么将该对象的引用计数器 +1。如果一个指向某一对象的引用,被赋值为其他值,那么将该对象的引用计数器 -1。也就是说,我们需要截获所有的引用更新操作,并且相应地增减目标对象的引用计数器。

    虽然引用计数法实现简单、判定效率很高,在大部分情况下它都是一个不错的算法。但是,至少主流的 Java 虚拟机没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

    举个例子,假设对象 a 与 b 相互引用,除此之外没有其他引用指向 a 或者 b。在这种情况下,a 和 b 实际上已经死了,但由于它们的引用计数器皆不为 0,在引用计数法的心中,这两个对象还活着。因此,这些循环引用对象所占据的空间将不可回收,从而造成了内存泄露。

    2、可达性分析

    目前 Java 虚拟机的主流垃圾回收器采取的是可达性分析算法。这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,开始向下探索,探索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说,就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。如下图所示,对象 object5、object6、object7 虽然互相有关联,但是它们到 GC Roots 是不可达的,所以它们将会判定为是可回收的对象。
    在这里插入图片描述

    那么什么是 GC Roots 呢?我们可以暂时理解为由堆外指向堆内的引用,一般而言,GC Roots 包括(但不限于)如下几种:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中 JNI (即一般说的 Native 方法)引用的对象。

    3、引用的几种方式

    在 JDK 1.2 以前,Java 中的引用定义很传统:如果 reference 类型的数据中存储的数值代表的另外一块内存的起始地址,就称这块内存代表着一个引用,这种定义很纯粹,但是太过险隘,一个对象在这种定义下只有被引用或没有被引用两种状态。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

    在 JDK 1.2 之后,Java 对引用的概念进行了补充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Week Reference)、虚引用(Phantom Reference)4 种,这 4 种引用强度依次逐渐减弱。

    • 强引用就是指在程序代码之中普遍存在的,类似 “Object obj = new Object()” 这类的引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
    • 软引用是用来描述一些还有用但并非必需的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
    • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
    • 虚引用也称为幻引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

    二、垃圾收集算法

    1、标记-清除算法

    标记-清除(Mark-Sweep)算法是最基础的收集算法,之所以说它是最基础的收集算法,是因为后续的收集的算法都是基于这种思路并对其不足进行改进而得到的。

    它的主要不足有两个:

    • 一个是效率问题,标记和清除两个过程的效率都不高;
    • 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

    该算法执行过程如下图:
    在这里插入图片描述

    2、复制算法

    为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

    只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。现在的商业虚拟机都采用这种收集算法来回收新生代。新生代中划分成 Eden 区、From Survivor 区、To Survivor 区,默认比例 8:1:1,当回收时,将 From Survivor 存活着的对象一次性复制到另一块 To Survivor 空间上,最后清理掉刚才用过的 Survivor 空间,这样只浪费了 10% 的内存,当 Survivor 空间不够用时,需要依赖其它内存(这里指老年代)进行分配担保。

    该算法执行过程如下图:

    在这里插入图片描述

    3、标记-整理算法

    复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端情况,所以在老年代一般不能直接选用这种算法。

    根据老年代的特点,有人提出了另外一种 “标记-整理(Mark-Compact)” 算法,标记过程仍然与 “标记-清除算法” 一样,但后续步骤不是对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

    该算法执行过程如下图:

    在这里插入图片描述

    4、分代收集算法

    当前商业虚拟机的垃圾收集都采用 “分代收集” (Generation Collection)算法,只是根据对象存活周期的不同将内存划分几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量的存活对象的复制成本就可以完成收集。而老年代中因为对象存活率较高、没有额外空间进行分配担保,就必须使用 “标记-清理” 或 “标记-整理” 算法进行回收。

    三、HotSpot 的算法实现

    虽然上面的可达性分析的算法本身很简明,但是在实践中还是有不少其他问题需要解决的。

    比如说,在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)。

    误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则很有可能会直接导致 Java 虚拟机崩溃。

    如何解决呢?请继续往下看:

    1、Stop-the-world

    在 Java 虚拟机里,传统的垃圾回收算法采用的是一种简单粗暴的方式,那便是 Stop-the-world,停止其他非垃圾回收线程的工作,直到完成垃圾回收。这也就造成了垃圾回收所谓的暂停时间(GC pause)。

    Java 虚拟机中的 Stop-the-world 是通过安全点(safepoint)机制来实现的。当 Java 虚拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 Stop-the-world 的线程进行独占的工作。

    当然,安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java 虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。

    2、安全点

    举个例子,当 Java 程序通过 JNI 执行本地代码时,如果这段代码不访问 Java 对象、调用 Java 方法或者返回至原 Java 方法,那么 Java 虚拟机的堆栈不会发生改变,也就代表着这段本地代码可以作为同一个安全点。

    只要不离开这个安全点,Java 虚拟机便能够在垃圾回收的同时,继续运行这段本地代码。

    由于本地代码需要通过 JNI 的 API 来完成上述三个操作,因此 Java 虚拟机仅需在 API 的入口处进行安全点检测(safepoint poll),测试是否有其他线程请求停留在安全点里,便可以在必要的时候挂起当前线程。

    除了执行 JNI 本地代码外,Java 线程还有其他几种状态:解释执行字节码、执行即时编译器生成的机器码和线程阻塞。阻塞的线程由于处于 Java 虚拟机线程调度器的掌控之下,因此属于安全点。

    其他几种状态则是运行状态,需要虚拟机保证在可预见的时间内进入安全点。否则,垃圾回收线程可能长期处于等待所有线程进入安全点的状态,从而变相地提高了垃圾回收的暂停时间。

    对于解释执行来说,字节码与字节码之间皆可作为安全点。Java 虚拟机采取的做法是,当有安全点请求时,执行一条字节码便进行一次安全点检测。

    执行即时编译器生成的机器码则比较复杂。由于这些代码直接运行在底层硬件之上,不受 Java 虚拟机掌控,因此在生成机器码时,即时编译器需要插入安全点检测,以避免机器码长时间没有安全点检测的情况。HotSpot 虚拟机的做法便是在生成代码的方法出口以及非计数循环的循环回边(back-edge)处插入安全点检测。

    那么为什么不在每一条机器码或者每一个机器码基本块处插入安全点检测呢?原因主要有两个:

    • 安全点检测本身也有一定的开销。不过 HotSpot 虚拟机已经将机器码中安全点检测简化为一个内存访问操作。在有安全点请求的情况下,Java 虚拟机会将安全点检测访问的内存所在的页设置为不可读,并且定义一个 segfault 处理器,来截获因访问该不可读内存而触发 segfault 的线程,并将它们挂起。
    • 即时编译器生成的机器码打乱了原本栈桢上的对象分布状况。在进入安全点时,机器码还需提供一些额外的信息,来表明哪些寄存器,或者当前栈帧上的哪些内存空间存放着指向对象的引用,以便垃圾回收器能够枚举 GC Roots。

    由于这些信息需要不少空间来存储,因此即时编译器会尽量避免过多的安全点检测。

    不过,不同的即时编译器插入安全点检测的位置也可能不同。以 Graal 为例,除了上述位置外,它还会在计数循环的循环回边处插入安全点检测。其他的虚拟机也可能选取方法入口而非方法出口来插入安全点检测。

    不管如何,其目的都是在可接受的性能开销以及内存开销之内,避免机器码长时间不进入安全点的情况,间接地减少垃圾回收的暂停时间。

    除了垃圾回收之外,Java 虚拟机其他一些对堆栈内容的一致性有要求的操作也会用到安全点这一机制。我会在涉及的时侯再进行具体的讲解。

    四、内存分配与回收策略

    Java 虚拟机可以给不同代使用不同的回收算法。对于新生代,我们猜测大部分的 Java 对象只存活一小段时间,那么便可以频繁地采用耗时较短的垃圾回收算法,让大部分的垃圾都能够在新生代被回收掉。

    对于老年代,我们猜测大部分的垃圾已经在新生代中被回收了,而在老年代中的对象有大概率会继续存活。当真正触发针对老年代的回收时,则代表这个假设出错了,或者堆的空间已经耗尽了。

    这时候,Java 虚拟机往往需要做一次全堆扫描,耗时也将不计成本。(当然,现代的垃圾回收器都在并发收集的道路上发展,来避免这种全堆扫描的情况。)

    下面针对新生代的 Minor GC 来看看 Java 虚拟机中的堆具体是怎么划分的。

    1、Java 虚拟机的堆划分

    前面提到,Java 虚拟机将堆划分为新生代和老年代。其中,新生代又被划分为 Eden 区,以及两个大小相同的 Survivor 区。

    默认情况下,Java 虚拟机采取的是一种动态分配的策略(对应 Java 虚拟机参数 -XX:+UsePSAdaptiveSurvivorSizePolicy),根据生成对象的速率,以及 Survivor 区的使用情况动态调整 Eden 区和 Survivor 区的比例。

    当然,你也可以通过参数 -XX:SurvivorRatio 来固定这个比例。但是需要注意的是,其中一个 Survivor 区会一直为空,因此比例越低浪费的堆空间将越高。

    在这里插入图片描述

    通常来说,当我们调用 new 指令时,它会在 Eden 区中划出一块作为存储对象的内存。由于堆空间是线程共享的,因此直接在这里边划空间是需要进行同步的。

    否则,将有可能出现两个对象共用一段内存的事故。

    解决办法是:每个线程可以向 Java 虚拟机申请一段连续的内存,比如 2048 字节,作为线程私有的 TLAB。这项技术被称之为 TLAB(Thread Local Allocation Buffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)。

    这个操作需要加锁,线程需要维护两个指针(实际上可能更多,但重要也就两个),一个指向 TLAB 中空余内存的起始位置,一个则指向 TLAB 末尾。

    接下来的 new 指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。

    如果加法后空余内存指针的值仍小于或等于指向末尾的指针,则代表分配成功。否则,TLAB 已经没有足够的空间来满足本次新建操作。这个时候,便需要当前线程重新申请新的 TLAB。

    当 Eden 区的空间耗尽了怎么办?这个时候 Java 虚拟机便会触发一次 Minor GC,来收集新生代的垃圾。存活下来的对象,则会被送到 Survivor 区。

    前面提到,新生代共有两个 Survivor 区,我们分别用 from 和 to 来指代。其中 to 指向的 Survivior 区是空的。

    当发生 Minor GC 时,Eden 区和 from 指向的 Survivor 区中的存活对象会被复制到 to 指向的 Survivor 区中,然后交换 from 和 to 指针,以保证下一次 Minor GC 时,to 指向的 Survivor 区还是空的。

    Java 虚拟机会记录 Survivor 区中的对象一共被来回复制了几次。如果一个对象被复制的次数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold),那么该对象将被晋升(promote)至老年代。另外,如果单个 Survivor 区已经被占用了 50%(对应虚拟机参数 -XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。

    总而言之,当发生 Minor GC 时,我们应用了标记 - 复制算法,将 Survivor 区中的老存活对象晋升到老年代,然后将剩下的存活对象和 Eden 区的存活对象复制到另一个 Survivor 区中。理想情况下,Eden 区中的对象基本都死亡了,那么需要复制的数据将非常少,因此采用这种标记 - 复制算法的效果极好。

    Minor GC 的另外一个好处是不用对整个堆进行垃圾回收。但是,它却有一个问题,那就是老年代的对象可能引用新生代的对象。也就是说,在标记存活对象的时候,我们需要扫描老年代中的对象。如果该对象拥有对新生代对象的引用,那么这个引用也会被作为 GC Roots。

    这样一来,岂不是又做了一次全堆扫描呢?

    2、卡麦

    HotSpot 给出的解决方案是一项叫做卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为 512 字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。

    在进行 Minor GC 的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到 Minor GC 的 GC Roots 里。当完成所有脏卡的扫描之后,Java 虚拟机便会将所有脏卡的标识位清零。

    由于 Minor GC 伴随着存活对象的复制,而复制需要更新指向该对象的引用。因此,在更新引用的同时,我们又会设置引用所在的卡的标识位。这个时候,我们可以确保脏卡中必定包含指向新生代对象的引用。

    在 Minor GC 之前,我们并不能确保脏卡中包含指向新生代对象的引用。其原因和如何设置卡的标识位有关。

    首先,如果想要保证每个可能有指向新生代对象引用的卡都被标记为脏卡,那么 Java 虚拟机需要截获每个引用型实例变量的写操作,并作出对应的写标识位操作。

    这个操作在解释执行器中比较容易实现。但是在即时编译器生成的机器码中,则需要插入额外的逻辑。这也就是所谓的写屏障(write barrier,注意不要和 volatile 字段的写屏障混淆)。

    写屏障需要尽可能地保持简洁。这是因为我们并不希望在每条引用型实例变量的写指令后跟着一大串注入的指令。

    因此,写屏障并不会判断更新后的引用是否指向新生代中的对象,而是宁可错杀,不可放过,一律当成可能指向新生代对象的引用。

    这么一来,写屏障便可精简为下面的伪代码。这里右移 9 位相当于除以 512,Java 虚拟机便是通过这种方式来从地址映射到卡表中的索引的。最终,这段代码会被编译成一条移位指令和一条存储指令。

    CARD_TABLE [this address >> 9] = DIRTY;
    

    虽然写屏障不可避免地带来一些开销,但是它能够加大 Minor GC 的吞吐率( 应用运行时间 /(应用运行时间 + 垃圾回收时间) )。总的来说还是值得的。不过,在高并发环境下,写屏障又带来了虚共享(false sharing)问题。

    在介绍对象内存布局中我曾提到虚共享问题,讲的是几个 volatile 字段出现在同一缓存行里造成的虚共享。这里的虚共享则是卡表中不同卡的标识位之间的虚共享问题。

    在 HotSpot 中,卡表是通过 byte 数组来实现的。对于一个 64 字节的缓存行来说,如果用它来加载部分卡表,那么它将对应 64 张卡,也就是 32KB 的内存。

    如果同时有两个 Java 线程,在这 32KB 内存中进行引用更新操作,那么也将造成存储卡表的同一部分的缓存行的写回、无效化或者同步操作,因而间接影响程序性能。

    为此,HotSpot 引入了一个新的参数 -XX:+UseCondCardMark,来尽量减少写卡表的操作。其伪代码如下所示:

    if (CARD_TABLE [this address >> 9] != DIRTY) 
      CARD_TABLE [this address >> 9] = DIRTY;
    

    五、JVM 中的垃圾回收器

    针对新生代的垃圾回收器共有三个:Serial,Parallel Scavenge 和 Parallel New。这三个采用的都是标记 - 复制算法。其中,Serial 是一个单线程的,Parallel New 可以看成 Serial 的多线程版本。Parallel Scavenge 和 Parallel New 类似,但更加注重吞吐率。此外,Parallel Scavenge 不能与 CMS 一起使用。

    针对老年代的垃圾回收器也有三个:刚刚提到的 Serial Old 和 Parallel Old,以及 CMS。Serial Old 和 Parallel Old 都是标记 - 压缩算法。同样,前者是单线程的,而后者可以看成前者的多线程版本。

    CMS 采用的是标记 - 清除算法,并且是并发的。除了少数几个操作需要 Stop-the-world 之外,它可以在应用程序运行过程中进行垃圾回收。在并发收集失败的情况下,Java 虚拟机会使用其他两个压缩型垃圾回收器进行一次垃圾回收。由于 G1 的出现,CMS 在 Java 9 中已被废弃。

    G1(Garbage First)是一个横跨新生代和老年代的垃圾回收器。实际上,它已经打乱了前面所说的堆结构,直接将堆分成极其多个区域。每个区域都可以充当 Eden 区、Survivor 区或者老年代中的一个。它采用的是标记 - 压缩算法,而且和 CMS 一样都能够在应用程序运行过程中并发地进行垃圾回收。

    G1 能够针对每个细分的区域来进行垃圾回收。在选择进行垃圾回收的区域时,它会优先回收死亡对象较多的区域。这也是 G1 名字的由来。

    展开全文
  • 初始化和赋值引用是有可能被指令重排的,所以产生了疑惑,初始化和赋值引用不是一个东西吗? 接下来,我在网上遍历文章寻找Java对象创建时候的过程以用来解答心中的疑惑。 以下是我暂时理解的东西: 关于Java对象...
  • C语言编程要点

    2017-09-18 00:10:37
    9.8. 为什么用const说明的常量不能用来定义一个数组的初始大小? 145 9.9. 字符串和数组有什么不同? 145 第10章 位(bit)和字节(byte) 147 10.1. 用什么方法存储标志(flag)效率最高? 147 10.2. 什么是“位屏蔽(bit ...
  • 9.8 为什么用const说明的常量不能用来定义一个数组的初始大小? 9.9 字符串和数组有什么不同? 第10章 位(bit)和字节(byte) 10.1 用什么方法存储标志(flag)效率最高? 10.2 什么是“位屏蔽(bit masking)”...
  • 你必须知道的495个C语言问题

    千次下载 热门讨论 2015-05-08 11:09:25
    4.11 C语言可以“按引用传参”吗? 其他指针问题 4.12 我看到了用指针调用函数的不同语法形式。到底怎么回事? 4.13 通用指针类型是什么?当我把函数指针赋向void*类型的时候,编译通不过。 4.14 怎样在整型...
  • 引用Nissan的Xterra的话来说就是PHP可以做到你想让它做到的一切而且无所不能! 1.3 竞争对手:ASP,mod_perl,JSP 我当然不清楚ASP/JSP能做些什么。不过明确的是编写那样的代码有多简单,购买它们会有多昂贵以及它们...
  • 4.11 C语言可以“按引用传参”吗? 50 其他指针问题 50 4.12 我看到了用指针调用函数的不同语法形式。到底怎么回事? 50 4.13 通用指针类型是什么?当我把函数指针赋向void *类型的时候,编译通不过。 51 ...
  • 9.3.6 引用和指针可以一块用 134 9.4 引用应注意的问题 135 9.4.1 引用容易犯的错误 135 9.4.2 引用一个按值返回的堆中对象 138 9.4.3 引用一个按别名返回的堆中对象 140 9.4.4 在哪里创建,就在哪里释放 141 ...
  • 《你必须知道的495个C语言问题》

    热门讨论 2010-03-20 16:41:18
    4.11 C语言可以“按引用传参”吗? 50 其他指针问题 50 4.12 我看到了用指针调用函数的不同语法形式。到底怎么回事? 50 4.13 通用指针类型是什么?当我把函数指针赋向void *类型的时候,编译通不过。 51 ...
  • 如果是的话,一个字节码文件能赋值给一个引用类型的变量吗?(不过有的说“class”是一个字面常量,可以通过类调用来获取类的Class对象)。 (3)class LoadedClass{ static{ System.out.println(“类LoadedClass...
  • 创建数组可以使用构造函数的方式也可以使用字面量的形式,另外可以使用concat从一个数组中复制一个副本出来。数组本身提供了很多方法让开发者使用来操作数组。 - length 数组的长度 - toString 可以返回一个以...
  • 引用可以转换到接口类型或从接口类型转换,instanceof 运算符可以用来决定某对象的类是否实现了接口。 [Page] 39.启动一个线程是用run()还是start()? 答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机...
  • 面向对象方法中的对象,是系统中用来描述客观事物的一个实体,它是用来构成系统的一个基本单位,由一组属性和一组行为构成。 面向对象的方法将数据及对数据的操作方法放在一起,作为一个相互依存、不可分离的整体--...
  • SQLHelper.cs

    热门讨论 2009-03-09 10:22:10
    SqlHelper 类提供了一组静态方法,可以用来向 SQL Server 数据库发出许多各种不同类型的命令。 SqlHelperParameterCache 类提供命令参数缓存功能,可以用来提高性能。该类由许多 Execute 方法(尤其是那些只运行存储...
  • SqlHelper 类提供了一组静态方法,可以用来向 SQL Server 数据库发出许多各种不同类型的命令。 SqlHelperParameterCache 类提供命令参数缓存功能,可以用来提高性能。该类由许多 Execute 方法(尤其是那些只运行存储...
  • 创建新工程,可以是任意工程,我们从最简单的Win32控制台程序开始,为了成功使用oSIP,我们需要引用相关库,调用相关头文件,经过多次试验,发现需要引用如下的库: exosip2.lib osip2.lib osipparser2....
  • 程序员面试宝典高清

    2012-03-16 16:28:17
    第13章 数据结构基础 167 面试时间一般有2小时,其中至少有约20~30分钟是用来回答数据结构相关问题的。链表、数组的排序和逆置是必考的内容之一。 13.1 单链表 167 13.2 双链表 173 13.3 循环链表 176 13.4 队列 ...
  • 其中0≤k≤3,则对x数组元素错误的引用是( )。 A) x[5-3] B) x[k] C) x[k+5] D) x[0] 7.下列语句序列执行后,ch1 的值是( )。 char ch1='A',ch2='W'; if(ch1 + 2 ) ++ch1; A) ‘A' B) ‘B' C) ‘C' D) B 8.下列...
  • C#微软培训教材(高清PDF)

    千次下载 热门讨论 2009-07-30 08:51:17
    7.3 赋值操作符和赋值表达式.64 7.4 关系操作符和关系表达式.65 <<page 2>> page begin==================== 7.5 逻辑操作符和逻辑表达式.68 7.6 位 运 算 .69 7.7 其它特殊操作符 .72 7.8 小 结 ....
  • 我们可以把一个函数赋值给该属性: <pre><code> var myComponent = { bindings: {}, controller: function () { this.$onInit = function() { }; } }; angular .module('app') ....

空空如也

空空如也

1 2
收藏数 36
精华内容 14
关键字:

引用可以用来赋值吗